zh CN Token 结构 - chiba233/yumeDSL GitHub Wiki
解析器把 DSL 文本变成一棵 token 树。树上每个节点就是一个 TextToken。
你的 handler 返回 TokenDraft(半成品),解析器自动补上 id 和 position 变成最终的 TextToken。
你的 DSL 文本
│
▼
解析器扫描
│
├─ 纯文本 → TextToken { type: "text", value: "Hello ", id: "rt-0" }
│
└─ $$bold(world)$$ → 调用你的 handler
│
▼
handler 返回 TokenDraft
{ type: "bold", value: [...子token] }
│
▼
解析器补上 id + position
│
▼
TextToken { type: "bold", value: [...], id: "rt-1" }
interface TextToken {
type: string; // "text" 或 handler 返回的类型
value: string | TextToken[]; // 文本内容 或 子 token 数组
id: string; // 解析内唯一标识
position?: SourceSpan; // 源码坐标(开了 trackPositions 才有)
[key: string]: unknown; // handler 附加的额外字段
}| 字段 | 什么意思 |
|---|---|
type |
纯文本是 "text",标签节点是 handler 返回的 type(通常是标签名如 "bold",但也可以是自定义类型如 "version-note") |
value |
文本节点 → string;inline/block 标签 → TextToken[](子节点);raw 标签 → string(原始内容) |
id |
默认顺序编号 rt-0, rt-1, ...。想要稳定 ID → 稳定 Token ID
|
position |
trackPositions: true 时才有。详见 源码位置追踪
|
[key] |
handler 返回什么额外字段就保留什么。比如 link 的 url、code 的 lang
|
判断 value 类型: typeof token.value === "string" → 文本/raw;否则 → 子 token 数组。
interface TokenDraft {
type: string;
value: string | TextToken[];
[key: string]: unknown;
}handler 返回的东西。和 TextToken 一样,但没有 id 和 position——解析器会自动补。
handler 必须设 type 和 value,想加什么额外字段随便加:
return {
type: "link",
value: childTokens, // 必须
url: "https://example.com", // 额外字段,会原样保留在 TextToken 上
};基础 TextToken 用索引签名保持灵活,但你可以——也应该——给自己的标签定义精确类型。
-
库公共边界:
parseRichText()返回的是通用TextToken[](允许未知标签扩展字段) -
业务内部边界:你自己的渲染器 / handler 用
TokenMap收窄成严格类型
这两层分开后,既不会牺牲扩展性,也能拿到完整的 TypeScript 校验。
定义一份 token map,然后用 createTokenGuard 零样板收窄:
import {
type NarrowToken,
type NarrowDraft,
type NarrowTokenUnion,
createTokenGuard,
type TextToken,
} from "yume-dsl-rich-text";
// 1. 定义 token map —— key 是 type,value 是该类型的额外字段
interface MyTokenMap {
text: Record<string, never>;
bold: Record<string, never>;
link: { url: string };
code: { lang: string };
}
type MyToken = NarrowTokenUnion<MyTokenMap>;
// 2. 创建类型守卫
const is = createTokenGuard<MyTokenMap>();
const renderChildren = (value: TextToken["value"]) =>
Array.isArray(value) ? value.map(render).join("") : value;
// 3. 在 if 分支里自动收窄 —— TypeScript 自动推导额外字段
function render(token: TextToken): string {
if (is(token, "text")) return typeof token.value === "string" ? token.value : renderChildren(token.value);
if (is(token, "bold")) return `<b>${renderChildren(token.value)}</b>`;
if (is(token, "link")) return `<a href="${token.url}">${renderChildren(token.value)}</a>`;
if (is(token, "code")) return `<pre data-lang="${token.lang}">${renderChildren(token.value)}</pre>`;
return "";
}
// 4. 如果你希望在某些模块里使用完整判别联合:
const tokens = parseRichText(input, { handlers }) as MyToken[];工具类型一览:
| 类型 | 作用 |
|---|---|
NarrowToken<TType, TExtra?> |
把 TextToken 收窄为特定 type 字面量 + 已知额外字段 |
NarrowDraft<TType, TExtra?> |
把 TokenDraft 收窄,用于 handler 返回类型标注 |
NarrowTokenUnion<TMap> |
从 token map 批量生成 NarrowToken 联合类型,适合 switch 穷举 |
createTokenGuard<TMap>() |
创建运行时类型守卫,按 type 键收窄 TextToken
|
小提示:
TokenMap里“无额外字段”的类型建议用Record<string, never>,避免{}在严格 ESLint 配置下触发no-empty-object-type。
handler 侧类型安全 —— NarrowDraft:
import {type NarrowDraft, type TagHandler, parsePipeArgs} from "yume-dsl-rich-text";
type LinkDraft = NarrowDraft<"link", { url: string }>;
const linkHandler: TagHandler = {
inline: (tokens, ctx): LinkDraft => {
const args = parsePipeArgs(tokens, ctx);
return {
type: "link",
url: args.text(0), // ← 漏写 url 会报编译错误
value: args.materializedTailTokens(1),
};
},
};如果你更喜欢显式接口:
// 1. 给每个标签定义接口
interface PlainText extends TextToken {
type: "text";
value: string;
}
interface BoldToken extends TextToken {
type: "bold";
value: TextToken[];
}
interface LinkToken extends TextToken {
type: "link";
url: string;
value: TextToken[];
}
interface CodeBlockToken extends TextToken {
type: "code";
lang: string;
value: string;
}
// 2. 联合类型
type MyToken = PlainText | BoldToken | LinkToken | CodeBlockToken;
// 3. 解析时 cast 一次
const tokens = parseRichText(input, {handlers}) as MyToken[];
// 4. switch 穷举
function render(token: MyToken): string {
switch (token.type) {
case "text":
return token.value;
case "bold":
return `<b>${token.value.map(t => render(t as MyToken)).join("")}</b>`;
case "link":
return `<a href="${token.url}">${token.value.map(t => render(t as MyToken)).join("")}</a>`;
case "code":
return `<pre data-lang="${token.lang}">${token.value}</pre>`;
default: {
const _: never = token;
return String(_);
}
}
}不想定义接口的话,运行时 typeof 也行:
if (token.type === "link" && typeof token.url === "string") {
console.log("Link to:", token.url);
}安全性不如判别联合(没有穷举检查),但临时用用够了。