Document Model
The document model is a tree of immutable data types. All mutations create new instances.
Document
Section titled “Document”The root container holding an array of blocks:
interface Document { readonly children: readonly BlockNode[];}Factory Functions
Section titled “Factory Functions”import { createDocument, createBlockNode, createTextNode, createInlineNode, nodeType, inlineType,} from '@notectl/core';
// Empty document (single empty paragraph)const doc = createDocument();
// Document with contentconst doc = createDocument([ createBlockNode(nodeType('heading'), [ createTextNode('Hello World'), ], undefined, { level: 1 }), createBlockNode(nodeType('paragraph'), [ createTextNode('Some text'), ]),]);
// InlineNode (atomic, width-1 element)const br = createInlineNode(inlineType('hard_break'));BlockNode
Section titled “BlockNode”A block-level node (paragraph, heading, list item, etc.):
interface BlockNode { readonly id: BlockId; readonly type: NodeTypeName; readonly attrs?: BlockAttrs; readonly children: readonly ChildNode[];}ChildNode
Section titled “ChildNode”A child of a BlockNode can be text, an inline element, or a nested block:
type ChildNode = TextNode | InlineNode | BlockNode;Block Types
Section titled “Block Types”| Type | Description | Attributes |
|---|---|---|
paragraph | Standard text block | — |
heading | Heading (H1-H6) | level: number |
title | Document title (H1 variant) | — |
subtitle | Document subtitle (H2 variant) | — |
list_item | List entry | listType, indent, checked |
blockquote | Block quote | — |
code_block | Code block with syntax highlighting | language?: string |
horizontal_rule | Horizontal line (void) | — |
image | Image block (void) | src, alt?, width?, height? |
table | Table container | — |
table_row | Table row | — |
table_cell | Table cell | colspan?, rowspan? |
TextNode
Section titled “TextNode”An inline text segment with marks:
interface TextNode { readonly type: 'text'; readonly text: string; readonly marks: readonly Mark[];}Factory
Section titled “Factory”import { createTextNode, markType } from '@notectl/core';
// Plain textconst text = createTextNode('hello');
// Text with marksconst bold = createTextNode('bold text', [ { type: markType('bold') },]);
// Text with attributed marksconst colored = createTextNode('red text', [ { type: markType('textColor'), attrs: { color: '#FF0000' } },]);InlineNode
Section titled “InlineNode”An atomic inline element that occupies width 1 in offset space (e.g., hard break, mention, emoji):
interface InlineNode { readonly type: 'inline'; readonly inlineType: InlineTypeName; readonly attrs: Readonly<Record<string, string | number | boolean>>;}Factory
Section titled “Factory”import { createInlineNode, inlineType } from '@notectl/core';
const br = createInlineNode(inlineType('hard_break'));const emoji = createInlineNode(inlineType('emoji'), { name: 'rocket' });InlineNodes are rendered with contenteditable="false" in the DOM and behave as atomic units for selection and editing.
An inline annotation applied to a text range:
interface Mark { readonly type: MarkTypeName; readonly attrs?: Readonly<Record<string, string | number | boolean>>;}Mark Types
Section titled “Mark Types”| Type | Attributes | Description |
|---|---|---|
bold | — | Bold text |
italic | — | Italic text |
underline | — | Underlined text |
strikethrough | — | Strikethrough text |
superscript | — | Superscript text |
subscript | — | Subscript text |
link | href: string | Hyperlink |
textColor | color: string | Text color |
highlight | color: string | Background highlight |
font | family: string | Font family |
fontSize | size: string | Font size |
Content Segments
Section titled “Content Segments”For mark-preserving undo/redo and block range operations:
interface TextSegment { readonly text: string; readonly marks: readonly Mark[];}
type ContentSegment = | { readonly kind: 'text'; readonly text: string; readonly marks: readonly Mark[] } | { readonly kind: 'inline'; readonly node: InlineNode };Utility Functions
Section titled “Utility Functions”Block Inspection
Section titled “Block Inspection”import { getBlockText, getBlockLength, getTextChildren, getInlineChildren, getBlockChildren, getBlockMarksAtOffset, getContentAtOffset, isTextNode, isInlineNode, isBlockNode, isLeafBlock,} from '@notectl/core';
const text = getBlockText(block); // "Hello World"const len = getBlockLength(block); // 11 (InlineNodes count as 1)const textNodes = getTextChildren(block); // TextNode[]const inlines = getInlineChildren(block); // (TextNode | InlineNode)[]const blocks = getBlockChildren(block); // BlockNode[] (nested children)const marks = getBlockMarksAtOffset(block, 5); // Mark[] at offsetconst content = getContentAtOffset(block, 0); // { kind: 'text', char, marks } | { kind: 'inline', node } | nullMark Operations
Section titled “Mark Operations”import { hasMark, markSetsEqual, markType } from '@notectl/core';
hasMark(marks, markType('bold')); // booleanmarkSetsEqual(marks1, marks2); // booleanNode Resolution
Section titled “Node Resolution”import { resolveNodeByPath, resolveParentByPath, findNodePath, findNode, findNodeWithPath, walkNodes,} from '@notectl/core';
const node = resolveNodeByPath(doc, path); // BlockNode | undefinedconst { parent, index } = resolveParentByPath(doc, path);const path = findNodePath(doc, blockId); // string[] | undefinedconst node = findNode(doc, blockId); // BlockNode | undefinedconst { node, path } = findNodeWithPath(doc, blockId);walkNodes(doc, (block, path) => { /* DFS visitor */ });Type Guards
Section titled “Type Guards”import { isNodeOfType, isMarkOfType, isInlineNodeOfType, isTextNode, isInlineNode, isBlockNode,} from '@notectl/core';
// These type guards require module augmentation of the attribute registries.// Plugins like HeadingPlugin, LinkPlugin, and HardBreakPlugin augment the// registries automatically when imported.
if (isNodeOfType(block, 'heading')) { console.log(block.attrs.level); // Type-safe access}
if (isMarkOfType(mark, 'link')) { console.log(mark.attrs.href); // Type-safe access}
if (isInlineNodeOfType(node, 'hard_break')) { // Type-safe InlineNode access}Branded Types
Section titled “Branded Types”notectl uses branded types for compile-time safety:
import { blockId, nodeType, markType, inlineType, pluginId, commandName,} from '@notectl/core';
const id = blockId('abc123'); // BlockIdconst nt = nodeType('paragraph'); // NodeTypeNameconst mt = markType('bold'); // MarkTypeNameconst it = inlineType('hard_break'); // InlineTypeNameconst pid = pluginId('my-plugin'); // PluginIdconst cmd = commandName('toggleBold'); // CommandNameThese prevent accidental mixing of string types at compile time.