From 08bc97bec55293424fe489e73dac1bb70d576996 Mon Sep 17 00:00:00 2001 From: hyper Date: Sat, 12 Sep 2026 23:45:01 +0800 Subject: [PATCH] fix(markdown): accept tilde fences when scanning Fenced block scanning was backtick-only in FENCED_BLOCK_RE (used by scanDoc) and in the yaml role=tag render extension, while stripFencedBlocks and the validator already accepted tildes. Match both marker styles and require the closing fence to reuse the opening marker. --- src/cli/completions/block-scanner.ts | 8 +++++-- src/cli/content-registry.test.ts | 29 ++++++++++++++++++++++++ src/cli/content-registry.ts | 1 + src/markdown/code-block-yaml-tag.test.ts | 10 ++++++++ src/markdown/code-block-yaml-tag.ts | 10 ++++---- 5 files changed, 51 insertions(+), 7 deletions(-) diff --git a/src/cli/completions/block-scanner.ts b/src/cli/completions/block-scanner.ts index 53c4728..b392645 100644 --- a/src/cli/completions/block-scanner.ts +++ b/src/cli/completions/block-scanner.ts @@ -22,5 +22,9 @@ export { parseBlockAttrs, type BlockAttrs } from "../../markdown/block-attrs.js" // Regex // --------------------------------------------------------------------------- -/** Matches fenced code blocks with an info string (at least one attr). */ -export const FENCED_BLOCK_RE = /^```(\w*)\s+(\S.*?)\s*\n([\s\S]*?)```\s*$/gm; \ No newline at end of file +/** + * Matches fenced code blocks with an info string (at least one attr). + * Supports both backtick and tilde fences; the closing fence reuses the + * opening marker character and must be at least as long. + */ +export const FENCED_BLOCK_RE = /^([`~])\1{2,}\s*(\w*)\s+(\S.*?)\s*\n([\s\S]*?)\1{3,}\s*$/gm; \ No newline at end of file diff --git a/src/cli/content-registry.test.ts b/src/cli/content-registry.test.ts index 9393ca6..a13e067 100644 --- a/src/cli/content-registry.test.ts +++ b/src/cli/content-registry.test.ts @@ -29,6 +29,35 @@ describe("scanDoc", () => { expect(entry.role).toBe("spark-table"); }); + test("tilde-fenced role blocks are scanned", () => { + const md = [ + "~~~csv role=spark-table", + "d6,Name", + "1,Alice", + "~~~", + ].join("\n"); + const { stripped, content } = scanDoc(md, "test.md"); + + expect(stripped).toMatch(/^:md-table\[\.\/csv_/); + const [entry] = Object.values(content); + expect(entry.body).toContain("d6,Name"); + }); + + test("tilde-fenced declare blocks are scanned", () => { + const md = [ + "~~~yaml role=declare", + "vars:", + " $hp: 10", + "~~~", + ].join("\n"); + const { stripped, content } = scanDoc(md, "test.md"); + + expect(stripped).toBe(""); + const [entry] = Object.values(content); + expect(entry.kind).toBe("declare"); + expect(entry.body).toContain("$hp: 10"); + }); + test("markdown role=spark-table body is converted to CSV", () => { const md = [ "```markdown role=spark-table", diff --git a/src/cli/content-registry.ts b/src/cli/content-registry.ts index 9e6651e..9dfe783 100644 --- a/src/cli/content-registry.ts +++ b/src/cli/content-registry.ts @@ -124,6 +124,7 @@ export function scanDoc(content: string, docPath: string): DocScanResult { FENCED_BLOCK_RE, ( match: string, + _marker: string, lang: string, infoString: string, body: string, diff --git a/src/markdown/code-block-yaml-tag.test.ts b/src/markdown/code-block-yaml-tag.test.ts index 4b563a4..02c3824 100644 --- a/src/markdown/code-block-yaml-tag.test.ts +++ b/src/markdown/code-block-yaml-tag.test.ts @@ -70,6 +70,16 @@ describe("code-block-yaml-tag", () => { expect(token).toBeUndefined(); }); + test("supports tilde fences", () => { + const html = render("~~~yaml role=tag\ntag: md-deck\nbody: ./cards.csv\n~~~"); + expect(html).toContain("./cards.csv"); + }); + + test("does not swallow plain tilde yaml code blocks", () => { + const token = ext.tokenizer("~~~yaml\nsize: 54x86\n~~~"); + expect(token).toBeUndefined(); + }); + test("does not swallow yaml blocks without role=tag", () => { const token = ext.tokenizer("```yaml tag=md-deck\nsize: 54x86\n```"); expect(token).toBeUndefined(); diff --git a/src/markdown/code-block-yaml-tag.ts b/src/markdown/code-block-yaml-tag.ts index b898c75..6120bc2 100644 --- a/src/markdown/code-block-yaml-tag.ts +++ b/src/markdown/code-block-yaml-tag.ts @@ -13,7 +13,7 @@ import { parseBlockAttrs } from "./block-attrs.js"; * `role=tag` on the info string marks the block (and lets the content * scanner keep it intact). `tag=` / `id=` / any other `key=value` fence * attributes override the same keys in the YAML body, so simple cases - * stay on one line. + * stay on one line. Both backtick and tilde fences are accepted. */ export default function markedCodeBlockYamlTag(): MarkedExtension { return { @@ -22,13 +22,13 @@ export default function markedCodeBlockYamlTag(): MarkedExtension { name: "code-block-yaml-tag", level: "block", start(src: string) { - return src.match(/^```yaml\s+role=tag(?:\s|\n)/m)?.index; + return src.match(/^(?:`{3,}|~{3,})yaml\s+role=tag(?:\s|\n)/m)?.index; }, tokenizer(src: string) { - const rule = /^```yaml\s+role=tag([^\n]*)\n([\s\S]*?)\n```/; + const rule = /^([`~])\1{2,}yaml\s+role=tag([^\n]*)\n([\s\S]*?)\n\1{3,}/; const match = rule.exec(src); if (match) { - const yamlContent = match[2]?.trim() || ""; + const yamlContent = match[3]?.trim() || ""; let yamlProps: Record = {}; try { yamlProps = @@ -39,7 +39,7 @@ export default function markedCodeBlockYamlTag(): MarkedExtension { } // Fence attributes override YAML body values. - const attrs = parseBlockAttrs(match[1] || ""); + const attrs = parseBlockAttrs(match[2] || ""); const fenceProps: Record = { ...(attrs.id ? { id: attrs.id } : {}), ...attrs.extra,