refactor(markdown): unify yaml tag block syntax on role=tag
Drop legacy yaml/tag info string; tag blocks now require ```yaml role=tag. Add fence-line tag=/id= attributes that override YAML body values, and move parseBlockAttrs to src/markdown/block-attrs.ts so scanner and render extensions share one parser.
This commit is contained in:
@@ -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" 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" }
|
: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
|
```yaml role=tag tag=md-deck
|
||||||
tag: md-deck
|
|
||||||
body: ./names.csv
|
body: ./names.csv
|
||||||
size: 54x86
|
size: 54x86
|
||||||
grid: 5x8
|
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
|
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
|
```yaml role=tag
|
||||||
tag: md-deck
|
tag: md-deck
|
||||||
body: ./names.csv
|
body: ./names.csv
|
||||||
|
|||||||
+4
-4
@@ -489,7 +489,7 @@ track npc#john.dwarf.warrior[hp=4/4 ac=15 name="John"]
|
|||||||
|
|
||||||
## YAML 标签
|
## YAML 标签
|
||||||
|
|
||||||
使用 ```yaml role=tag 代码块创建自定义标签(推荐,`yaml` 语言可被语法高亮):
|
使用 ```yaml role=tag 代码块创建自定义标签(`yaml` 语言可被语法高亮):
|
||||||
|
|
||||||
````markdown
|
````markdown
|
||||||
```yaml role=tag
|
```yaml role=tag
|
||||||
@@ -500,11 +500,11 @@ body: 标签内容
|
|||||||
```
|
```
|
||||||
````
|
````
|
||||||
|
|
||||||
也支持旧写法 ```yaml/tag(等价,但无语法高亮):
|
`tag`、`id` 等简单属性也可以直接写在 fence 行上(fence 行属性优先于 YAML 内容):
|
||||||
|
|
||||||
````markdown
|
````markdown
|
||||||
```yaml/tag
|
```yaml role=tag tag=tag-name id=my-id
|
||||||
tag: tag-name
|
class: custom-class
|
||||||
body: 标签内容
|
body: 标签内容
|
||||||
```
|
```
|
||||||
````
|
````
|
||||||
|
|||||||
@@ -7,20 +7,12 @@
|
|||||||
* - `role` dispatches to content scanners (declare, spark-table)
|
* - `role` dispatches to content scanners (declare, spark-table)
|
||||||
* - `as` controls rendering (none, codeblock, md-table, md-card, md-dice)
|
* - `as` controls rendering (none, codeblock, md-table, md-card, md-dice)
|
||||||
* - `id` is used for cross-references (file paths)
|
* - `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.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
export { parseBlockAttrs, type BlockAttrs } from "../../markdown/block-attrs";
|
||||||
// Types
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
export interface BlockAttrs {
|
|
||||||
lang: string;
|
|
||||||
id?: string;
|
|
||||||
role?: string;
|
|
||||||
as?: string;
|
|
||||||
/** Any other attributes not in the standard set */
|
|
||||||
extra: Record<string, string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Regex
|
// Regex
|
||||||
@@ -29,23 +21,6 @@ export interface BlockAttrs {
|
|||||||
/** Matches fenced code blocks with an info string (at least one attr). */
|
/** 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;
|
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<string, string> = {};
|
|
||||||
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
|
// as resolution
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -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<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parse key="value" and key=value pairs from an attribute string. */
|
||||||
|
export function parseBlockAttrs(info: string): BlockAttrs {
|
||||||
|
const attrs: Record<string, string> = {};
|
||||||
|
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 };
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ function render(src: string): string {
|
|||||||
describe("code-block-yaml-tag", () => {
|
describe("code-block-yaml-tag", () => {
|
||||||
test("renders body and scalar props as attributes", () => {
|
test("renders body and scalar props as attributes", () => {
|
||||||
const html = render(
|
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("<md-deck size=\"54x86\">./cards.csv</md-deck>");
|
expect(html).toContain("<md-deck size=\"54x86\">./cards.csv</md-deck>");
|
||||||
});
|
});
|
||||||
@@ -21,7 +21,7 @@ describe("code-block-yaml-tag", () => {
|
|||||||
test("serializes data-config to a JSON attribute", () => {
|
test("serializes data-config to a JSON attribute", () => {
|
||||||
const html = render(
|
const html = render(
|
||||||
[
|
[
|
||||||
"```yaml/tag",
|
"```yaml role=tag",
|
||||||
"tag: md-deck",
|
"tag: md-deck",
|
||||||
"body: ./cards.csv",
|
"body: ./cards.csv",
|
||||||
"data-config:",
|
"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(
|
const html = render(
|
||||||
[
|
"```yaml role=tag tag=md-deck id=my-deck\nbody: ./cards.csv\n```",
|
||||||
"```yaml role=tag",
|
|
||||||
"tag: md-deck",
|
|
||||||
"body: ./cards.csv",
|
|
||||||
"data-config:",
|
|
||||||
" grid: 5x5",
|
|
||||||
" layers:",
|
|
||||||
" - template: |",
|
|
||||||
" **{{name}}**",
|
|
||||||
" pos: 1,1-5,8",
|
|
||||||
"```",
|
|
||||||
].join("\n"),
|
|
||||||
);
|
);
|
||||||
expect(html).toContain("<md-deck data-config=");
|
expect(html).toContain("<md-deck id=\"my-deck\">./cards.csv</md-deck>");
|
||||||
expect(html).toContain("./cards.csv</md-deck>");
|
});
|
||||||
const m = html.match(/data-config="([^"]*)"/);
|
|
||||||
const config = JSON.parse((m![1] || "").replace(/"/g, '"'));
|
test("fence attributes override YAML body values", () => {
|
||||||
expect(config.layers[0].template).toBe("**{{name}}**\n");
|
const html = render(
|
||||||
|
"```yaml role=tag tag=md-deck size=54x86\ntag: md-other\nsize: 63x88\n```",
|
||||||
|
);
|
||||||
|
expect(html).toContain("<md-deck size=\"54x86\"></md-deck>");
|
||||||
|
expect(html).not.toContain("md-other");
|
||||||
|
expect(html).not.toContain("63x88");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("does not swallow plain yaml code blocks", () => {
|
test("does not swallow plain yaml code blocks", () => {
|
||||||
@@ -76,8 +70,13 @@ describe("code-block-yaml-tag", () => {
|
|||||||
expect(token).toBeUndefined();
|
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", () => {
|
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("<tag-unknown class=\"foo\"></tag-unknown>");
|
expect(html).toContain("<tag-unknown class=\"foo\"></tag-unknown>");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1,6 +1,20 @@
|
|||||||
import type { MarkedExtension } from "marked";
|
import type { MarkedExtension } from "marked";
|
||||||
import yaml from "js-yaml";
|
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 {
|
export default function markedCodeBlockYamlTag(): MarkedExtension {
|
||||||
return {
|
return {
|
||||||
extensions: [
|
extensions: [
|
||||||
@@ -8,27 +22,30 @@ 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 (
|
return src.match(/^```yaml\s+role=tag(?:\s|\n)/m)?.index;
|
||||||
src.match(/^```yaml\/tag\s*\n/m)?.index ??
|
|
||||||
src.match(/^```yaml\s+role=tag(?:\s|\n)/m)?.index
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
tokenizer(src: string) {
|
tokenizer(src: string) {
|
||||||
// Both `yaml/tag` (legacy) and `yaml role=tag` (highlight-friendly)
|
const rule = /^```yaml\s+role=tag([^\n]*)\n([\s\S]*?)\n```/;
|
||||||
// info strings identify a yaml-defined tag block.
|
|
||||||
const rule = /^```yaml(?:\/tag|\s+role=tag[^\n]*)\n([\s\S]*?)\n```/;
|
|
||||||
const match = rule.exec(src);
|
const match = rule.exec(src);
|
||||||
if (match) {
|
if (match) {
|
||||||
const yamlContent = match[1]?.trim() || "";
|
const yamlContent = match[2]?.trim() || "";
|
||||||
let props: Record<string, unknown> = {};
|
let yamlProps: Record<string, unknown> = {};
|
||||||
try {
|
try {
|
||||||
props =
|
yamlProps =
|
||||||
(yaml.load(yamlContent) as Record<string, unknown>) || {};
|
(yaml.load(yamlContent) as Record<string, unknown>) || {};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("YAML Parse Error in code-block-yaml-tag:", 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<string, unknown> = {
|
||||||
|
...(attrs.id ? { id: attrs.id } : {}),
|
||||||
|
...attrs.extra,
|
||||||
|
};
|
||||||
|
const props: Record<string, unknown> = { ...yamlProps, ...fenceProps };
|
||||||
|
|
||||||
const tagName = (props.tag as string) || "tag-unknown";
|
const tagName = (props.tag as string) || "tag-unknown";
|
||||||
const { tag, ...rest } = props;
|
const { tag, ...rest } = props;
|
||||||
|
|
||||||
@@ -68,4 +85,4 @@ export default function markedCodeBlockYamlTag(): MarkedExtension {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user