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" 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
|
||||
|
||||
+4
-4
@@ -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: 标签内容
|
||||
```
|
||||
````
|
||||
|
||||
@@ -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<string, string>;
|
||||
}
|
||||
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<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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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", () => {
|
||||
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("<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", () => {
|
||||
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("<md-deck data-config=");
|
||||
expect(html).toContain("./cards.csv</md-deck>");
|
||||
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("<md-deck id=\"my-deck\">./cards.csv</md-deck>");
|
||||
});
|
||||
|
||||
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("<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", () => {
|
||||
@@ -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("<tag-unknown class=\"foo\"></tag-unknown>");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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<string, unknown> = {};
|
||||
const yamlContent = match[2]?.trim() || "";
|
||||
let yamlProps: Record<string, unknown> = {};
|
||||
try {
|
||||
props =
|
||||
yamlProps =
|
||||
(yaml.load(yamlContent) as Record<string, unknown>) || {};
|
||||
} 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<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 { tag, ...rest } = props;
|
||||
|
||||
@@ -68,4 +85,4 @@ export default function markedCodeBlockYamlTag(): MarkedExtension {
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user