mirror of
https://github.com/morluto/rea
synced 2026-10-26 22:28:27 -04:00
fix(cli): preserve literal evidence in Markdown output
This commit is contained in:
@@ -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, " ").replace(/\t/g, "	"),
|
||||
);
|
||||
|
||||
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");
|
||||
};
|
||||
|
||||
@@ -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> ©";
|
||||
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) <T> &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");
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user