zh CN Tutorial Blog Renderer - chiba233/yume-dsl-token-walker GitHub Wiki
在这个教程里,你将从零构建一个完整的 DSL → HTML 博客渲染管线——支持 bold、italic、link、code block,还有主题感知样式。
你将学到:
- 用
yume-dsl-rich-text搭建 parser - 用
createRuleset+fromHandlerMap构建 ruleset - 用
env注入运行时上下文(主题、语言) - 处理管道参数(link 的 URL + 显示文本)
- 用
flattenText提取纯文本(搜索索引、预览) - 用
wrapHandlers统一容器包装
前置条件: npm install yume-dsl-rich-text yume-dsl-token-walker
先定义博客支持哪些标签:
// parser.ts
import {
createParser,
createSimpleInlineHandlers,
createSimpleBlockHandlers,
createSimpleRawHandlers,
createPipeHandlers,
declareMultilineTags,
} from "yume-dsl-rich-text";
export const parser = createParser({
handlers: {
// 简单 inline:$$bold(text)$$, $$italic(text)$$
...createSimpleInlineHandlers(["bold", "italic", "underline", "strike"]),
// 带 arg 的 block:$$info(title)* content *end$$
...createSimpleBlockHandlers(["info", "warning"]),
// 带 arg 的 raw:$$code(ts)% content %end$$
...createSimpleRawHandlers(["code"]),
// 管道参数:$$link(url | display text)$$
...createPipeHandlers({
link: {
inline: (args, ctx) => ({
type: "link",
url: args.text(0, "#"),
value: args.materializedTailTokens(1),
}),
},
}),
},
blockTags: declareMultilineTags(["info", "warning", "code"]),
});测试一下:
const tokens = parser.parse("Hello $$bold($$italic(world)$$)$$!");
console.log(tokens);
// [
// { type: "text", value: "Hello ", id: "rt-0" },
// { type: "bold", value: [{ type: "italic", value: [...], ... }], ... },
// { type: "text", value: "!", id: "rt-3" },
// ]把每种 token 类型映射到 HTML 输出:
// handlers.ts
import type { TextToken } from "yume-dsl-rich-text";
import type { InterpretHelpers, ResolvedResult } from "yume-dsl-token-walker";
interface Env {
theme: "light" | "dark";
}
type H = InterpretHelpers<string, Env>;
// 工具函数:用标签包裹子节点
const tag = (name: string, token: TextToken, h: H, attrs = ""): ResolvedResult<string> => ({
type: "nodes",
nodes: [`<${name}${attrs}>`, ...h.interpretChildren(token.value), `</${name}>`],
});
export const handlers: Record<string, (token: TextToken, h: H) => ResolvedResult<string>> = {
bold: (token, h) => tag("strong", token, h),
italic: (token, h) => tag("em", token, h),
underline: (token, h) => tag("u", token, h),
strike: (token, h) => tag("del", token, h),
link: (token, h) => {
const url = typeof token.url === "string" ? token.url : "#";
return tag("a", token, h, ` href="${url}"`);
},
info: (token, h) => {
const title = typeof token.arg === "string" ? token.arg : "Info";
const bg = h.env.theme === "dark" ? "#1a3a4a" : "#e8f4fd";
return {
type: "nodes",
nodes: [
`<div class="callout info" style="background:${bg}">`,
`<div class="callout-title">${title}</div>`,
...h.interpretChildren(token.value),
"</div>",
],
};
},
warning: (token, h) => {
const title = typeof token.arg === "string" ? token.arg : "Warning";
return {
type: "nodes",
nodes: [
`<div class="callout warning">`,
`<div class="callout-title">${title}</div>`,
...h.interpretChildren(token.value),
"</div>",
],
};
},
code: (token, h) => {
const lang = typeof token.arg === "string" ? token.arg : "text";
return {
type: "text",
text: `<pre><code class="language-${lang}">${
typeof token.value === "string" ? token.value : h.flattenText(token.value)
}</code></pre>`,
};
},
};// ruleset.ts
import { createRuleset, fromHandlerMap, debugUnhandled } from "yume-dsl-token-walker";
import { handlers } from "./handlers";
export const ruleset = createRuleset<string, { theme: "light" | "dark" }>({
createText: (text) => text,
interpret: fromHandlerMap(handlers),
onUnhandled: process.env.NODE_ENV === "production"
? "flatten"
: debugUnhandled(), // 开发环境显示 "[unhandled:someTag]"
});为什么用 createRuleset? 它只是恒等函数——但它让 TypeScript 自动推断 TNode 和 TEnv,不用手动标泛型。
为什么用 fromHandlerMap? 它把你的 Record<type, handler> 变成 InterpretRuleset 需要的 interpret 函数。未匹配的 token 类型自动返回 { type: "unhandled" }。
// render.ts
import { interpretText, collectNodes } from "yume-dsl-token-walker";
import { parser } from "./parser";
import { ruleset } from "./ruleset";
export function renderBlogPost(dslSource: string, theme: "light" | "dark"): string {
return collectNodes(
interpretText(dslSource, parser, ruleset, { theme }),
).join("");
}
// 使用
const html = renderBlogPost(
`$$bold(Welcome)$$ to my blog!
$$link(https://example.com | Click here)$$ for more.
$$info(Note)*
This uses $$italic(custom DSL)$$ for rich text.
*end$$
$$code(ts)%
const x: number = 42;
%end$$`,
"dark",
);纯文本不需要 ruleset——flattenText 是独立工具函数:
import { flattenText } from "yume-dsl-token-walker";
import { parser } from "./parser";
export function extractSearchText(dslSource: string): string {
return flattenText(parser.parse(dslSource));
}
const plain = extractSearchText("Hello $$bold($$italic(world)$$)$$ - $$link(https://x.com | click)$$");
// → "Hello world - click"用于搜索索引、RSS feed、Open Graph 描述、通知预览。
假设所有 block 标签(info、warning)都需要相同的 <section> 外层容器。与其在每个 handler 里重复写,不如用 wrapHandlers:
import { fromHandlerMap, wrapHandlers } from "yume-dsl-token-walker";
const inlineHandlers = { bold: ..., italic: ..., link: ... };
const blockHandlers = wrapHandlers(
{ info: ..., warning: ... },
(result, token) => {
if (result.type !== "nodes") return result;
return {
type: "nodes",
nodes: [`<section class="block-${token.type}">`, ...result.nodes, "</section>"],
};
},
);
const interpret = fromHandlerMap({ ...inlineHandlers, ...blockHandlers });wrapHandlers 预处理 handler map——每个 handler 的结果在返回前都会经过 wrap。然后 fromHandlerMap 把整个 map 变成最终的 interpret 函数。
src/
dsl/
parser.ts ← createParser + handlers
handlers.ts ← handler map (Record<type, handler>)
ruleset.ts ← createRuleset + fromHandlerMap
render.ts ← interpretText 包装
search.ts ← flattenText 纯文本提取
学到的原则:
-
env只放运行时上下文(主题、语言)——不放业务状态 - handler 是纯映射:token 进,result 出——副作用放 render 层
- 一种输出格式一个 ruleset——需要 HTML + 纯文本?两个 ruleset,或者用独立的
flattenText -
fromHandlerMap+wrapHandlers让 handler 代码干净可组合
-
异步解释 API — 如果 code handler 需要调 Shiki
codeToHtml异步渲染 - 教程:游戏对话引擎 — yield 打字指令而非字符串
- 教程:编辑器 Lint + 自动修复 — 用自定义规则校验 DSL 源码