zh CN 编写标签处理器 - chiba233/yumeDSL GitHub Wiki

编写标签处理器

稳定 Token ID | 处理器工具函数

大部分标签用 处理器辅助函数 批量注册就行了。 只有辅助函数搞不定的场景——条件输出、验证、副作用——才需要手写 TagHandler

关于 ctx 回调里的 ctx 是解析器传给你的上下文对象,你不需要知道它是什么,写上就行。想深入了解的看 DslContext

签名注意: 本页讨论的是底层 TagHandler 接口,回调的第一个参数是原始的 tokens(inline)或 arg(raw/block)。如果你用 createPipeHandlers,它帮你做了管道预解析,回调签名不同——第一个参数变成了 PipeArgs。两套签名见下方对比。


TagHandler 长什么样

TagHandler = {
    inline?  → 用户写 $$tag(content)$$     时调用
    raw?     → 用户写 $$tag(arg)%...%end$$  时调用
    block?   → 用户写 $$tag(arg)*...*end$$  时调用
}
interface TagHandler {
    inline?: (tokens: TextToken[], ctx?: DslContext) => TokenDraft;
    raw?: (arg: string | undefined, content: string, ctx?: DslContext) => TokenDraft;
    block?: (arg: string | undefined, content: TextToken[], ctx?: DslContext) => TokenDraft;
}

只实现你的标签支持的形式。用户写了你没实现的形式 → 整段标记降级为字面文本,不报错。

两套签名对比

形式 底层 TagHandler(本页) createPipeHandlers 包装后
inline (tokens: TextToken[], ctx?) => TokenDraft (args: PipeArgs, ctx?) => TokenDraft
raw (arg: string|undefined, content: string, ctx?) => TokenDraft (args: PipeArgs, content: string, ctx?, rawArg?) => TokenDraft
block (arg: string|undefined, content: TextToken[], ctx?) => TokenDraft (args: PipeArgs, content: TextToken[], ctx?, rawArg?) => TokenDraft

底层版本的第一个参数是原始数据(tokens 或 arg 字符串)。包装版本帮你做了管道预解析,第一个参数变成了 PipeArgs


三种 handler 的参数一览

inline

inline: (tokens, ctx) => TokenDraft
参数 是什么
tokens 括号内递归解析的子 token。注意:文本叶子里的转义还没被解掉。
ctx 解析上下文,转发给工具函数用

关于转义: 用户写了 $$bold(hello \| world)$$,你拿到的文本 token 值是 "hello \\| world"(还有反斜杠)。想要干净的文本?用 materializeTextTokens(tokens, ctx)parsePipeArgs

raw

raw: (arg, content, ctx) => TokenDraft
参数 是什么
arg ()% 之间的文本。空参数时为 undefined。是原始字符串,不是管道解析后的
content raw 正文——原封不动的字符串,不解析嵌套标签
ctx 解析上下文

适合:代码块、数学公式、嵌入 JSON——任何不该被递归解析的内容。

block

block: (arg, content, ctx) => TokenDraft
参数 是什么
arg 和 raw 一样的原始参数字符串
content TextToken[]——已经递归解析好的 block 正文
ctx 解析上下文

完整示例:辅助函数 + 手写混搭

import {
    createSimpleInlineHandlers,
    createPipeHandlers,
    parseRichText,
    parsePipeTextArgs,
    type TagHandler,
    type TokenDraft,
    type DslContext,
} from "yume-dsl-rich-text";

// 简单标签用辅助函数
const simple = createSimpleInlineHandlers(["bold", "italic", "underline"]);

// 需要管道参数的用 createPipeHandlers
const piped = createPipeHandlers({
    link: {
        inline: (args, ctx) => ({
            type: "link",
            url: args.text(0),
            value: args.materializedTailTokens(1),
        }),
    },
});

// 需要自定义逻辑的手写
const manual: Record<string, TagHandler> = {
    code: {
        raw: (arg, content, ctx): TokenDraft => {
            const pipeArgs = parsePipeTextArgs(arg ?? "", ctx);
            return {
                type: "code",
                lang: pipeArgs.text(0, "text"),
                label: pipeArgs.text(1, ""),
                value: content.trim(),
            };
        },
    },
};

// 合并
const handlers = {...simple, ...piped, ...manual};
const tokens = parseRichText("$$bold(Hello)$$ $$link(https://example.com | click)$$", {handlers});

PipeArgs

parsePipeArgs / parsePipeTextArgs 返回的结构化视图。详见 处理器工具函数

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 开始所有段合并——适合"后面全是自由文本可能含管道"的场景

典型用法

// URL 是纯文本,显示内容可能含管道和富文本
const args = parsePipeArgs(tokens, ctx);
return {
    type: "link",
    url: args.text(0),                         // "https://example.com"
    value: args.materializedTailTokens(1, []),  // "Click | here | for details" 全部合并
};

parsePipeTextList

最简单的管道分割——字符串进字符串数组出:

parsePipeTextList("ts | Demo | Label");  // → ["ts", "Demo", "Label"]
parsePipeTextList("a \\| b | c");        // → ["a | b", "c"]

raw/block handler 里拆 arg 参数的首选:

code: {
    raw: (arg, content, ctx) => {
        const parts = parsePipeTextList(arg ?? "");
        return {type: "code", lang: parts[0] || "text", value: content};
    },
}