core: Concepts & Nodes

core is the pure data layer: the concept classes, the type definitions of the document tree, and the tree validation functions. It has no React dependency and runs in any JavaScript environment, including Node.js (validating documents on a server, for example).

Concept classes

FirstClassConcept

new FirstClassConcept({
    type: "group" | "inline" | "structure" | "support" | "abstract",
    name: string,
    parameter_prototype?: ParameterList,   // the parameter prototype, defaults to {}
    meta_parameters?: MetaParameters,      // meta parameters, defaults to {}
})

A first-class concept declares the mechanism of a category of document content: which parameters it has and their types and defaults. The pair (type, name) identifies it uniquely.

meta_parameters are a small set of switches affecting editor behavior; unlike ordinary parameters they are invisible to authors:

FieldMeaning
force_inline?: booleanForce inline rendering. An inline image, for example: a support node that must sit inside the text flow.
force_block?: booleanForce block rendering.
force_void?: booleanForce the node to have no children (the editor will not allow typing inside).

SecondClassConcept

new SecondClassConcept({
    type: AllConceptTypes,
    first_concept: string,               // name of the inherited first-class concept
    name: string,                        // the authors' word, stored in nodes' concept field
    default_override?: ParameterList,    // override defaults (authors can still edit)
    fixed_override?: FixedParameterList, // pin values (authors cannot edit; functions allowed)
})

Second-class concepts are the vocabulary authors actually use. In fixed_override, an entry of the form {type: "function", val: "p => ..."} is a function-typed parameter: it is evaluated at print time with the processed parameter table as its argument, letting the value follow other parameters (see tutorial chapter 7).

Node types

The interfaces and child constraints of the seven node kinds (for the narrative introduction, see tutorial chapter 1):

TypeKey fieldsChild constraint
TextNode{text: string}no children
ParagraphNode{children}, no type fieldtext / inline / support
InlineNodetype: "inline" plus concept fieldstext / inline
GroupNodetype: "group" plus concept fields and relationnon-leaf nodes (no bare text or inline)
StructNodetype: "structure" plus concept fields and relationgroups only
SupportNodetype: "support" plus concept fieldsfixed placeholder [{text: ""}]
AbstractNodetype: "abstract" plus concept fieldssame as group; the document root is this type

The concept fields are the four fields every concept node shares:

idx: string              // node identifier, unique per document, produced by gene_idx()
concept: string          // second-class concept name
parameters: ParameterList
abstract: AbstractNode[] // abstract nodes attached to this node

The common unions:

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"

Parameter types

Parameter values are not bare values but small typed objects, with three base types shown below. The design was explained in tutorial chapter 1: the type annotation is what lets the editor build a fitting editing control for each parameter.

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 }

Parameter value objects may carry extra fields. The default parameter editor understands choices: [...]: a parameter carrying it renders as a dropdown rather than free input. FixedParameterValue additionally allows {type: "function", val: string}.

Type guards and lookup utilities

When handling a node object of unknown origin (walking a tree, parsing clipboard content), the first step is telling which kind of node it is. core provides a guard function for each kind, doubling as TypeScript type narrowing; two path-based lookup helpers come along with them:

FunctionDescription
is_concetnode(node)Whether the node is a concept node. The "concet" in the name is a historical spelling.
is_inlinenode / is_groupnode / is_supportnode / is_structnode / is_abstractnodePer-kind guards
is_paragraphnode / is_textnodeParagraph and text guards
get_node_type(node): AllNodeTypesThe node's type string; throws BadNodeError on unrecognizable objects
find_node_by_path(root, path)Locate a node by path (an array of child indices)
find_concept_nodes_by_path(root, path)Collect the concept nodes passed along a path, excluding the root

Validation

There are two validation functions, with the following signatures:

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

validate recursively checks whether a tree is a legal document tree: fields present, parameter types matching, the child constraints of each node kind holding. It returns a pair: whether the tree is legal, and if not, an error message carrying the node path, shaped like node [1,0]: .... Call it before handing external data (uploads, old archives) to the editor; see tutorial chapter 7. validate_parameters is the parameter-checking part, exported separately as well.

General utilities (lib/utils)

A few small utilities belong to no particular module. merge_object deserves a separate mention: the "specify only what you change, keep the rest" semantics of style configuration is implemented with it:

FunctionDescription
gene_idx(): stringGenerate a random node identifier (a numeric string)
merge_object(a, b)Recursively merge two objects, b winning at conflicting leaves. The partial-override style configuration is built on it.
object_foreach(obj, fn)Map every value of an object into a new object

Error classes

The library defines three error classes, all extending Error and distinguishable with instanceof: UnexpectedParametersError (a function received arguments violating its contract), BadNodeError (a node violating its contract), and ImpossibleError (a code path that should be unreachable was reached).