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.
This commit is contained in:
@@ -22,5 +22,9 @@ export { parseBlockAttrs, type BlockAttrs } from "../../markdown/block-attrs.js"
|
|||||||
// Regex
|
// 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;
|
* 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;
|
||||||
@@ -29,6 +29,35 @@ describe("scanDoc", () => {
|
|||||||
expect(entry.role).toBe("spark-table");
|
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", () => {
|
test("markdown role=spark-table body is converted to CSV", () => {
|
||||||
const md = [
|
const md = [
|
||||||
"```markdown role=spark-table",
|
"```markdown role=spark-table",
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ export function scanDoc(content: string, docPath: string): DocScanResult {
|
|||||||
FENCED_BLOCK_RE,
|
FENCED_BLOCK_RE,
|
||||||
(
|
(
|
||||||
match: string,
|
match: string,
|
||||||
|
_marker: string,
|
||||||
lang: string,
|
lang: string,
|
||||||
infoString: string,
|
infoString: string,
|
||||||
body: string,
|
body: string,
|
||||||
|
|||||||
@@ -70,6 +70,16 @@ describe("code-block-yaml-tag", () => {
|
|||||||
expect(token).toBeUndefined();
|
expect(token).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("supports tilde fences", () => {
|
||||||
|
const html = render("~~~yaml role=tag\ntag: md-deck\nbody: ./cards.csv\n~~~");
|
||||||
|
expect(html).toContain("<md-deck>./cards.csv</md-deck>");
|
||||||
|
});
|
||||||
|
|
||||||
|
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", () => {
|
test("does not swallow yaml blocks without role=tag", () => {
|
||||||
const token = ext.tokenizer("```yaml tag=md-deck\nsize: 54x86\n```");
|
const token = ext.tokenizer("```yaml tag=md-deck\nsize: 54x86\n```");
|
||||||
expect(token).toBeUndefined();
|
expect(token).toBeUndefined();
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { parseBlockAttrs } from "./block-attrs.js";
|
|||||||
* `role=tag` on the info string marks the block (and lets the content
|
* `role=tag` on the info string marks the block (and lets the content
|
||||||
* scanner keep it intact). `tag=` / `id=` / any other `key=value` fence
|
* scanner keep it intact). `tag=` / `id=` / any other `key=value` fence
|
||||||
* attributes override the same keys in the YAML body, so simple cases
|
* 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 {
|
export default function markedCodeBlockYamlTag(): MarkedExtension {
|
||||||
return {
|
return {
|
||||||
@@ -22,13 +22,13 @@ export default function markedCodeBlockYamlTag(): MarkedExtension {
|
|||||||
name: "code-block-yaml-tag",
|
name: "code-block-yaml-tag",
|
||||||
level: "block",
|
level: "block",
|
||||||
start(src: string) {
|
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) {
|
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);
|
const match = rule.exec(src);
|
||||||
if (match) {
|
if (match) {
|
||||||
const yamlContent = match[2]?.trim() || "";
|
const yamlContent = match[3]?.trim() || "";
|
||||||
let yamlProps: Record<string, unknown> = {};
|
let yamlProps: Record<string, unknown> = {};
|
||||||
try {
|
try {
|
||||||
yamlProps =
|
yamlProps =
|
||||||
@@ -39,7 +39,7 @@ export default function markedCodeBlockYamlTag(): MarkedExtension {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fence attributes override YAML body values.
|
// Fence attributes override YAML body values.
|
||||||
const attrs = parseBlockAttrs(match[1] || "");
|
const attrs = parseBlockAttrs(match[2] || "");
|
||||||
const fenceProps: Record<string, unknown> = {
|
const fenceProps: Record<string, unknown> = {
|
||||||
...(attrs.id ? { id: attrs.id } : {}),
|
...(attrs.id ? { id: attrs.id } : {}),
|
||||||
...attrs.extra,
|
...attrs.extra,
|
||||||
|
|||||||
Reference in New Issue
Block a user