feat: md渲染优化

This commit is contained in:
2026-07-15 14:40:00 +08:00
parent a03e1b4819
commit 4dd4944107
3 changed files with 108 additions and 33 deletions

View File

@@ -1,5 +1,6 @@
package com.codechecker.cache.report; package com.codechecker.cache.report;
import com.codechecker.cache.diff.ChangeType;
import com.codechecker.cache.diff.SchemaChange; import com.codechecker.cache.diff.SchemaChange;
import com.codechecker.cache.diff.Severity; import com.codechecker.cache.diff.Severity;
@@ -14,7 +15,7 @@ import java.util.Set;
/** /**
* 将 {@link CheckReport} 渲染为企微 Markdown / 控制台文本。 * 将 {@link CheckReport} 渲染为企微 Markdown / 控制台文本。
* <ul> * <ul>
* <li>企微:按 key 展示位置/类型/序列化骨架变更,不含字段明细,不分 P0/P1/P2</li> * <li>企微:按 key 展示位置/类型/序列化骨架变更;类型变更另附摘要行,不分 P0/P1/P2</li>
* <li>未解析 key 展示源码表达式 + 灰色「key 无法解析」提示</li> * <li>未解析 key 展示源码表达式 + 灰色「key 无法解析」提示</li>
* <li>多 key 优先拼成一条;超过企微上限则按 key 拆成多条</li> * <li>多 key 优先拼成一条;超过企微上限则按 key 拆成多条</li>
* <li>CI先打字段明细再完整输出企微 Markdown拆分后的每条</li> * <li>CI先打字段明细再完整输出企微 Markdown拆分后的每条</li>
@@ -129,22 +130,84 @@ public class ReportBuilder {
String newJson = nvl(kc.getNewSkeletonJson()); String newJson = nvl(kc.getNewSkeletonJson());
Set<String> oldHighlight = SkeletonAnnotator.pathsForOldSkeleton(kc.getFieldDetails()); Set<String> oldHighlight = SkeletonAnnotator.pathsForOldSkeleton(kc.getFieldDetails());
Set<String> newGreen = SkeletonAnnotator.pathsForNewSkeleton(kc.getFieldDetails()); Set<String> newGreen = SkeletonAnnotator.pathsForNewSkeleton(kc.getFieldDetails());
Set<String> newOrange = SkeletonAnnotator.pathsForTypeChanged(kc.getFieldDetails()); Set<String> typeGray = SkeletonAnnotator.pathsForTypeChanged(kc.getFieldDetails());
String oldRendered = oldJson.isEmpty() String oldRendered = oldJson.isEmpty()
? "" : "" + SkeletonAnnotator.annotateOldForWecom(oldJson, oldHighlight) + ""; ? "" : "" + SkeletonAnnotator.annotateOldForWecom(oldJson, oldHighlight, typeGray) + "";
String newRendered = newJson.isEmpty() String newRendered = newJson.isEmpty()
? "" : "" + SkeletonAnnotator.annotateNewForWecom(newJson, newGreen, newOrange) + ""; ? "" : "" + SkeletonAnnotator.annotateNewForWecom(newJson, newGreen, typeGray) + "";
if (oldJson.isEmpty() && !newJson.isEmpty()) { if (oldJson.isEmpty() && !newJson.isEmpty()) {
sb.append(" > **value 新增为:** ").append(newRendered).append("\n\n"); sb.append(" > **value 新增为:** ").append(newRendered).append('\n');
} else if (!oldJson.isEmpty() && newJson.isEmpty()) { } else if (!oldJson.isEmpty() && newJson.isEmpty()) {
sb.append(" > **value 原结构:** ").append(oldRendered).append("(已删除写入)\n\n"); sb.append(" > **value 原结构:** ").append(oldRendered).append("(已删除写入)\n");
} else { } else {
sb.append(" > **value值由** ").append(oldRendered).append('\n'); sb.append(" > **value值由** ").append(oldRendered).append('\n');
sb.append(" > **变更为:** ").append(newRendered).append("\n\n"); sb.append(" > **变更为:** ").append(newRendered).append('\n');
} }
appendTypeChangeSummary(sb, kc.getFieldDetails());
sb.append('\n');
return sb.toString(); return sb.toString();
} }
/**
* 类型变更单独一行摘要,避免仅靠骨架颜色/占位难以识别。
* 例:{@code > **类型变更**: amount <font color="warning">BigDecimal → Integer</font>}
*/
private void appendTypeChangeSummary(StringBuilder sb, List<SchemaChange> details) {
if (details == null || details.isEmpty()) {
return;
}
List<String> parts = new ArrayList<>();
for (SchemaChange c : details) {
if (c == null || c.getChangeType() != ChangeType.TYPE_CHANGED) {
continue;
}
String path = nvl(c.getFieldPath());
if (path.isEmpty()) {
continue;
}
String oldType = displayJavaType(c.getOldValue());
String newType = displayJavaType(c.getNewValue());
StringBuilder part = new StringBuilder();
// 字段名用普通文本(避免反引号被企微渲染成色块)
part.append(path);
if (!oldType.isEmpty() || !newType.isEmpty()) {
part.append(" <font color=\"warning\">")
.append(oldType.isEmpty() ? "?" : oldType)
.append("")
.append(newType.isEmpty() ? "?" : newType)
.append("</font>");
}
parts.add(part.toString());
}
if (parts.isEmpty()) {
return;
}
sb.append(" > **类型变更**: ");
for (int i = 0; i < parts.size(); i++) {
if (i > 0) {
sb.append("");
}
sb.append(parts.get(i));
}
sb.append('\n');
}
/** 将 {@code BigDecimal/NUMBER} 转为展示用 {@code BigDecimal}。 */
private static String displayJavaType(String typeLabel) {
if (typeLabel == null) {
return "";
}
String s = typeLabel.trim();
if (s.isEmpty()) {
return "";
}
int slash = s.indexOf('/');
if (slash > 0) {
s = s.substring(0, slash).trim();
}
return s;
}
/** /**
* Key 行:展示解析后的模式或未解析时的源码表达式;未解析时追加灰色提示。 * Key 行:展示解析后的模式或未解析时的源码表达式;未解析时追加灰色提示。
* 反引号仅包裹 key 文本,避免与加粗/颜色嵌套冲突。 * 反引号仅包裹 key 文本,避免与加粗/颜色嵌套冲突。
@@ -160,6 +223,7 @@ public class ReportBuilder {
sb.append("- Key --> `").append(keyText).append('`'); sb.append("- Key --> `").append(keyText).append('`');
if (unresolved) { if (unresolved) {
sb.append(" <font color=\"comment\">key 无法解析)</font>"); sb.append(" <font color=\"comment\">key 无法解析)</font>");
// sb.append(" key 无法解析)");
} }
sb.append('\n'); sb.append('\n');
} }

View File

@@ -15,22 +15,24 @@ import java.util.Set;
* <li>删除字段 → 旧骨架,橙色 {@code warning}</li> * <li>删除字段 → 旧骨架,橙色 {@code warning}</li>
* <li>新增字段 / 新增包装层 → 新骨架,绿色 {@code info}</li> * <li>新增字段 / 新增包装层 → 新骨架,绿色 {@code info}</li>
* <li>路径迁移 → 旧路径橙、新路径绿</li> * <li>路径迁移 → 旧路径橙、新路径绿</li>
* <li>类型变更 → 新旧骨架均为色 {@code warning}(提高识别度)</li> * <li>类型变更(修改类)→ 新旧骨架均为色 {@code comment}</li>
* </ul> * </ul>
*/ */
final class SkeletonAnnotator { final class SkeletonAnnotator {
/** 企微橙:删除 / 类型变更 / 旧侧变更 */ /** 企微橙:删除 / 路径迁移旧侧 */
static final String COLOR_REMOVE = "warning"; static final String COLOR_REMOVE = "warning";
/** 企微绿:新增 / 新侧路径迁移 */ /** 企微绿:新增 / 新侧路径迁移 */
static final String COLOR_ADD = "info"; static final String COLOR_ADD = "info";
/** 企微灰:类型变更(修改类) */
static final String COLOR_TYPE = "comment";
private static final String FONT_CLOSE = "</font>"; private static final String FONT_CLOSE = "</font>";
private SkeletonAnnotator() { private SkeletonAnnotator() {
} }
/** 旧骨架应橙色标注的路径:删除、路径迁移旧侧类型变更。 */ /** 旧骨架应橙色标注的路径:删除、路径迁移旧侧(不含类型变更。 */
static Set<String> pathsForOldSkeleton(List<SchemaChange> details) { static Set<String> pathsForOldSkeleton(List<SchemaChange> details) {
Set<String> paths = new LinkedHashSet<>(); Set<String> paths = new LinkedHashSet<>();
if (details == null) { if (details == null) {
@@ -40,8 +42,7 @@ final class SkeletonAnnotator {
if (c == null || c.getChangeType() == null) { if (c == null || c.getChangeType() == null) {
continue; continue;
} }
if (c.getChangeType() == ChangeType.FIELD_REMOVED if (c.getChangeType() == ChangeType.FIELD_REMOVED) {
|| c.getChangeType() == ChangeType.TYPE_CHANGED) {
addIfPresent(paths, c.getFieldPath()); addIfPresent(paths, c.getFieldPath());
} else if (c.getChangeType() == ChangeType.FIELD_PATH_MOVED) { } else if (c.getChangeType() == ChangeType.FIELD_PATH_MOVED) {
addIfPresent(paths, c.getOldValue()); addIfPresent(paths, c.getOldValue());
@@ -73,7 +74,7 @@ final class SkeletonAnnotator {
return paths; return paths;
} }
/** 新骨架应橙色标注的路径:类型变更(与旧侧同色,便于识别)。 */ /** 类型变更路径:新旧骨架均用灰色标注。 */
static Set<String> pathsForTypeChanged(List<SchemaChange> details) { static Set<String> pathsForTypeChanged(List<SchemaChange> details) {
Set<String> paths = new LinkedHashSet<>(); Set<String> paths = new LinkedHashSet<>();
if (details == null) { if (details == null) {
@@ -88,30 +89,35 @@ final class SkeletonAnnotator {
} }
/** /**
* 标注旧骨架改动字段(删除 / 类型变更 → 橙色) * 标注旧骨架:删除等橙色;类型变更灰色
*/ */
static String annotateOldForWecom(String json, Set<String> highlightPaths) { static String annotateOldForWecom(String json, Set<String> orangePaths) {
return annotateForWecom(json, highlightPaths, COLOR_REMOVE); return annotateOldForWecom(json, orangePaths, null);
}
static String annotateOldForWecom(String json, Set<String> orangePaths, Set<String> grayPaths) {
String result = annotateForWecom(json, grayPaths, COLOR_TYPE);
return annotateForWecom(result, orangePaths, COLOR_REMOVE);
} }
/** /**
* 标注新骨架:类型变更先着橙色,其余新增绿色。 * 标注新骨架:类型变更灰色,新增绿色。
*/ */
static String annotateNewForWecom(String json, Set<String> greenPaths) { static String annotateNewForWecom(String json, Set<String> greenPaths) {
return annotateNewForWecom(json, greenPaths, null); return annotateNewForWecom(json, greenPaths, null);
} }
/** /**
* 标注新骨架改动字段(类型变更 → 色;新增 / 迁移新侧 → 绿色)。 * 标注新骨架改动字段(类型变更 → 色;新增 / 迁移新侧 → 绿色)。
*/ */
static String annotateNewForWecom(String json, Set<String> greenPaths, Set<String> orangePaths) { static String annotateNewForWecom(String json, Set<String> greenPaths, Set<String> grayTypePaths) {
String result = annotateForWecom(json, orangePaths, COLOR_REMOVE); String result = annotateForWecom(json, grayTypePaths, COLOR_TYPE);
return annotateForWecom(result, greenPaths, COLOR_ADD); return annotateForWecom(result, greenPaths, COLOR_ADD);
} }
/** /**
* 仅标注改动字段:其余正文保持普通文本。 * 仅标注改动字段:其余正文保持普通文本。
* 改动片段格式:{@code <font color="warning|info">"field":value</font>} * 改动片段格式:{@code <font color="warning|info|comment">"field":value</font>}
*/ */
static String annotateForWecom(String json, Set<String> highlightPaths) { static String annotateForWecom(String json, Set<String> highlightPaths) {
return annotateForWecom(json, highlightPaths, COLOR_REMOVE); return annotateForWecom(json, highlightPaths, COLOR_REMOVE);

View File

@@ -112,7 +112,7 @@ class ReportBuilderTest {
} }
@Test @Test
void typeChangedHighlightedOrangeInBothSkeletons() { void typeChangedHighlightedGrayInBothSkeletons() {
SchemaChange typeChanged = new SchemaChange(ChangeType.TYPE_CHANGED); SchemaChange typeChanged = new SchemaChange(ChangeType.TYPE_CHANGED);
typeChanged.setFieldPath("amount"); typeChanged.setFieldPath("amount");
typeChanged.setOldValue("BigDecimal/NUMBER"); typeChanged.setOldValue("BigDecimal/NUMBER");
@@ -125,20 +125,22 @@ class ReportBuilderTest {
Set<String> oldPaths = SkeletonAnnotator.pathsForOldSkeleton(details); Set<String> oldPaths = SkeletonAnnotator.pathsForOldSkeleton(details);
Set<String> newGreen = SkeletonAnnotator.pathsForNewSkeleton(details); Set<String> newGreen = SkeletonAnnotator.pathsForNewSkeleton(details);
Set<String> newOrange = SkeletonAnnotator.pathsForTypeChanged(details); Set<String> typeGray = SkeletonAnnotator.pathsForTypeChanged(details);
assertEquals(Collections.singleton("amount"), oldPaths); assertTrue(oldPaths.isEmpty());
assertTrue(newGreen.isEmpty()); assertTrue(newGreen.isEmpty());
assertEquals(Collections.singleton("amount"), newOrange); assertEquals(Collections.singleton("amount"), typeGray);
String oldMd = SkeletonAnnotator.annotateOldForWecom(oldJson, oldPaths); String oldMd = SkeletonAnnotator.annotateOldForWecom(oldJson, oldPaths, typeGray);
String newMd = SkeletonAnnotator.annotateNewForWecom(newJson, newGreen, newOrange); String newMd = SkeletonAnnotator.annotateNewForWecom(newJson, newGreen, typeGray);
assertTrue(oldMd.contains("<font color=\"warning\">\"amount\":0</font>")); assertTrue(oldMd.contains("<font color=\"comment\">\"amount\":0</font>"));
assertFalse(oldMd.contains("<font color=\"warning\">\"name\":\"\"</font>")); assertFalse(oldMd.contains("<font color=\"warning\">\"amount\""));
assertTrue(newMd.contains("<font color=\"warning\">\"amount\":\"\"</font>")); assertFalse(oldMd.contains("<font color=\"comment\">\"name\":\"\"</font>"));
assertTrue(newMd.contains("<font color=\"comment\">\"amount\":\"\"</font>"));
assertFalse(newMd.contains("<font color=\"info\">\"amount\"")); assertFalse(newMd.contains("<font color=\"info\">\"amount\""));
assertFalse(newMd.contains("<font color=\"warning\">\"name\":\"\"</font>")); assertFalse(newMd.contains("<font color=\"warning\">\"amount\""));
assertFalse(newMd.contains("<font color=\"comment\">\"name\":\"\"</font>"));
CheckReport report = baseReport(); CheckReport report = baseReport();
KeyStructureChange key = new KeyStructureChange(); KeyStructureChange key = new KeyStructureChange();
@@ -157,9 +159,12 @@ class ReportBuilderTest {
assertTrue(newSection > oldSection); assertTrue(newSection > oldSection);
String oldPart = md.substring(oldSection, newSection); String oldPart = md.substring(oldSection, newSection);
String newPart = md.substring(newSection); String newPart = md.substring(newSection);
assertTrue(oldPart.contains("<font color=\"warning\">\"amount\":0</font>")); assertTrue(oldPart.contains("<font color=\"comment\">\"amount\":0</font>"));
assertTrue(newPart.contains("<font color=\"warning\">\"amount\":\"\"</font>")); assertTrue(newPart.contains("<font color=\"comment\">\"amount\":\"\"</font>"));
assertFalse(newPart.contains("<font color=\"info\">\"amount\"")); assertFalse(newPart.contains("<font color=\"info\">\"amount\""));
assertFalse(oldPart.contains("<font color=\"warning\">\"amount\""));
assertTrue(md.contains("> **类型变更**: amount <font color=\"warning\">BigDecimal → String</font>"),
"应包含类型变更摘要行,实际 markdown:\n" + md);
} }
@Test @Test