printer:印刷器

把文档树渲染成排版成品的核心模块:Printer(概念与渲染器的登记处)、 PrinterComponent(执行两阶段渲染的组件)、PrinterRenderer(渲染器协议), 以及预处理产物的类型定义。预处理机制的原理见教程第 4 章和第 6 章。

Printer

Printer 是输出一侧的入口,同时也是全部概念定义的保管者(编辑器通过它查询概念)。 和 EditorCore 一样,应用启动时构造一次、全程共用。四个构造参数在教程第 3 章出现过:

new Printer(
    first_class_concepts: FirstClassConcept[],
    second_class_concepts: SecondClassConcept[],
    renderers: RendererDict,            // {group: {一级概念名: PrinterRenderer}, ...}
    default_renderers: DefaultRendererDict, // 每种节点类型一个默认渲染器
)

它的方法可以分成四类:动态注册(add_ 系列,用于运行时追加概念和渲染器)、 概念与渲染器的查询(get_ 系列)、参数加工(process_parameters), 以及预处理的入口(preprocess,通常由印刷器组件自动调用,很少手动触发):

方法说明
add_first_concept(x) / add_second_concept(x) / add_renderer(type, name, renderer)构造之后动态注册概念和渲染器
get_first_concept(type, name) / get_second_concept(type, name)按名字查询概念
get_node_first_concept(node) / get_node_second_concept(node)从节点出发解析它的概念
get_renderer(type, name?) / get_node_renderer(node)查询渲染器,查不到时返回默认渲染器
process_parameters(node)把节点参数加工成处理后的形式:套用二级概念的重写、求值函数型参数、剥掉类型包装
preprocess({root, init_env?})对整棵树做预处理,返回四件产物 [环境, 各节点上下文, 各节点参数, 缓存]
preprocess_init()异步初始化:调用各渲染器的 init(比如预加载数据),返回 Promise

PrinterComponent

印刷器的 React 组件(类组件),负责执行预处理和渲染。 通常通过它的包装 DefaultPrinterComponent 使用(见默认实现那页)。

interface PrinterComponentProps {
    printer: Printer
    root: AbstractNode
    init_env?: Env                        // 预处理的初始环境,一般不需要
    onUpdateCache?: (cache: PrinterCache) => void  // 每次预处理后把缓存交出来
}
实例方法说明
scroll_to(path) / scroll_to_idx(idx)滚动到指定节点,「点击引用跳转到目标」用它实现
get_ref(path) / get_ref_from_idx(idx) / bind_ref(path)取得或绑定节点对应的 DOM 元素
preprocess(params?) / update()手动触发预处理、手动刷新

PrinterRenderer:渲染器协议

印刷渲染器是 PrinterRenderer 类的实例。它把一个渲染器拆成几个阶段函数:

new PrinterRenderer<NodeType>({
    init?: () => Promise<void>,            // 一次性的异步准备工作
    enter?: PrinterEnterFunction<NodeType>, // 预处理进入节点时调用
    exit?: PrinterExitFunction<NodeType>,   // 预处理离开节点时调用
    renderer: PrinterRenderFunction<NodeType>,          // 渲染阶段的 React 组件
    renderer_as_property?: PrinterRenderFunction<NodeType>, // 仅抽象节点:作为属性渲染时用
})

各阶段函数的签名:

// 预处理阶段,按文档顺序对每个节点调用。直接原地修改 env 和 context。
type PrinterEnterFunction<NT> = (
    node: Readonly<NT>, path: Readonly<number[]>,
    parameters: Readonly<ProcessedParameterList>,
    env_draft: Env, context_draft: Context,
) => void

// 离开节点时调用。返回二元组:写入缓存的内容,以及是否已处理完成
//(返回 false 表示需要再跑一轮预处理迭代)。
type PrinterExitFunction<NT> = (同上述参数) => [PrinterCacheItem, boolean]

// 渲染阶段:一个普通的 React 组件。
interface PrinterRenderFunctionProps<NT> {
    node: NT
    context: Context                       // 预处理为该节点留下的结论
    parameters: {[key: string]: any}       // 处理后的参数
    children?: React.ReactNode             // 子节点的渲染结果,务必输出
}

直接手写 enterexit 属于底层用法。日常开发用 默认实现的工厂函数配合 contexter: 工厂会把 contexter 的钩子自动接到这两个位置上(见教程第 6 章)。

预处理产物的类型

预处理返回的四件产物(环境、各节点上下文、各节点参数、缓存)各有自己的类型名。 写渲染器和 contexter 时会反复见到它们,值得对照教程第 6 章记一遍:

类型说明
Env{[key]: any}。预处理过程中的全局环境,各 contexter 用自己的 key 在其中各占一块。
Context{[key]: any}。预处理留给单个节点的结论,渲染阶段该节点可读。
ProcessedParameterList{[key]: any}。处理后的参数,值是普通值。
PrinterCache / PrinterCacheItem按节点 idx 索引的缓存表。exit 的返回值汇集于此,全局可查,引用名等「给别人看的信息」放这里。
RendererDict{group: {名: PrinterRenderer}, inline: ..., structure: ..., support: ..., abstract: ...}
DefaultRendererDict七种节点类型各一个默认 PrinterRenderer

渲染上下文 hooks

印刷渲染函数内部可以使用一组 hooks 访问整个印刷现场的信息 (由 PrinterGlobalInfo 这个 React Context 提供):

Hook说明
usePrinter() / usePrinterGlobalInfo()印刷器全局信息
usePrinterRoot()正在被渲染的根节点
usePrinterComponent()PrinterComponent 实例,可调用 scroll_to_idx 等方法
usePrinterAllEnv() / usePrinterAllContexts() / usePrinterAllParameters() / usePrinterAllCaches()预处理的全部产物。实现引用显示时最常用的是 caches:按目标 idx 查出引用名。