core:概念与节点

core 是纯数据层,包含概念类、文档树的类型定义和树的校验函数。 它不依赖 React,可以在任何 JavaScript 环境中使用,包括 Node.js(比如在服务端校验文档)。

概念类

FirstClassConcept

new FirstClassConcept({
    type: "group" | "inline" | "structure" | "support" | "abstract",
    name: string,
    parameter_prototype?: ParameterList,   // 参数原型,缺省为 {}
    meta_parameters?: MetaParameters,      // 元参数,缺省为 {}
})

一级概念声明一类文档成分的机制:有哪些参数、各参数的类型和默认值。 (type, name) 二元组是它的唯一标识。

meta_parameters(元参数)是少数几个影响编辑器行为的开关,与普通参数不同,写作者不可见:

字段含义
force_inline?: boolean强制以行内样式渲染。比如行内图片:虽然是支撑节点,但要嵌在文字流里。
force_block?: boolean强制以块级样式渲染。
force_void?: boolean强制无子节点(编辑器不允许在其中输入内容)。

SecondClassConcept

new SecondClassConcept({
    type: AllConceptTypes,
    first_concept: string,               // 继承的一级概念名
    name: string,                        // 写作者使用的名字,写进节点的 concept 字段
    default_override?: ParameterList,    // 重写参数默认值(写作者仍可修改)
    fixed_override?: FixedParameterList, // 固定参数值(写作者不可修改,支持函数型)
})

二级概念是写作者实际使用的词汇。fixed_override 中值为 {type: "function", val: "p => ..."} 的参数是函数型参数: 印刷时以处理后的参数表为入参求值,让参数值随其他参数联动(用法见教程第 7 章)。

节点类型

七种节点的接口和子节点约束如下(直观的介绍见教程第 1 章):

类型关键字段子节点约束
TextNode{text: string}无子节点
ParagraphNode{children},没有 type 字段text / inline / support
InlineNodetype: "inline" 加概念字段text / inline
GroupNodetype: "group" 加概念字段和 relation非叶节点(不能直接是 text 或 inline)
StructNodetype: "structure" 加概念字段和 relation只能是 group
SupportNodetype: "support" 加概念字段固定为 [{text: ""}] 占位
AbstractNodetype: "abstract" 加概念字段与 group 相同;文档根节点就是此类型

「概念字段」指所有概念节点共有的四个字段:

idx: string              // 节点编号,全文唯一,由 gene_idx() 生成
concept: string          // 二级概念名
parameters: ParameterList
abstract: AbstractNode[] // 附着在此节点上的抽象节点

常用的联合类型:

type Node               = TextNode | ParagraphNode | ConceptNode
type ConceptNode        = InlineNode | GroupNode | StructNode | SupportNode | AbstractNode
type NonLeafNode        = ParagraphNode | NonLeafConceptNode
type AllConceptTypes    = "group" | "inline" | "structure" | "support" | "abstract"
type AllNodeTypes       = AllConceptTypes | "paragraph" | "text"

参数类型

参数的值不是裸值,而是带类型标注的小对象,三种基础类型如下。 这个设计在教程第 1 章解释过:类型标注让编辑器能为每种参数生成合适的编辑控件。

type ParameterValue =
    | {type: "string",  val: string,  [k: string]: any}
    | {type: "number",  val: number,  [k: string]: any}
    | {type: "boolean", val: boolean, [k: string]: any}

interface ParameterList { [key: string]: ParameterValue }

参数值对象允许携带额外字段。默认参数编辑器认识其中的 choices: [...]: 带这个字段的参数会渲染成下拉选择而不是自由输入。 FixedParameterValue 在此基础上额外允许 {type: "function", val: string}

类型守卫与查找工具

处理来历不明的节点对象时(遍历一棵树、解析剪贴板内容都属于这种情况),第一件事是判断它是哪种节点。 core 为每种节点提供了守卫函数,返回值兼作 TypeScript 的类型收窄;此外还有两个按路径查找节点的工具:

函数说明
is_concetnode(node)是否为概念节点。注意函数名里的 concet 是历史拼写。
is_inlinenode / is_groupnode / is_supportnode / is_structnode / is_abstractnode各类型的守卫函数
is_paragraphnode / is_textnode段落和文本的守卫函数
get_node_type(node): AllNodeTypes返回节点类型字符串;遇到无法辨认的对象抛 BadNodeError
find_node_by_path(root, path)按路径(子节点下标数组)查找节点
find_concept_nodes_by_path(root, path)收集路径沿途经过的所有概念节点,不含根节点

校验

校验函数有两个,签名如下:

validate(tree: any): [boolean, string]
validate_parameters(parameters: any, gene_msg: (s: string) => string): [boolean, string]

validate 递归检查一棵树是否为合法的文档树:字段是否齐全、参数类型是否匹配、 各类节点的子节点约束是否成立。返回值是一个二元组,第一项表示是否合法, 第二项在不合法时给出带节点路径的错误说明,形如 node [1,0]: ...。 在把外部数据(用户上传、旧存档)交给编辑器之前应当先调用它,用法见教程第 7 章。 validate_parameters 是其中校验参数表的部分,也单独导出。

通用工具(lib/utils)

另有几个不属于任何模块的通用小工具。其中 merge_object 值得单独认识: 样式配置「只写想改的项、其余沿用默认」的合并语义就是它实现的:

函数说明
gene_idx(): string生成一个随机的节点编号(数字字符串)
merge_object(a, b)递归合并两个对象,叶子值冲突时以 b 为准。样式配置的「部分覆盖」就是用它实现的。
object_foreach(obj, fn)对对象的每个值做映射,返回新对象

异常类

库定义了三个异常类,都继承自 Error,可以用 instanceof 判别: UnexpectedParametersError(函数收到了不符合约定的参数)、 BadNodeError(遇到了不符合约定的节点)、 ImpossibleError(执行到了理论上不可能到达的代码路径)。