en Interpret - chiba233/yume-dsl-token-walker GitHub Wiki

Interpret API

Structural Query | Lint | Home

Interpret 是 token-walker 的核心能力:把 TextToken[] 树变成你想要的任何输出类型。 HTML 字符串、React VNode、Vue render function、游戏引擎指令、纯文本——都行。 你定义 TNode 是什么,interpret 就 yield 什么。


一张图看懂

你的 DSL 文本
    │
    ▼
yume-dsl-rich-text 解析
    │
    ▼
TextToken[]  ←── 这是你拿到的东西
    │
    ▼
interpretTokens(tokens, ruleset, env)
    │
    ├─ token.type === "text"  → ruleset.createText(token.value)
    │
    └─ token.type === "bold"  → ruleset.interpret(token, helpers)
                                    │
                                    ▼
                               你返回 InterpretResult
                               { type: "nodes", nodes: [...] }
                                    │
                                    ▼
                               Generator<TNode> ── 逐个 yield,不缓冲

关键设计:

  • interpretTokens 是一个 Generator——节点逐个 yield,不会一次性塞进数组。你可以流式消费上千 token 而不需要缓冲
  • type: "text" 的 token 自动走 createText,不经过 interpret——你只需要处理非文本标签
  • 如果 interpret 返回 { type: "unhandled" },走 onUnhandled 策略(默认:flatten 成纯文本)

30 秒上手

import { createParser, createSimpleInlineHandlers } from "yume-dsl-rich-text";
import { interpretText, collectNodes } from "yume-dsl-token-walker";

// 1. 建 parser
const parser = createParser({
  handlers: createSimpleInlineHandlers(["bold", "italic"]),
});

// 2. 一行搞定:解析 + 解释 + 收集
const html = collectNodes(
  interpretText("Hello $$bold(world)$$!", parser, {
    createText: (text) => text,
    interpret: (token, helpers) => {
      if (token.type === "bold")
        return { type: "nodes", nodes: ["<b>", ...helpers.interpretChildren(token.value), "</b>"] };
      if (token.type === "italic")
        return { type: "nodes", nodes: ["<em>", ...helpers.interpretChildren(token.value), "</em>"] };
      return { type: "unhandled" };
    },
  }, undefined),
).join("");

// → "Hello <b>world</b>!"

interpretTextparser.parse(input) + interpretTokens(...) 的语法糖。 如果你已经有 TextToken[],直接用 interpretTokens


你的 interpret 函数返回什么

每次你的 interpret 被调用,你需要返回一个 InterpretResult。一共五种:

返回值 意思 什么时候用
{ type: "nodes", nodes: [...] } yield 这些节点 大多数情况——包裹子节点、加标签
{ type: "text", text: "..." } yield 一个文本节点 输出一段特定文本,不递归子节点
{ type: "flatten" } 把 token.value 展平成纯文本后 yield 搜索索引、aria label、纯文本预览
{ type: "drop" } 什么都不 yield 注释、元数据等不产生输出的标签
{ type: "unhandled" } 交给 onUnhandled 策略 你不认识这个标签

nodes 里用 helpers.interpretChildren 递归

helpers.interpretChildren(token.value) 返回一个 Iterable<TNode>——它会递归解释子 token 树。 用展开运算符 ...helpers.interpretChildren(token.value) 把子节点嵌进你的输出:

interpret: (token, helpers) => {
  if (token.type === "bold") {
    return {
      type: "nodes",
      nodes: ["<b>", ...helpers.interpretChildren(token.value), "</b>"],
    };
  }
  return { type: "unhandled" };
},

flattenhelpers.flattenText 的区别

  • { type: "flatten" } —— 让引擎自动展平 token.value,结果经过 createText 包装
  • helpers.flattenText(token.value) —— 在 handler 里手动拿到纯文本字符串,你自己决定怎么用
// 用 flattenText 做搜索标签
if (token.type === "searchable") {
  const plain = helpers.flattenText(token.value);
  return { type: "text", text: `[SEARCH:${plain}]` };
}

Demo: 三种输出类型

Demo 1: 输出 HTML 字符串

最常见的场景——token 树变成 HTML:

import { createParser, createSimpleInlineHandlers, createPipeHandlers } from "yume-dsl-rich-text";
import { interpretText, collectNodes, createRuleset, fromHandlerMap } from "yume-dsl-token-walker";

