zh CN 处理器工具函数 - chiba233/yumeDSL GitHub Wiki
处理器工具函数
写 handler 时用到的底层工具函数。
和
createPipeHandlers的关系:createPipeHandlers(见 处理器辅助函数)内部就是帮你调这里的parsePipeArgs/parsePipeTextArgs。如果你用了createPipeHandlers,通常不需要自己调这些函数。 当createPipeHandlers的包装不够用(比如你需要在调parsePipeArgs之前做预处理,或者你根本不走管道分割),才需要直接用本页的工具。
分两层:日常的够用了,高级的是日常函数的底层积木。
全景图:谁调谁
createPipeHandlers (辅助函数页)
│ 内部自动调用 ↓
│
你的 handler 代码(或 createPipeHandlers 内部)
│
├─ parsePipeArgs(tokens, ctx) ← 把 inline token 按管道拆分
│ └─ splitTokensByPipe ← 底层:逐字符扫描、转义处理
│
├─ parsePipeTextArgs(text, ctx) ← 同上,但输入是字符串(raw/block 的 arg)
│ └─ createTextToken → parsePipeArgs
│
├─ parsePipeTextList(text, ctx) ← 管道分割 → 纯字符串数组
│ └─ parsePipeTextArgs
│
├─ extractText(tokens) ← 从 token 树提取纯文本
│
├─ createTextToken(value, ctx) ← 手动造一个文本 token
│ └─ createToken
│
└─ materializeTextTokens(tokens) ← 递归反转义文本叶子
└─ unescapeInline
└─ readEscaped
└─ readEscapedSequence ← 最底层:字符级转义扫描
快速选择
| 你想干嘛 | 用什么 | 备注 |
|---|---|---|
| inline handler 里拆管道参数 | parsePipeArgs(tokens, ctx) |
createPipeHandlers 自动帮你调了这个 |
| raw/block handler 里拆 arg 字符串的管道参数 | parsePipeTextArgs(arg ?? "", ctx) |
createPipeHandlers 自动帮你调了这个 |
| 只要纯字符串数组,不要 token | parsePipeTextList(text, ctx) |
|
| 从 token 树提纯文本(搜索/展示用) | extractText(tokens) |
|
| 手动造一个文本 token | createTextToken(value, ctx) |
日常工具函数
parsePipeArgs(tokens, ctx?)
function parsePipeArgs(tokens: TextToken[], ctx?: DslContext): PipeArgs
按管道分隔符拆分 inline token,返回 PipeArgs 视图。你可以用 .text(0) 拿纯文本,用 .materializedTokens(0) 拿反转义后的
token。
如果你用了
createPipeHandlers,这一步已经自动完成——你的回调直接收到PipeArgs。 手动调parsePipeArgs的场景是:你没用createPipeHandlers,或者你在自定义 handler 里需要更精细的控制。
const linkHandler: TagHandler = {
inline: (tokens, ctx) => {
const args = parsePipeArgs(tokens, ctx);
return {
type: "link",
url: args.text(0),
value: args.materializedTailTokens(1),
};
},
};
边界情况: 空 tokens → 一个空段;没有管道 → 整体是一段;转义的 \| 不拆。
parsePipeTextArgs(text, ctx?)
function parsePipeTextArgs(text: string, ctx?: DslContext): PipeArgs
和 parsePipeArgs 一样,但接受字符串而不是 token 数组。在 raw/block handler 里用——因为 raw/block 的参数是字符串,不是 token。
createPipeHandlers的raw/block回调已经自动调了parsePipeTextArgs。 原始字符串参数通过rawArg第四参数传给你,需要时才用。
const codeHandler: TagHandler = {
raw: (arg, content, ctx) => {
const args = parsePipeTextArgs(arg ?? "", ctx);
return {type: "code", lang: args.text(0, "text"), value: content};
},
};
parsePipeTextList(text, ctx?)
function parsePipeTextList(text: string, ctx?: DslContext): string[]
管道分割 → 纯字符串数组。不涉及 token,最简单的一种。
parsePipeTextList("ts | Demo | Label");
// → ["ts", "Demo", "Label"]
parsePipeTextList("a \\| b | c");
// → ["a | b", "c"] — 转义管道不拆
extractText(tokens?)
function extractText(tokens?: TextToken[]): string
递归拼出 token 树里的所有纯文本。不反转义(\| 原样输出)、不保留结构(bold 和纯文本贡献同样的字符串)。
const tokens = parseRichText("Hello $$bold(world)$$", {handlers});
extractText(tokens);
// → "Hello world"
要反转义的纯文本?用 unescapeInline(extractText(tokens), ctx) 或 PipeArgs.text()。
createTextToken(value, ctx?)
function createTextToken(value: string, ctx?: DslContext): TextToken
手动造一个 { type: "text", value, id } 的文本 token。在 handler 里需要插入分隔符 token 时用。
const separator = createTextToken(" | ", ctx);
高级工具函数
日常函数的底层积木。需要更低级别控制时才用。
splitTokensByPipe(tokens, ctx?)
function splitTokensByPipe(tokens: TextToken[], ctx?: DslContext): TextToken[][]
不带 PipeArgs 包装的原始管道分割。逐字符扫描文本 token,遇到管道就拆。
行为细节:
- 转义管道
\|不拆,保留转义形式 - 管道后面的空白被吃掉(
"a | b"→["a "]+["b"]) - 能产生空段(
"a || b"→ 三段) - 非文本 token 不扫描,原样归入当前段
大多数情况用 parsePipeArgs 就够了。
materializeTextTokens(tokens, ctx?)
function materializeTextTokens(tokens: TextToken[], ctx?: DslContext): TextToken[]
递归反转义文本叶子节点:\| → |,\)$$ → )$$,等等。
为什么叫"物化"? 解析过程中,文本 token 故意保留转义序列(这样管道分割才能区分真管道和转义管道)。物化是最终把转义解析成字面值的步骤。
为什么跳过非文本 token? raw-code 里的 \n 是真 JavaScript,不是 DSL 转义。只处理 type === "text" 的 token 才安全。
unescapeInline(str, ctx?)
function unescapeInline(str: string, ctx?: DslContext | SyntaxConfig): string
反转义字符串中所有 DSL 转义序列。从左到右扫描,每次消耗一个"输出单元"。
unescapeInline("hello \\) world"); // → "hello ) world"
unescapeInline("a \\| b \\| c"); // → "a | b | c"
unescapeInline("path\\to\\file"); // → "path\\to\\file" (t 不是可转义 token,原样保留)
readEscapedSequence(text, i, ctx?)
function readEscapedSequence(text: string, i: number, ctx?): [string | null, number]
字符级转义扫描器。在位置 i 检查是不是转义序列的开头。
- 找到转义 →
[转义的字面值, 序列结束位置] - 不是转义 →
[null, i](位置不变,调用者自己推进)
readEscapedSequence("hello \\| world", 6); // → ["|", 8]
readEscapedSequence("hello \\| world", 0); // → [null, 0]
readEscaped(text, i, ctx?)
function readEscaped(text: string, i: number, ctx?): [string, number]
readEscapedSequence 的带兜底版本:找到转义就返回字面值,没找到就返回当前字符。永远有返回值,适合写扫描循环。
readEscaped("a\\|b", 0); // → ["a", 1]
readEscaped("a\\|b", 1); // → ["|", 3] — 吃掉 \|,输出 |
readEscaped("a\\|b", 3); // → ["b", 4]
unescapeInline 本质上就是循环调 readEscaped 拼结果。
createToken(draft, position?, ctx?)
function createToken(draft: TokenDraft, position?: SourceSpan, ctx?: DslContext | CreateId): TextToken
从 TokenDraft 构建最终的 TextToken——分配 ID、附加位置。所有 token 创建最终都经过这个函数。
ID 解析顺序:
ctx是函数 → 直接当 CreateId 用ctx是 DslContext 且有createId→ 用它- 模块级
activeCreateId(已弃用路径) - 兜底:顺序计数器
rt-0, rt-1, ...
新代码请传 DslContext,别传裸函数。
性能
测量环境:鲲鹏 920 aarch64 / Node v24.14.0——5000 次迭代取平均。
unescapeInline(自 1.1.0)
重写为批量 slice() 非转义区间,代替逐字符 readEscaped() + +=。没有转义序列时直接返回原字符串(零分配快速路径)。
| 场景 | 输入大小 | 1.0.x | 1.1.0 | 提升 |
|---|---|---|---|---|
| 无转义(最常见) | 4950 字符 | 0.164 ms | 0.054 ms | 3.0x |
大量转义(\(、\)、|、\\) |
2500 字符 | 0.151 ms | 0.127 ms | 1.2x |
"无转义"是实际使用中的主要路径——大部分文本内容不包含 DSL 转义序列。3x 加速来自消除了 ~5000 次单独的 readEscaped
调用(每次都做一个 text.slice(i, i+1) 单字符分配),替换为检测到无 escapeChar 后直接 return str。
splitTokensByPipe(自 1.1.0)
重写为追踪 run 起始位置,代替逐字符 buffer += val[i]。一次性 slice 整段非 divider/非转义区间。
| 场景 | 输入大小 | 1.0.x | 1.1.0 | 提升 |
|---|---|---|---|---|
| 50 个管道分段 | 587 字符 | 0.068 ms | 0.051 ms | 1.3x |
extractText
测试了 string[] + join("") 作为递归 += 的替代方案。基准测试显示 V8 的 ConsString 优化在典型 token 树规模(300
token)下更快。与 1.0.x 行为一致——保留了原始实现。
这对你的应用意味着什么
这些工具函数在 handler 代码内部调用——unescapeInline 在 parsePipeArgs、materializeTextTokens、parsePipeTextList
中运行。如果你的 DSL 文档有 1000 个标签且每个都有管道参数,3x 的 unescapeInline 提升在 ~50 KB 文档上节省约 100 ms 的
handler 处理时间。
对大多数应用来说差异不可感知。如果你在构建逐键重解析的实时编辑器,收益会累积。