@nextgensoftwares/folio-model
Document model: ProseMirror-shaped JSON, schemas, validation. DOM-free, no dependencies. Guide: Document model & schema.
Node types
type Attrs = Record<string, unknown>;
interface Mark { type: string; attrs?: Attrs }
interface FolioNode { type: string; attrs?: Attrs; content?: FolioNode[]; text?: string; marks?: Mark[] }
interface FolioDocument extends FolioNode { type: 'doc'; content: FolioNode[] }
type NodePath = readonly number[];| Export | Signature | Description |
|---|---|---|
isTextNode | (node: FolioNode) => boolean | node.type === 'text' |
attr | <T>(node: FolioNode, name: string, fallback: T) => T | attribute value, or fallback when undefined/null |
nodeAt | (root: FolioNode, path: NodePath) => FolioNode | undefined | follow a child-index path |
textContent | (node: FolioNode) => string | concatenated text; inline atoms and hard breaks contribute nothing |
marksEqual | (a?: readonly Mark[], b?: readonly Mark[]) => boolean | same mark types and attrs, in order |
Schema
interface AttrSpec { default?: unknown; validate?: (value: unknown) => boolean } // no default = required
interface NodeSpec {
group?: string; content?: string; inline?: boolean; atom?: boolean;
marks?: string; // '_' all (textblock default), '' none, or space-separated names
code?: boolean; // preformatted, no marks
ownSelection?: boolean; // a plugin draws this node's selection (views paint no node highlight)
attrs?: Record<string, AttrSpec>;
}
interface MarkSpec { attrs?: Record<string, AttrSpec>; excludes?: string }
interface GlobalAttrs { types: string[]; attrs: Record<string, AttrSpec> }
interface SchemaSpec { nodes: Record<string, NodeSpec>; marks: Record<string, MarkSpec>; globalAttrs?: GlobalAttrs[]; topNode?: string }
interface ResolvedNodeSpec extends NodeSpec { name: string; attrs: Record<string, AttrSpec>; contentExpr: ContentExpr; isTextblock: boolean }class Schema
| Member | Description |
|---|---|
new Schema(spec: SchemaSpec) | resolves global attrs into each node, parses content expressions, computes isTextblock; throws if topNode (default 'doc') is missing |
spec, nodes: Map<string, ResolvedNodeSpec>, marks: ReadonlyMap<string, MarkSpec>, topNode | the resolved schema |
extend(extra: Partial<SchemaSpec>): Schema | a new schema with extra nodes, marks and global attrs (host extensions) |
node(name) | resolved node spec or undefined |
fits(childType, name) | does a node of childType satisfy the name-or-group name? |
matches(parentType, childTypes) | { ok, failedAt } against the parent's content expression |
allowsMark(parentType, markType) | mark allowed inside this parent (never in code nodes) |
withDefaults(specAttrs?, attrs?) | default-filled attrs; unknown attrs kept untouched |
standardSchema
The built-in node and mark set (StarterKit + tables + code + math + media + text styles, plus Word pagination attrs). See the full list.
ListLevel, isListLevel(value): a list's own marker definition (the listLevel attr on bulletList/orderedList: format, template text, font, colour, size, bold, picture bullet, indent, hanging, align; see the schema) and its validator (rejects unknown keys and javascript: images).
LIST_STYLE_TYPES: the accepted listStyleType values (disc, circle, square, decimal, decimal-leading-zero, lower-alpha, upper-alpha, lower-latin, upper-latin, lower-roman, upper-roman, lower-greek, none).
FONT_VARIANTS: the accepted textStyle fontVariant values (small-caps, all-small-caps); see caps and letter spacing.
TAB_ALIGNS, TAB_LEADERS, tabStopsAttr: the accepted tabStops alignments (left, center, right, decimal) and leaders (none, dot, hyphen, underscore, middleDot), and the attr spec validating paragraph/heading tabStops ([{ pos, align?, leader? }], at most 64). Tabs themselves are \t characters in text.
Paragraph borders and shading (Word's w:pBdr / w:shd): paragraphs and headings declare borders ({ top?, bottom?, left?, right?, between? }, each { style: 'single' | 'double' | 'dashed' | 'dotted' | 'none', width, color, space } with px lengths) and shading ({ fill?, pattern?: 'solid' | 'pctNN' | 'clear', color? }). Plain JSON. Consecutive paragraphs with equal borders, shading, indents and direction share one box: the top border on the first, between rules between them, the bottom border on the last; a box cut by a page break has no rule at the cut. Borders sit space px outside the text, which keeps its width.
WRAP_MODES: the accepted media wrap values (inline, square, topBottom, behind, front). resizableMedia also declares distT, distB, distL, distR (px, 0–480), offsetX, offsetY (px, ±4000) and anchor (paragraph | margin); see Media ▸ Size and placement.
Content expressions
interface ContentTerm { names: string[]; min: number; max: number }
type ContentExpr = ContentTerm[];| Export | Signature | Description |
|---|---|---|
parseContentExpr | (source: string) => ContentExpr | "paragraph block*", "(tableCell|tableHeader)*"; throws on bad syntax |
matchContent | (expr, childTypes, fits) => { ok: boolean; failedAt: number } | greedy, non-backtracking match; failedAt is the first child that doesn't fit (-1 if all match) |
Validation
interface ValidationIssue { path: NodePath; message: string }| Export | Signature | Description |
|---|---|---|
validate | (root: FolioNode, schema: Schema) => ValidationIssue[] | unknown types and marks, missing/invalid attrs, content mismatches, disallowed marks, empty text nodes. Never throws |
normalize | <T extends FolioNode>(node: T, schema: Schema) => T | cleaned copy: defaults filled, empty text dropped, equal-mark text merged, disallowed marks removed |
omitDefaults | <T extends FolioNode>(node: T, schema: Schema) => T | the opposite, for storage: attrs that are null or equal to their schema default removed from nodes and marks (the editor and plugins write every declared attr). Unknown attrs kept; unchanged subtrees keep their identity. canonicalJson(json, { schema }) also ignores defaults when comparing |
Block ids
Every block type (not inline, not the top node) declares an optional id (BLOCK_ID_ATTR, spec BLOCK_ID_SPEC: null or a non-empty string), host types from schema.extend() included, so ids survive the editor, sync and exports. Heading ids are also PDF named destinations (#id links, outline).
| Export | Signature | Description |
|---|---|---|
blockIdOf | (node) => string | null | a node's id |
hasBlockId | (schema, type) => boolean | can this type carry an id |
blockById | (doc, id) => { node, path } | null | first block with this id (O(blocks)) |
pathOfBlockId / blockIdAt | (doc, id) => number[] | null / (doc, path) => string | null | path ↔ id |
indexBlockIds | (doc) => Map<string, number[]> | every id → path of its first occurrence |
ensureBlockIds | <T>(doc: T, { seed, schema?, generate? }: EnsureBlockIdsOptions) => T | gives every id-capable block an id: existing ids kept (later duplicates renamed), missing ones generate(path, seed, attempt) (default defaultBlockId: hashId(seed/path)). Pure, deterministic per seed, idempotent (returns the same object), unchanged subtrees keep their identity |
hashId | (s: string) => string | 12-character base32 id (60 bits) from a string; same on every platform |
randomBlockId | (length = 12) => string | a fresh random id (editor-minted) |