diff --git a/src/cli/cliOutputFormatter.ts b/src/cli/cliOutputFormatter.ts
index 049600d4ed..ed4cd5910b 100644
--- a/src/cli/cliOutputFormatter.ts
+++ b/src/cli/cliOutputFormatter.ts
@@ -323,21 +323,36 @@ const escapeTableCell = (value: string): string =>
(character) =>
`\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`,
)
- .replace(/&/g, "&")
- .replace(//g, ">")
- .replace(/[\\|`*[\]~]|_+/g, escapeInlineSyntax)
+ .replace(/[\\|`*[\]~{}#]|_+/g, escapeInlineSyntax)
+ // Encode renderer-extension introducers at the same boundary as HTML
+ // characters. Generated entities must not themselves be re-escaped.
+ .replace(/[&<>:]|;(?=-?[)(])/g, (character) => {
+ switch (character) {
+ case "&":
+ return "&";
+ case "<":
+ return "<";
+ case ">":
+ return ">";
+ case ":":
+ return ":";
+ default:
+ return ";";
+ }
+ })
+ .replace(/([xXB8]-)\)/g, "$1)")
.replace(/\r\n|\r|\n/g, "
");
+const encodeMarkdownWhitespace = (whitespace: string): string =>
+ whitespace.replace(/ /g, " ").replace(/\t/g, " ");
+
/** 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, " ").replace(/\t/g, " "),
- );
+ .replace(/^[ \t]+/, encodeMarkdownWhitespace)
+ .replace(/[ \t]+$/u, encodeMarkdownWhitespace);
const markdownHeading = (path: readonly string[]): string =>
`## ${escapeMarkdownText(path.join("."))}\n\n`;
@@ -348,8 +363,8 @@ const renderTable = (
): string => {
if (headers.length === 0) return "";
const encodeTableCell = (value: string): string =>
- escapeTableCell(value).replace(/^ +| +$/g, (spaces) =>
- " ".repeat(spaces.length),
+ escapeTableCell(value).replace(/^[ \t]+|[ \t]+$/g, (spaces) =>
+ encodeMarkdownWhitespace(spaces),
);
const escapedHeaders = headers.map(encodeTableCell);
const widths = escapedHeaders.map((header) => header.length);
diff --git a/tests/boundary/cli/cliOutput.test.ts b/tests/boundary/cli/cliOutput.test.ts
index 984f0c4cea..40b1299153 100644
--- a/tests/boundary/cli/cliOutput.test.ts
+++ b/tests/boundary/cli/cliOutput.test.ts
@@ -113,14 +113,6 @@ describe("CLI output argument boundary", () => {
expect(JSON.parse(renderCliDocument(value, "json", false))).toEqual(value);
});
- 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({ empty: [{}] }, "md", false)).toBe(
- "## empty\n\n\\[{}\\]\n",
- );
- });
-
it("rejects token windows that would corrupt structured output", () => {
for (const format of ["json", "jsonl", "yaml"] as const) {
const validation = parseCliOutputArguments([
@@ -174,6 +166,16 @@ 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({ empty: [{}] }, "md", false)).toBe(
+ "## empty\n\n\\[\\{\\}\\]\n",
+ );
+});
+
it("preserves flag-shaped values consumed by valued output options", () => {
expect(
adaptCliOutputArgumentsForIncur([
diff --git a/tests/boundary/cli/filteredArrayOutput.test.ts b/tests/boundary/cli/filteredArrayOutput.test.ts
index 33fd871f0a..9d2b13aa90 100644
--- a/tests/boundary/cli/filteredArrayOutput.test.ts
+++ b/tests/boundary/cli/filteredArrayOutput.test.ts
@@ -242,7 +242,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(
diff --git a/tests/boundary/cli/markdownLiteralCells.test.ts b/tests/boundary/cli/markdownLiteralCells.test.ts
index 559b7e0e32..4942240f23 100644
--- a/tests/boundary/cli/markdownLiteralCells.test.ts
+++ b/tests/boundary/cli/markdownLiteralCells.test.ts
@@ -95,11 +95,15 @@ cliTest(
" leading ": " leading ",
leading: "leading",
"trailing ": "trailing ",
+ "\tleading tab": "trailing tab\t",
+ " mixed\t ": " mixed\t ",
" ": " ",
" ": " ",
"": "",
" δΈζπ ": " δΈζπ ",
" © a|b ": " © a|b ",
+ "{.hidden}": "literal {.hidden}",
+ "{name=value}": "{name=value}",
};
const row = {
[literal]: literal,
@@ -168,6 +172,96 @@ cliTest(
},
);
+cliTest(
+ "preserves capability provider identities in rendered Markdown",
+ async ({ cli }) => {
+ const json = await cli.run({ arguments: ["capabilities", "--json"] });
+ const markdown = await cli.run({
+ arguments: ["capabilities", "--format", "md"],
+ });
+ expect(json.exitCode, json.stderr).toBe(0);
+ expect(markdown.exitCode, markdown.stderr).toBe(0);
+ const data = JSON.parse(json.stdout) as {
+ capabilities: { provider: unknown }[];
+ };
+ const renderer = await createMarkdownRenderer(process.cwd(), {
+ linkify: false,
+ });
+ expect(
+ tableColumnByHeader(renderer.render(markdown.stdout), "provider"),
+ ).toEqual(
+ data.capabilities.map(({ provider }) => JSON.stringify(provider)),
+ );
+ },
+);
+
+cliTest(
+ "keeps scalar markers, edge spaces, and emoji-like names literal in rendered reports",
+ async ({ cli }) => {
+ const root = await createTestTempDirectory("rea-markdown-literal-markers-");
+ const path = join(root, "bundle.json");
+ const result = {
+ container: "::: details",
+ trailing: "archive ",
+ trailingTab: "archive\t",
+ trailingMixed: "archive \t ",
+ emoji: ":)",
+ shortcuts: ":smile: :D :-( ;) ;-() x-) X-) 8-) B-) <3 &",
+ rows: [{ value: ":)", nested: { path: ";) :smile: 8-)" } }],
+ after: "still visible",
+ };
+ 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 });
+ const html = renderer.render(markdown.stdout);
+ const text = (node: DefaultTreeAdapterTypes.Node): string =>
+ node.nodeName === "#text" && "value" in node
+ ? node.value
+ : "childNodes" in node
+ ? node.childNodes.map(text).join("")
+ : "";
+ const children = parseFragment(html).childNodes;
+ for (const [key, value] of Object.entries(result).filter(
+ ([, value]) => typeof value === "string",
+ )) {
+ const index = children.findIndex(
+ (node) =>
+ "tagName" in node &&
+ node.tagName === "h2" &&
+ text(node).replaceAll("\u200b", "").trim().endsWith(`.${key}`),
+ );
+ expect(index, key).toBeGreaterThanOrEqual(0);
+ const body = children.slice(index + 1).find((node) => "tagName" in node);
+ expect(body && "tagName" in body ? body.tagName : undefined, key).toBe(
+ "p",
+ );
+ expect(body ? text(body) : undefined, key).toBe(value);
+ }
+ expect(html).not.toMatch(/<(?:details|ol|li)(?:\s|>)/u);
+ expect(renderedCells(html)).toContain(
+ JSON.stringify(result.rows[0]?.nested),
+ );
+ expect(renderedCells(html)).toContain(":)");
+ },
+);
+
cliTest(
"preserves safetensors identifier edge spaces in rendered CLI tables",
async ({ cli }) => {
@@ -258,9 +352,13 @@ cliTest(
renderCliDocument(result, "md", false),
]) {
const html = renderer.render(document);
- expect(html).toContain(
- "**literal** [source](wrong) <T> ©",
- );
+ const text = (node: DefaultTreeAdapterTypes.Node): string =>
+ node.nodeName === "#text" && "value" in node
+ ? node.value
+ : "childNodes" in node
+ ? node.childNodes.map(text).join("")
+ : "";
+ expect(text(parseFragment(html))).toContain(literal);
expect(html).toContain("**key**
# injected");
expect(html).toContain("literal
- next
# heading");
expect(html).not.toMatch(/<(?:strong|em|ul|ol|li|T|pre|hr)(?:\s|>)/u);
diff --git a/tests/boundary/cli/markdownNestedFacts.test.ts b/tests/boundary/cli/markdownNestedFacts.test.ts
index 07408de1a2..d5d9daaffc 100644
--- a/tests/boundary/cli/markdownNestedFacts.test.ts
+++ b/tests/boundary/cli/markdownNestedFacts.test.ts
@@ -2,6 +2,8 @@ import { join } from "node:path";
import { Writable } from "node:stream";
import { Cli } from "incur";
import { expect, it } from "vitest";
+import { createMarkdownRenderer } from "vitepress";
+import { parseFragment, type DefaultTreeAdapterTypes } from "parse5";
import {
parseCliOutputArguments,
@@ -70,7 +72,7 @@ it("keeps source keys literal in Markdown headings through CLI result output", a
expect(JSON.stringify(value)).toBe(original);
});
-it("retains nested facts in standalone Markdown table cells and mixed arrays", () => {
+it("retains nested facts in standalone Markdown table cells and mixed arrays", async () => {
const value = [
{
details: { path: "alpha.txt" },
@@ -79,13 +81,32 @@ it("retains nested facts in standalone Markdown table cells and mixed arrays", (
},
];
const markdown = renderCliDocument(value, "md", false);
- expect(markdown).toContain('{"path":"alpha.txt"}');
- expect(markdown).toContain('\\[{"path":"sub/beta.txt"}\\]');
- expect(markdown).toContain("caller\\|value");
- expect(markdown).not.toContain("[object Object]");
- expect(
- renderCliDocument({ mixed: [1, { path: "retained" }] }, "md", false),
- ).toContain('{"path":"retained"}');
+ const renderer = await createMarkdownRenderer(process.cwd(), {
+ linkify: false,
+ });
+ const text = (node: DefaultTreeAdapterTypes.Node): string =>
+ node.nodeName === "#text" && "value" in node
+ ? node.value
+ : "childNodes" in node
+ ? node.childNodes.map(text).join("")
+ : "";
+ const rendered = text(parseFragment(renderer.render(markdown)));
+ expect(rendered).toContain('{"path":"alpha.txt"}');
+ expect(rendered).toContain('[{"path":"sub/beta.txt"}]');
+ const literal = "colon: :) ;-) x) #tag :";
+ const document = renderCliDocument({ [literal]: literal }, "md", false);
+ const renderedLiteral = text(parseFragment(renderer.render(document)));
+ expect(renderedLiteral.split(literal)).toHaveLength(3);
+ expect(rendered).toContain("caller|value");
+ expect(rendered).not.toContain("[object Object]");
+ const mixed = renderCliDocument(
+ { mixed: [1, { path: "retained" }] },
+ "md",
+ false,
+ );
+ expect(text(parseFragment(renderer.render(mixed)))).toContain(
+ '{"path":"retained"}',
+ );
expect(JSON.parse(renderCliDocument(value, "json", false))).toEqual(value);
});