diff --git a/content/deck-test.md b/content/deck-test.md index b6aa5d5..f961518 100644 --- a/content/deck-test.md +++ b/content/deck-test.md @@ -1,13 +1,12 @@ -# yaml/tag 代码块格式测试 +# yaml role=tag 代码块格式测试 :md-deck[./names.csv]{size="54x86" grid="5x8" bleed="1" padding="2" layers="name1:1,1-2,2f12 name2:4,7-5,8f12s num1:1,3-2,4f12 num2:4,5-5,6f12s"} :md-deck[./names.csv]{size="54x86" grid="5x8" layers="num1:1,1-2,2 num2:4,5-5,6f12s name1:1,1-2,2 name2:1,1-2,2 name1:1,1-2,2" } -## 使用 yaml/tag 语法创建 md-deck +## 使用 yaml role=tag 语法创建 md-deck(fence 行内联 tag,简洁写法) -```yaml/tag -tag: md-deck +```yaml role=tag tag=md-deck body: ./names.csv size: 54x86 grid: 5x8 @@ -16,8 +15,6 @@ padding: 2 layers: name1:1,1-2,2f12 name2:4,7-5,8f12s num1:1,3-2,4f12 num2:4,5-5,6f12s ``` -## 使用 yaml role=tag 语法创建 md-deck(推荐,可语法高亮) - ```yaml role=tag tag: md-deck body: ./names.csv diff --git a/docs/markdown.md b/docs/markdown.md index 189acde..cac4640 100644 --- a/docs/markdown.md +++ b/docs/markdown.md @@ -489,7 +489,7 @@ track npc#john.dwarf.warrior[hp=4/4 ac=15 name="John"] ## YAML 标签 -使用 ```yaml role=tag 代码块创建自定义标签(推荐,`yaml` 语言可被语法高亮): +使用 ```yaml role=tag 代码块创建自定义标签(`yaml` 语言可被语法高亮): ````markdown ```yaml role=tag @@ -500,11 +500,11 @@ body: 标签内容 ``` ```` -也支持旧写法 ```yaml/tag(等价,但无语法高亮): +`tag`、`id` 等简单属性也可以直接写在 fence 行上(fence 行属性优先于 YAML 内容): ````markdown -```yaml/tag -tag: tag-name +```yaml role=tag tag=tag-name id=my-id +class: custom-class body: 标签内容 ``` ```` diff --git a/src/cli/completions/block-scanner.ts b/src/cli/completions/block-scanner.ts index 800765e..09c7c51 100644 --- a/src/cli/completions/block-scanner.ts +++ b/src/cli/completions/block-scanner.ts @@ -7,20 +7,12 @@ * - `role` dispatches to content scanners (declare, spark-table) * - `as` controls rendering (none, codeblock, md-table, md-card, md-dice) * - `id` is used for cross-references (file paths) + * + * Attribute parsing itself lives in `src/markdown/block-attrs.ts` so render + * extensions can share the same syntax. */ -// --------------------------------------------------------------------------- -// Types -// --------------------------------------------------------------------------- - -export interface BlockAttrs { - lang: string; - id?: string; - role?: string; - as?: string; - /** Any other attributes not in the standard set */ - extra: Record; -} +export { parseBlockAttrs, type BlockAttrs } from "../../markdown/block-attrs"; // --------------------------------------------------------------------------- // Regex @@ -29,23 +21,6 @@ export interface BlockAttrs { /** 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; -// --------------------------------------------------------------------------- -// Attribute parsing -// --------------------------------------------------------------------------- - -/** Parse key="value" and key=value pairs from an attribute string. */ -export function parseBlockAttrs(info: string): BlockAttrs { - const attrs: Record = {}; - const re = /(\w+)\s*=\s*("[^"]*"|\S+)/g; - let m: RegExpExecArray | null; - while ((m = re.exec(info)) !== null) { - attrs[m[1]] = m[2].replace(/^"|"$/g, ""); - } - - const { lang, id, role, as, ...extra } = attrs; - return { lang: lang || "", id, role, as, extra }; -} - // --------------------------------------------------------------------------- // as resolution // --------------------------------------------------------------------------- diff --git a/src/markdown/block-attrs.ts b/src/markdown/block-attrs.ts new file mode 100644 index 0000000..665eafb --- /dev/null +++ b/src/markdown/block-attrs.ts @@ -0,0 +1,29 @@ +/** + * Fenced code block attribute parsing — shared by the content scanner + * (CLI + browser registry) and the markdown render extensions. + * + * Parses info strings like: + * ```lang id=xxx role=xxx as=xxx key=value + */ + +export interface BlockAttrs { + lang: string; + id?: string; + role?: string; + as?: string; + /** Any other attributes not in the standard set */ + extra: Record; +} + +/** Parse key="value" and key=value pairs from an attribute string. */ +export function parseBlockAttrs(info: string): BlockAttrs { + const attrs: Record = {}; + const re = /(\w+)\s*=\s*("[^"]*"|\S+)/g; + let m: RegExpExecArray | null; + while ((m = re.exec(info)) !== null) { + attrs[m[1]] = m[2].replace(/^"|"$/g, ""); + } + + const { lang, id, role, as, ...extra } = attrs; + return { lang: lang || "", id, role, as, extra }; +} \ No newline at end of file diff --git a/src/markdown/code-block-yaml-tag.test.ts b/src/markdown/code-block-yaml-tag.test.ts index 07078bb..4b563a4 100644 --- a/src/markdown/code-block-yaml-tag.test.ts +++ b/src/markdown/code-block-yaml-tag.test.ts @@ -13,7 +13,7 @@ function render(src: string): string { describe("code-block-yaml-tag", () => { test("renders body and scalar props as attributes", () => { const html = render( - "```yaml/tag\ntag: md-deck\nbody: ./cards.csv\nsize: 54x86\n```", + "```yaml role=tag\ntag: md-deck\nbody: ./cards.csv\nsize: 54x86\n```", ); expect(html).toContain("./cards.csv"); }); @@ -21,7 +21,7 @@ describe("code-block-yaml-tag", () => { test("serializes data-config to a JSON attribute", () => { const html = render( [ - "```yaml/tag", + "```yaml role=tag", "tag: md-deck", "body: ./cards.csv", "data-config:", @@ -49,26 +49,20 @@ describe("code-block-yaml-tag", () => { }); }); - test("supports yaml role=tag info string (highlight-friendly)", () => { + test("supports tag= and id= on the info string", () => { const html = render( - [ - "```yaml role=tag", - "tag: md-deck", - "body: ./cards.csv", - "data-config:", - " grid: 5x5", - " layers:", - " - template: |", - " **{{name}}**", - " pos: 1,1-5,8", - "```", - ].join("\n"), + "```yaml role=tag tag=md-deck id=my-deck\nbody: ./cards.csv\n```", ); - expect(html).toContain(""); - const m = html.match(/data-config="([^"]*)"/); - const config = JSON.parse((m![1] || "").replace(/"/g, '"')); - expect(config.layers[0].template).toBe("**{{name}}**\n"); + expect(html).toContain("./cards.csv"); + }); + + test("fence attributes override YAML body values", () => { + const html = render( + "```yaml role=tag tag=md-deck size=54x86\ntag: md-other\nsize: 63x88\n```", + ); + expect(html).toContain(""); + expect(html).not.toContain("md-other"); + expect(html).not.toContain("63x88"); }); test("does not swallow plain yaml code blocks", () => { @@ -76,8 +70,13 @@ describe("code-block-yaml-tag", () => { 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(); + }); + test("handles missing tag and body", () => { - const html = render("```yaml/tag\nclass: foo\n```"); + const html = render("```yaml role=tag\nclass: foo\n```"); expect(html).toContain(""); }); -}); +}); \ No newline at end of file diff --git a/src/markdown/code-block-yaml-tag.ts b/src/markdown/code-block-yaml-tag.ts index b8bfb64..9253504 100644 --- a/src/markdown/code-block-yaml-tag.ts +++ b/src/markdown/code-block-yaml-tag.ts @@ -1,6 +1,20 @@ import type { MarkedExtension } from "marked"; import yaml from "js-yaml"; +import { parseBlockAttrs } from "./block-attrs"; +/** + * YAML-defined tag blocks: + * + * ```yaml role=tag tag=md-deck id=my-deck + * body: ./cards.csv + * data-config: ... + * ``` + * + * `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. + */ export default function markedCodeBlockYamlTag(): MarkedExtension { return { extensions: [ @@ -8,27 +22,30 @@ export default function markedCodeBlockYamlTag(): MarkedExtension { name: "code-block-yaml-tag", level: "block", start(src: string) { - return ( - src.match(/^```yaml\/tag\s*\n/m)?.index ?? - src.match(/^```yaml\s+role=tag(?:\s|\n)/m)?.index - ); + return src.match(/^```yaml\s+role=tag(?:\s|\n)/m)?.index; }, tokenizer(src: string) { - // Both `yaml/tag` (legacy) and `yaml role=tag` (highlight-friendly) - // info strings identify a yaml-defined tag block. - const rule = /^```yaml(?:\/tag|\s+role=tag[^\n]*)\n([\s\S]*?)\n```/; + const rule = /^```yaml\s+role=tag([^\n]*)\n([\s\S]*?)\n```/; const match = rule.exec(src); if (match) { - const yamlContent = match[1]?.trim() || ""; - let props: Record = {}; + const yamlContent = match[2]?.trim() || ""; + let yamlProps: Record = {}; try { - props = + yamlProps = (yaml.load(yamlContent) as Record) || {}; } catch (e) { console.error("YAML Parse Error in code-block-yaml-tag:", e); - props = { error: "Invalid YAML content" }; + yamlProps = { error: "Invalid YAML content" }; } + // Fence attributes override YAML body values. + const attrs = parseBlockAttrs(match[1] || ""); + const fenceProps: Record = { + ...(attrs.id ? { id: attrs.id } : {}), + ...attrs.extra, + }; + const props: Record = { ...yamlProps, ...fenceProps }; + const tagName = (props.tag as string) || "tag-unknown"; const { tag, ...rest } = props; @@ -68,4 +85,4 @@ export default function markedCodeBlockYamlTag(): MarkedExtension { }, ], }; -} +} \ No newline at end of file