zh CN 处理器辅助函数 - chiba233/yumeDSL GitHub Wiki
处理器辅助函数
你定义了一堆标签(bold、link、code……),每个都要写 handler 对象?太累了。 辅助函数帮你批量注册,一行搞定一个标签。
本页讲的是"怎么快速注册" 。如果你需要更底层的控制——手动拆管道、反转义、逐字符扫描——请看 处理器工具函数。
签名注意: 本页的
createPipeHandlers回调签名和底层TagHandler不同。createPipeHandlers帮你预解析了管道参数,所以第一个参数是PipeArgs,不是原始tokens或arg。底层TagHandler签名见 编写标签处理器。
什么时候用哪个
"我要注册标签 handler"
│
┌─────────────┼──────────────┐
▼ ▼ ▼
需要管道参数? 纯包装器? 只声明换行规范化?
需要多种形式? (无管道) (不创建 handler)
│ │ │
▼ ▼ ▼
createPipeHandlers createSimple* declareMultilineTags
(推荐,万能) Handlers (配合 blockTags 用)
│
┌─────────┼─────────┐
▼ ▼ ▼
Inline Block Raw
Handlers Handlers Handlers
一句话版本:
| 辅助函数 | 干嘛的 | 什么时候用 |
|---|---|---|
createPipeHandlers |
一个定义覆盖 inline/raw/block 任意组合,自动帮你解析管道参数 | 大多数标签用这个就对了 |
createSimpleInlineHandlers |
传个名字数组,批量生成 inline 包装器 | ["bold", "italic", "underline"] 这种不需要参数的简单标签 |
createSimpleBlockHandlers |
同上,但是 block 形式 | 简单 block 包装器 |
createSimpleRawHandlers |
同上,但是 raw 形式 | ["code", "math"] 这种内容不需要递归解析的 |
| 空对象 handlers | 直接声明标签名,交给默认 materialization / fallback | 只想零成本声明标签存在;这是推荐的标准隐式写法 |
declareMultilineTags |
不创建 handler,只告诉解析器哪些标签要做换行规范化 | 只针对需要特殊处理的标签(和自动推导合并) |
createPipeHandlers(definitions)
function createPipeHandlers<const T extends Record<string, PipeHandlerDefinition>>(
definitions: T
): { [K in keyof T]: TagHandler }
推荐的 handler 辅助函数。它做的事很简单:帮你在回调执行前自动调一次 parsePipeArgs,这样你拿到的第一个参数就已经是拆好管道的 PipeArgs,而不是原始的 tokens / arg 字符串。
它到底帮你省了什么
下面两段代码完全等价。左边是手写 TagHandler,右边是用 createPipeHandlers:
// ── 手写 TagHandler(你自己调 parsePipeArgs)──
const handlers = {
link: {
inline: (tokens, ctx) => {
const args = parsePipeArgs(tokens, ctx);
return { type: "link", url: args.text(0), value: args.materializedTailTokens(1) };
},
},
};
// ── createPipeHandlers(它帮你调 parsePipeArgs)──
const handlers = createPipeHandlers({
link: {
inline: (args, ctx) => ({
type: "link", url: args.text(0), value: args.materializedTailTokens(1),
}),
},
});
区别只有一个:回调的第一个参数从 tokens: TextToken[] 变成了 args: PipeArgs。其余参数(content、ctx)位置和类型都一样。
PipeHandlerDefinition — 你写的回调签名
interface PipeHandlerDefinition {
inline?: (args: PipeArgs, ctx?: DslContext) => TokenDraft;
raw?: (args: PipeArgs, content: string, ctx?: DslContext, rawArg?: string) => TokenDraft;
block?: (args: PipeArgs, content: TextToken[], ctx?: DslContext, rawArg?: string) => TokenDraft;
}
和底层 TagHandler 的签名对照:
| 形式 | TagHandler(底层) |
PipeHandlerDefinition(你写的) |
差异 |
|---|---|---|---|
| inline | (tokens, ctx) => TokenDraft |
(args, ctx) => TokenDraft |
tokens → args(自动 parsePipeArgs) |
| raw | (arg, content, ctx) => … |
(args, content, ctx, rawArg) => … |
arg → args(自动 parsePipeTextArgs);原始 arg 移到 rawArg |
| block | (arg, content, ctx) => … |
(args, content, ctx, rawArg) => … |
同 raw |
rawArg 就是底层 TagHandler 收到的那个原始 arg 字符串(管道分割前的),需要时才用。
PipeArgs — 你拿到的参数对象
interface PipeArgs {
parts: TextToken[][];
has: (index: number) => boolean;
text: (index: number, fallback?: string) => string;
materializedTokens: (index: number, fallback?: TextToken[]) => TextToken[];
materializedTailTokens: (startIndex: number, fallback?: TextToken[]) => TextToken[];
}
| 方法 | 一句话 |
|---|---|
parts |
按管道拆分的原始 token 段 |
has(i) |
第 i 段存在吗? |
text(i) |
第 i 段的纯文本(已反转义、已修剪) |
materializedTokens(i) |
第 i 段的 token(文本已反转义,但保留结构) |
materializedTailTokens(start) |
从 start 开始的所有段合并成一个数组——适合"标签内容本身含管道"的场景 |
完整示例
import {createPipeHandlers} from "yume-dsl-rich-text";
const handlers = createPipeHandlers({
// 仅 inline: $$link(https://example.com | click here)$$
link: {
inline: (args, ctx) => ({
type: "link",
url: args.text(0),
value: args.materializedTailTokens(1),
}),
},
// Inline + block: $$info(tip)$$ 或 $$info(tip)*\ncontent\n*end$$
info: {
inline: (args, ctx) => ({
type: "info",
title: args.text(0),
value: args.materializedTailTokens(1),
}),
block: (args, content, ctx) => ({
type: "info",
title: args.text(0),
value: content,
}),
},
// 仅 raw: $$code(ts)%\nconst x = 1;\n%end$$
code: {
raw: (args, content, ctx) => ({
type: "code",
lang: args.text(0, "text"),
value: content,
}),
},
});
createSimpleInlineHandlers(names)
function createSimpleInlineHandlers<const T extends readonly string[]>(
names: T
): Record<T[number], TagHandler>
传个标签名数组进去,批量生成 inline handler。每个 handler 自动反转义子 token、包成
{ type: tagName, value: materializedTokens }。
const handlers = createSimpleInlineHandlers(["bold", "italic", "underline"]);
// 等价于手写三个 { inline: (tokens, ctx) => ({ type: "bold", value: materializeTextTokens(tokens, ctx) }) }
标准隐式写法:空对象 handlers
如果你并不需要显式写 inline / block / raw 回调,只是想声明“这些标签名存在”,最短写法就是直接写空对象:
const handlers = {
bold: {},
italic: {},
};
这表示:
- 标签名已注册
- 最终产物交给解析器默认的 materialization / fallback 逻辑
- 你没有显式指定固定输出形状
- 它实际只给你一条inline 默认输出路径
它和 createSimpleInlineHandlers(...) 的区别是:
| 写法 | 语义 |
|---|---|
createSimpleInlineHandlers(["bold"]) |
显式安装 inline handler,固定产出 { type: "bold", value: materializedTokens } |
bold: {} |
只声明标签名存在,依赖默认 materialization / fallback |
这套空对象写法是文档正式推荐的标准隐式写法。createPassthroughTags 之所以被放进待弃用,是因为库不再想维护一个专门包装这层语义的 helper;
但空对象 handler 这种“零成本声明语法”本身仍然是推荐用法。
实际效果(基于代码和运行结果):
parseRichText("$$bold(world)$$", {
handlers: {bold: {}},
});
// => [{ type: "bold", value: [{ type: "text", value: "world" }] }]
parseRichText("$$code(js)%\nconst x = 1;\n%end$$", {
handlers: {code: {}},
});
// => [{ type: "text", value: "$$code(js)%\nconst x = 1;\n%end$$" }]
parseRichText("$$info(note)*\nhello\n*end$$", {
handlers: {info: {}},
});
// => [{ type: "text", value: "$$info(note)*\nhello\n*end$$" }]
结论:
- 空对象写法的默认输出只覆盖 inline
- raw / block 不会自动启用,没有对应 handler 时仍然降级回源码文本
- 如果同名标签既要支持 inline,又要支持 raw 或 block,请显式写
inline+raw/block
createSimpleBlockHandlers(names)
function createSimpleBlockHandlers<const T extends readonly string[]>(
names: T
): Record<T[number], TagHandler>
批量生成 block handler。每个 handler 直接透传 arg 和已递归解析的 content,不做反转义。输出
{ type: tagName, arg, value: content }。
const handlers = createSimpleBlockHandlers(["info", "warning", "collapse"]);
createSimpleRawHandlers(names)
function createSimpleRawHandlers<const T extends readonly string[]>(
names: T
): Record<T[number], TagHandler>
批量生成 raw handler。和 block 类似,但 content 是 string(不递归解析)。
const handlers = createSimpleRawHandlers(["code", "math", "latex"]);
declareMultilineTags(names)
function declareMultilineTags<const T extends readonly BlockTagInput[]>(
names: T
): BlockTagInput[]
它解决什么问题
对于具有块级/容器渲染语义的标签——对话框、代码块、折叠面板、信息卡片——不管它们在 DSL 里用的是 block()*...*end$$)、raw(
)%...%end$$)还是 inline($$tag(...)$$)形式,只要最终渲染成块级元素,首尾换行都会产生同一个问题:凭空多出空行。
以 block 形式为例:
$$speaker(Alice)*
Hello!
*end$$
人类会很自然地把 )* 和 *end$$ 各占一行。但从解析器的视角看,)* 之后紧接着的是一个 \n,*end$$ 之前紧接着的也是一个
\n——于是原始内容变成了 "\nHello!\n" 而不是 "Hello!"。这两个边界换行不是作者想要的内容,而是多行语法的书写副产物。
即使是 inline 形式,只要标签渲染为块级元素,内容中的首尾换行同样会导致渲染出多余空行。关键不在于 DSL 用了哪种语法形式,而在于 标签的渲染语义是不是块级的。
如果不做规范化,这些多余换行会在每个块级标签的渲染输出中反复出现,是一类极其隐蔽且难以排查的视觉 bug。
换行规范化做了什么
解析器对声明过的标签执行首尾各剥一个换行的规范化:
| 位置 | 原始内容 | 规范化后 |
|---|---|---|
)* / )% 之后 |
\n 或 \r\n → 剥掉 |
内容从第一行实际文字开始 |
*end$$ / %end$$ 之前 |
\n 或 \r\n → 剥掉 |
内容到最后一行实际文字结束 |
只剥恰好一个换行,不会多吃。如果作者有意写了多个空行,只有紧贴边界的那一个被剥掉,其余保留。
同时,剥离产生的偏移量会精确回传给 position tracker,所以即使在开启 trackPositions 的场景下,源码定位依然准确。
默认行为:自动推导
大多数情况下你不需要手动调用。 解析器在创建时会自动扫描 handlers:
- handler 有
raw方法 → 该标签在 raw 形式下做规范化 - handler 有
block方法 → 该标签在 block 形式下做规范化
也就是说,只要你用 createSimpleBlockHandlers、createSimpleRawHandlers、createPipeHandlers 等注册了多行
handler,规范化就已经自动生效了。
blockTags 和自动推导的合并
自动推导始终作为基底运行。传 blockTags 时,覆盖是按标签的,不是全局的:
- 你显式列出的标签 → 你的声明完全替换该标签的自动推导(替换的是所有形式,不是只替换你列出的——没列出的形式对该标签变为禁用)
- 你没提到的标签 → 自动推导继续生效,不受影响
这意味着你只需要声明需要特殊处理的标签,不用为了给一个标签加 inline 规范化就把所有 raw/block 标签重新列一遍。但如果你列了某个标签,确保把你想要的所有形式都写上——自动推导不会帮你补齐剩下的。
// 只声明 center — 其他所有 raw/block 标签保留自动推导
blockTags: declareMultilineTags([{tag: "center", forms: ["inline"]}])
经验法则
对具有块级/容器渲染语义的标签,通常都应该确保它出现在 blockTags 中(不管是自动推导还是手动声明)。否则首尾换行会被算进内容,导致渲染时出现多余空白行。
什么时候需要手动声明
当自动推导的结果不是你想要的时候:
| 场景 | 做法 |
|---|---|
| 标签只用空对象 handlers 注册,但你知道它会以 block 形式使用 | 手动声明 |
| 标签渲染为块级元素,但只注册了 inline handler——自动推导不会覆盖到它 | 用 { tag, forms: ["inline"] } 声明 |
| 标签同时有 raw 和 block handler,但你只想在 raw 形式下规范化 | 用 { tag, forms: ["raw"] } — 只覆盖该标签的自动推导 |
用法
传字符串: 一次声明三种形式全部规范化(raw + block + inline)——最常用的方式。
blockTags: declareMultilineTags(["info", "warning", "center"])
传对象: 用 { tag, forms } 精细控制只在哪种形式下规范化。
blockTags: declareMultilineTags([
"info", // 字符串:三种形式全部规范化
{tag: "code", forms: ["raw"]}, // 仅 raw 形式
{tag: "center", forms: ["inline"]}, // 仅 inline 形式
])
forms 接受的值:
| 值 | 规范化行为 | 适用场景 |
|---|---|---|
"raw" |
剥掉 )% 后的前导 \n 和 %end$$ 前的尾随 \n |
多行 raw 标签($$code(ts)%\n...\n%end$$) |
"block" |
剥掉 )* 后的前导 \n 和 *end$$ 前的尾随 \n |
多行 block 标签($$info()*\n...\n*end$$) |
"inline" |
剥掉 inline close $$ 后紧跟的 \n |
用 inline 语法但渲染为块级元素的标签($$center(...)$$) |
省略 forms 的对象形式默认为 ["raw", "block"](向后兼容)。
注意:
declareMultilineTags不创建 handler,也不注册标签——它只控制换行规范化策略。标签注册请用本页其他辅助函数或手写 handler。
已弃用
| 已弃用 | 用这个替代 |
|---|---|
createPipeBlockHandlers |
createPipeHandlers + block 方法 |
createPipeRawHandlers |
createPipeHandlers + raw 方法 |
createPassthroughTags |
空对象 handlers / 本地 helper(如果你就是要保留隐式 fallback) |