const parser = createParser({
  handlers: {
    ...createSimpleInlineHandlers(["bold", "italic", "underline"]),
    ...createPipeHandlers({
      link: {
        inline: (args, ctx) => ({
          type: "link",
          url: args.text(0),
          value: args.materializedTailTokens(1),
        }),
      },
    }),
  },
});

const ruleset = createRuleset<string, void>({
  createText: (text) => text,
  interpret: fromHandlerMap({
    bold:      (token, h) => ({ type: "nodes", nodes: ["<b>", ...h.interpretChildren(token.value), "</b>"] }),
    italic:    (token, h) => ({ type: "nodes", nodes: ["<em>", ...h.interpretChildren(token.value), "</em>"] }),
    underline: (token, h) => ({ type: "nodes", nodes: ["<u>", ...h.interpretChildren(token.value), "</u>"] }),
    link:      (token, h) => ({
      type: "nodes",
      nodes: [`<a href="${token.url}">`, ...h.interpretChildren(token.value), "</a>"],
    }),
  }),
  onUnhandled: "flatten",
});

const input = "Hello $$bold($$italic(world)$$)$$ - $$link(https://example.com | click)$$";
const html = collectNodes(interpretText(input, parser, ruleset, undefined)).join("");
// → 'Hello <b><em>world</em></b> - <a href="https://example.com">click</a>'

Demo 2: 输出自定义 AST 节点

TNode 不必是 string。可以是你自己定义的 AST、VNode、或任何类型:

type HtmlNode =
  | { kind: "text"; value: string }
  | { kind: "element"; tag: string; attrs?: Record<string, string>; children: HtmlNode[] };

const ruleset = createRuleset<HtmlNode, void>({
  createText: (text) => ({ kind: "text", value: text }),
  interpret: fromHandlerMap({
    bold: (token, h) => ({
      type: "nodes",
      nodes: [{ kind: "element", tag: "strong", children: Array.from(h.interpretChildren(token.value)) }],
    }),
    link: (token, h) => ({
      type: "nodes",
      nodes: [{
        kind: "element",
        tag: "a",
        attrs: { href: (token.url as string) ?? "#" },
        children: Array.from(h.interpretChildren(token.value)),
      }],
    }),
  }),
});

// 拿到的是 HtmlNode[],不是字符串——你可以 serialize 成 HTML,也可以交给 React/Vue 渲染
const nodes = collectNodes(interpretTokens(tokens, ruleset, undefined));

Demo 3: 输出纯文本(搜索索引 / 预览)

不需要 ruleset——flattenText 是独立工具函数:

import { flattenText } from "yume-dsl-token-walker";

const tokens = parser.parse("Hello $$bold($$italic(world)$$)$$ - $$link(https://example.com | click)$$");
const plain = flattenText(tokens);
// → "Hello world - click"

用于搜索索引、aria label、通知预览、纯文本导出。


env: 注入运行时上下文

env 是你传给 interpretTokens 的第三个参数,在每个 handler 里通过 helpers.env 访问。 用它传入主题、语言、权限、feature flag 等运行时信息:

interface Env {
  theme: "light" | "dark";
  locale: string;
}

const ruleset = createRuleset<string, Env>({
  createText: (text) => text,
  interpret: (token, helpers) => {
    if (token.type === "bold") {
      const color = helpers.env.theme === "dark" ? "#fff" : "#000";
      return {
        type: "nodes",
        nodes: [`<b style="color:${color}">`, ...helpers.interpretChildren(token.value), "</b>"],
      };
    }
    return { type: "unhandled" };
  },
});

const html = collectNodes(
  interpretTokens(tokens, ruleset, { theme: "dark", locale: "ja" }),
).join("");

不要往 env 里塞业务状态。 env 是只读的运行时配置。如果你需要累加器或计数器,用闭包外部的变量。


onUnhandled 策略

interpret 返回 { type: "unhandled" } 时,引擎按 onUnhandled 策略处理。 默认是 "flatten"——展平成纯文本。

策略 行为 什么时候用
"flatten" (默认) 展平成纯文本 生产环境——未知标签安静降级
"throw" 直接 throw 测试环境——漏处理的标签立即暴露
"drop" 什么都不输出 需要完全忽略未知标签
function 自定义处理 调试——输出可见占位符
// 调试模式:用 debugUnhandled 输出占位符
import { debugUnhandled } from "yume-dsl-token-walker";

const ruleset = createRuleset({
  createText: (text) => text,
  interpret: fromHandlerMap({ /* 你的 handler */ }),
  onUnhandled: process.env.NODE_ENV === "production"
    ? "flatten"
    : debugUnhandled(), // → "[unhandled:someTag]"
});

