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,