fix(cli): preserve literal evidence in Markdown output

This commit is contained in:
morluto
2026-10-27 09:04:08 +08:00
parent bf7721080f
commit 6d7e627783
4 changed files with 78 additions and 11 deletions
+20 -6
View File
@@ -66,6 +66,19 @@ const escapeTableCell = (value: string): string =>
.replace(/[\\|`*[\]~]|_+/g, escapeInlineSyntax)
.replace(/\r\n|\r|\n/g, "<br>");
/** Heading and paragraph facts are literal data, including block syntax. */
const escapeMarkdownText = (value: string): string =>
escapeTableCell(value)
.replace(/#/g, "\\#")
.replace(/^(\s*)([-+=])/, "$1\\$2")
.replace(/^(\s*\d{1,9})([.)])(?=\s)/, "$1\\$2")
.replace(/^[ \t]+/, (indent) =>
indent.replace(/ /g, "&#32;").replace(/\t/g, "&#9;"),
);
const markdownHeading = (path: readonly string[]): string =>
`## ${escapeMarkdownText(path.join("."))}\n\n`;
const renderTable = (
headers: readonly string[],
rows: readonly string[][],
@@ -138,13 +151,13 @@ const formatMarkdown = (
): string => {
if (value == null && path.length === 0) return "";
if (isScalar(value)) {
if (path.length === 0) return String(value);
return `## ${path.join(".")}\n\n${String(value)}`;
if (path.length === 0) return escapeMarkdownText(String(value));
return `${markdownHeading(path)}${escapeMarkdownText(String(value))}`;
}
if (Array.isArray(value)) {
if (isObjectArray(value)) {
const table = formatObjectTable(value);
return path.length === 0 ? table : `## ${path.join(".")}\n\n${table}`;
return path.length === 0 ? table : `${markdownHeading(path)}${table}`;
}
return formatMarkdown(markdownCell(value), path);
}
@@ -163,8 +176,9 @@ const formatMarkdown = (
return entries
.map(([key, child]) => {
const childPath = [...path, key];
const heading = `## ${childPath.join(".")}\n\n`;
if (isScalar(child)) return `${heading}${String(child)}`;
const heading = markdownHeading(childPath);
if (isScalar(child))
return `${heading}${escapeMarkdownText(String(child))}`;
if (isObjectArray(child)) return `${heading}${formatObjectTable(child)}`;
if (
typeof child === "object" &&
@@ -176,7 +190,7 @@ const formatMarkdown = (
? `${heading}${formatKeyValueTable(nested)}`
: formatMarkdown(nested, childPath);
}
return `${heading}${markdownCell(child)}`;
return `${heading}${escapeMarkdownText(markdownCell(child))}`;
})
.join("\n\n");
};
+3 -3
View File
@@ -35,10 +35,10 @@ describe("CLI output argument boundary", () => {
});
it("formats arrays of empty objects without column keys as JSON in Markdown", () => {
expect(renderCliDocument([{}], "md", false)).toBe("[{}]\n");
expect(renderCliDocument([{}, {}], "md", false)).toBe("[{},{}]\n");
expect(renderCliDocument([{}], "md", false)).toBe("\\[{}\\]\n");
expect(renderCliDocument([{}, {}], "md", false)).toBe("\\[{},{}\\]\n");
expect(renderCliDocument({ empty: [{}] }, "md", false)).toBe(
"## empty\n\n[{}]\n",
"## empty\n\n\\[{}\\]\n",
);
});
@@ -100,7 +100,7 @@ it("formats arrays of empty objects without columns as JSON in Markdown", async
const json = await runFilteredCli(value, "json");
expect(JSON.parse(json)).toEqual(value);
const markdown = await runFilteredCli(value, "md");
expect(markdown).toBe("[{},{}]\n");
expect(markdown).toBe("\\[{},{}\\]\n");
});
it.each(
@@ -375,7 +375,7 @@ it.each(["json", "jsonl", "yaml", "toon", "md"] as const)(
.split("\n")
.map((line) => JSON.parse(line)),
).toEqual([1, null, null]);
else if (format === "md") expect(output).toBe("[1,null,null]\n");
else if (format === "md") expect(output).toBe("\\[1,null,null\\]\n");
else expect(output).toBe(`${Formatter.format(selected, format)}\n`);
expect(value).toEqual([{ selected: 1 }, {}, { selected: null }]);
},
@@ -103,3 +103,56 @@ cliTest(
).toEqual(expectedCells);
},
);
cliTest(
"preserves literal scalar, array, and heading Evidence facts",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-markdown-literal-text-");
const path = join(root, "bundle.json");
const literal = "**literal** [source](wrong) <T> &copy;";
const heading = "**key**\n# injected";
const result = {
[heading]: literal,
list: [literal],
bullet: "- literal",
numbered: "1. literal",
rule: "---",
indented: " literal\n- next\n# heading",
nested: { value: 1 },
};
const record = createEvidence(
undefined,
{ id: "fixture", name: "Fixture", version: "1" },
{ operation: "ordinary-json", parameters: {}, result },
);
await writeFile(path, JSON.stringify(createEvidenceBundle([record])));
const base = [
"inspect-evidence-bundle",
JSON.stringify({ path, detail: "complete" }),
"--filter-output",
"records.0.normalized_result",
];
const markdown = await cli.run({ arguments: [...base, "--format", "md"] });
const json = await cli.run({ arguments: [...base, "--json"] });
expect(markdown.exitCode, markdown.stderr).toBe(0);
expect(json.exitCode, json.stderr).toBe(0);
expect(JSON.parse(json.stdout)).toEqual({
records: { "0": { normalized_result: result } },
});
const renderer = await createMarkdownRenderer(root, { linkify: false });
for (const document of [
markdown.stdout,
renderCliDocument(result, "md", false),
]) {
const html = renderer.render(document);
expect(html).toContain(
"**literal** [source](wrong) &lt;T&gt; &amp;copy;",
);
expect(html).toContain("**key**<br># injected");
expect(html).toContain("literal<br>- next<br># heading");
expect(html).not.toMatch(/<(?:strong|em|ul|ol|li|T|pre|hr)(?:\s|>)/u);
expect(html).not.toContain('href="wrong"');
expect(html).not.toContain("<h1");
}
},
);