辅助函数速查

函数 干什么 什么时候用
createRuleset(ruleset) 恒等函数,只提供类型推断 定义 ruleset 时获得完整 TS 提示
fromHandlerMap(handlers) Record<type, handler> 变成 interpret 函数 中大型项目,handler 按标签分文件
dropToken 现成的 { type: "drop" } handler 注释、元数据等不产生输出的标签
unwrapChildren 现成的"透传子节点"handler 结构性标签,不需要包装容器
wrapHandlers(handlers, wrap) 给每个 handler 套一层统一的包装 所有 block 标签都加 <div> 容器
debugUnhandled(format?) 返回 onUnhandled 函数,输出可见占位符 调试模式
collectNodes(iterable) Array.from 语法糖 把 Generator 收集成数组

Async API

Need await in your handlers? See the dedicated Async Interpret page — full demo, design points, helpers, and type reference.


推荐项目结构

小项目——写在一个文件里

const html = collectNodes(
  interpretTokens(tokens, {
    createText: (t) => t,
    interpret: (token, h) => {
      if (token.type === "bold")
        return { type: "nodes", nodes: ["<b>", ...h.interpretChildren(token.value), "</b>"] };
      return { type: "unhandled" };
    },
  }, undefined),
).join("");

中型项目——handler 分文件

src/
  dsl/
    handlers.ts    ← handler map
    ruleset.ts     ← createRuleset + fromHandlerMap
    interpret.ts   ← 调用 interpretTokens
// handlers.ts
export const handlers = {
  bold: (token, h) => ({ type: "nodes", nodes: ["<b>", ...h.interpretChildren(token.value), "</b>"] }),
  italic: (token, h) => ({ type: "nodes", nodes: ["<em>", ...h.interpretChildren(token.value), "</em>"] }),
  comment: () => ({ type: "drop" }),
};

// ruleset.ts
import { createRuleset, fromHandlerMap, debugUnhandled } from "yume-dsl-token-walker";
import { handlers } from "./handlers";

export const ruleset = createRuleset({
  createText: (text) => text,
  interpret: fromHandlerMap(handlers),
  onUnhandled: process.env.NODE_ENV === "production" ? "flatten" : debugUnhandled(),
});

大型项目——解析 / 解释 / 渲染分层

src/
  dsl/
    parser.ts      ← yume-dsl-rich-text 配置
    handlers/
      inline.ts    ← bold, italic, link, ...
      block.ts     ← info, warning, spoiler, ...
      index.ts     ← 合并 handler map
    ruleset.ts     ← createRuleset, env 类型
    interpret.ts   ← interpretTokens 包装
  render/
    toHtml.ts      ← TNode → HTML string
    toPlainText.ts ← flattenText 用于搜索 / 预览

原则:

  • env 只放运行时上下文——主题、语言、权限。不放业务状态
  • handler 是纯映射——token 进,result 出。副作用放 render 层
  • 一种输出格式一个 ruleset——需要 HTML 和纯文本就建两个 ruleset

完整签名参考

interpretText

function* interpretText<TNode, TEnv>(
  input: string,
  parser: ParserLike,
  ruleset: InterpretRuleset<TNode, TEnv>,
  env: TEnv,
): Generator<TNode>;

ParserLike 是任何有 parse(input, overrides?) 方法的对象。createParser(...) 返回的就是。

interpretTokens

function* interpretTokens<TNode, TEnv>(
  tokens: TextToken[],
  ruleset: InterpretRuleset<TNode, TEnv>,
  env: TEnv,
): Generator<TNode>;

InterpretRuleset

interface InterpretRuleset<TNode, TEnv = unknown> {
  createText: (text: string) => TNode;
  interpret: (token: TextToken, helpers: InterpretHelpers<TNode, TEnv>) => InterpretResult<TNode>;
  onUnhandled?: UnhandledStrategy<TNode, TEnv>;   // 默认 "flatten"
  onError?: (context: { error: Error; phase: "interpret" | "flatten" | "traversal" | "internal"; token?: TextToken; position?: SourceSpan; env: TEnv }) => void;
}

InterpretHelpers

interface InterpretHelpers<TNode, TEnv = unknown> {
  interpretChildren: (value: string | TextToken[]) => Iterable<TNode>;
  flattenText: (value: string | TextToken[]) => string;
  env: TEnv;
}
⚠️ **GitHub.com Fallback** ⚠️