Skip to content

Schema

The schema system defines how block types, inline marks, and inline nodes behave. Plugins register specs through the SchemaRegistry, which the editor uses for rendering, parsing, serialization, and content validation.

Central registry for all specs registered by plugins. Model-only — no DOM dependencies.

import { SchemaRegistry } from '@notectl/core';
const registry = new SchemaRegistry();
MethodSignatureDescription
registerNodeSpec(spec: NodeSpec<T>) => voidRegister a block node type
getNodeSpec(type: string) => NodeSpec | undefinedLook up by type name
removeNodeSpec(type: string) => voidRemove a registered spec
getNodeTypes() => string[]List all registered node type names
MethodSignatureDescription
registerMarkSpec(spec: MarkSpec<T>) => voidRegister an inline mark type
getMarkSpec(type: string) => MarkSpec | undefinedLook up by type name
removeMarkSpec(type: string) => voidRemove a registered spec
getMarkTypes() => string[]List all registered mark type names
MethodSignatureDescription
registerInlineNodeSpec(spec: InlineNodeSpec<T>) => voidRegister an inline node type
getInlineNodeSpec(type: string) => InlineNodeSpec | undefinedLook up by type name
removeInlineNodeSpec(type: string) => voidRemove a registered spec
getInlineNodeTypes() => string[]List all registered inline node type names
MethodReturn TypeDescription
getBlockParseRules()readonly { rule: ParseRule; type: string }[]All NodeSpec parse rules, sorted by priority descending
getInlineParseRules()readonly { rule: ParseRule; type: string }[]All InlineNodeSpec parse rules, sorted by priority descending
getMarkParseRules()readonly { rule: ParseRule; type: string }[]All MarkSpec parse rules, sorted by priority descending
MethodReturn TypeDescription
getAllowedTags()string[]All allowed HTML tags from base defaults + all spec sanitize configs
getAllowedAttrs()string[]All allowed HTML attributes from base defaults + all spec sanitize configs
registry.clear(); // Remove all registered specs

Defines how a block node type behaves, renders, and serializes.

interface NodeSpec<T extends string = string> {
readonly type: T;
toDOM(node: Omit<BlockNode, 'attrs'> & { readonly attrs: NodeAttrsFor<T> }): HTMLElement;
readonly attrs?: Readonly<Record<string, AttrSpec>>;
readonly isVoid?: boolean;
readonly content?: ContentRule;
readonly group?: string;
readonly isolating?: boolean;
readonly selectable?: boolean;
readonly excludeMarks?: readonly string[];
readonly toHTML?: (node: BlockNode, content: string, ctx?: HTMLExportContext) => string;
readonly parseHTML?: readonly ParseRule[];
readonly sanitize?: SanitizeConfig;
wrapper?(node: Omit<BlockNode, 'attrs'> & { readonly attrs: NodeAttrsFor<T> }): WrapperSpec;
}
PropertyTypeDefaultDescription
typeTrequiredUnique type name (e.g. 'heading', 'code_block')
toDOM(node) => HTMLElementrequiredRenders the block to a DOM element. Must set data-block-id on the root
attrsRecord<string, AttrSpec>undefinedAllowed attributes with defaults
isVoidbooleanfalseIf true, the node contains no editable text (e.g. image, horizontal rule)
contentContentRuleundefinedWhich children this node can contain
groupstringundefinedGroup membership ('block', 'inline', or custom)
isolatingbooleanfalseIf true, selection cannot cross this node’s boundary (e.g. table cells)
selectablebooleanfalseIf true, the node can be selected as an object via mouse
excludeMarksreadonly string[]undefinedMark types stripped when converting to this block type
toHTML(node, content, ctx?) => stringundefinedSerializes to an HTML string. content is pre-serialized inline children
parseHTMLreadonly ParseRule[]undefinedRules for matching HTML elements during parsing
sanitizeSanitizeConfigundefinedTags and attributes needed through DOMPurify sanitization
wrapper(node) => WrapperSpecundefinedGroups consecutive blocks into shared wrappers (e.g. <ul> around <li> items)
const headingSpec: NodeSpec<'heading'> = {
type: 'heading',
attrs: { level: { default: 1 } },
toDOM(node) {
const el = document.createElement(`h${node.attrs.level}`);
el.dataset.blockId = node.id;
return el;
},
toHTML(node, content) {
const level = node.attrs?.level ?? 1;
return `<h${level}>${content}</h${level}>`;
},
parseHTML: [
{ tag: 'H1', getAttrs: () => ({ level: 1 }) },
{ tag: 'H2', getAttrs: () => ({ level: 2 }) },
],
};

Defines how an inline mark type renders and serializes.

interface MarkSpec<T extends string = string> {
readonly type: T;
toDOM(mark: Omit<Mark, 'attrs'> & { readonly attrs: MarkAttrsFor<T> }): HTMLElement;
readonly rank?: number;
readonly attrs?: Readonly<Record<string, AttrSpec>>;
readonly toHTMLString?: (mark: Mark, content: string, ctx?: HTMLExportContext) => string;
readonly toHTMLStyle?: (mark: Mark) => string | null;
readonly parseHTML?: readonly ParseRule[];
readonly sanitize?: SanitizeConfig;
}
PropertyTypeDefaultDescription
typeTrequiredUnique type name (e.g. 'bold', 'link')
toDOM(mark) => HTMLElementrequiredWraps text content in a DOM element
ranknumberundefinedNesting priority — lower rank renders closer to the text
attrsRecord<string, AttrSpec>undefinedAllowed attributes with defaults
toHTMLString(mark, content, ctx?) => stringundefinedSerializes as an HTML wrapper string
toHTMLStyle(mark) => string | nullundefinedReturns CSS declarations. When defined, the serializer merges all style marks into a single <span style="...">
parseHTMLreadonly ParseRule[]undefinedRules for matching HTML elements during parsing
sanitizeSanitizeConfigundefinedTags and attributes needed through DOMPurify sanitization

Defines how an inline node type (atomic, non-text inline element) renders and serializes. Registered via PluginContext.registerInlineNodeSpec().

interface InlineNodeSpec<T extends string = string> {
readonly type: T;
toDOM(node: InlineNode): HTMLElement;
readonly attrs?: Readonly<Record<string, AttrSpec>>;
readonly group?: string;
readonly toHTMLString?: (node: InlineNode) => string;
readonly parseHTML?: readonly ParseRule[];
readonly sanitize?: SanitizeConfig;
}
PropertyTypeDefaultDescription
typeTrequiredUnique type name (e.g. 'emoji', 'mention')
toDOM(node) => HTMLElementrequiredRenders the inline node. Should set contentEditable="false"
attrsRecord<string, AttrSpec>undefinedAllowed attributes with defaults
groupstring'inline'Group membership for content rules
toHTMLString(node) => stringundefinedSerializes to an HTML string
parseHTMLreadonly ParseRule[]undefinedRules for matching HTML elements during parsing
sanitizeSanitizeConfigundefinedTags and attributes needed through DOMPurify sanitization

Describes a single attribute with an optional default value:

interface AttrSpec {
readonly default?: string | number | boolean;
}

Describes which children a node type can contain:

interface ContentRule {
readonly allow: readonly string[];
readonly min?: number;
readonly max?: number;
}
  • allow — child types or group names that are permitted
  • min / max — optional count constraints

Groups consecutive blocks into a shared wrapper element (e.g. <ul> around <li> items):

interface WrapperSpec {
readonly tag: string;
readonly key: string;
readonly className?: string;
readonly attrs?: Readonly<Record<string, string>>;
}
PropertyDescription
tagHTML tag for the wrapper (e.g. 'ul', 'ol')
keyGrouping key — consecutive blocks with the same key share a wrapper
classNameOptional CSS class
attrsOptional HTML attributes

Passed to toHTML() during serialization, providing mode-aware helpers:

interface HTMLExportContext {
readonly styleAttr: (declarations: string) => string;
}

The styleAttr function returns an attribute fragment depending on the CSS mode:

  • Inline mode: ' style="color: red"'
  • Class mode: ' class="notectl-s-a3f2k9"'
  • Empty input: ''

Describes how an HTML element maps to a document node or mark during parsing:

interface ParseRule {
readonly tag: string;
readonly getAttrs?: (el: HTMLElement) => Record<string, unknown> | false;
readonly priority?: number;
}
  • tag — HTML tag name to match (e.g. 'STRONG', 'H1')
  • getAttrs — extracts attributes from the element. Return false to skip this rule
  • priority — higher values are matched first. Default: 50

Declares tags and attributes that a spec needs to survive DOMPurify sanitization:

interface SanitizeConfig {
readonly tags?: readonly string[];
readonly attrs?: readonly string[];
}

Two pure functions for validating document structure:

import { canContain, validateContent } from '@notectl/core';

canContain(registry, parentType, childType)

Section titled “canContain(registry, parentType, childType)”

Checks if a parent node type can contain a given child node type, using content rules and the group system:

const allowed: boolean = canContain(registry, 'table_row', 'table_cell');

validateContent(registry, parentType, childTypes)

Section titled “validateContent(registry, parentType, childTypes)”

Validates whether the given children types satisfy a parent’s content rules (allow list, min/max constraints):

const valid: boolean = validateContent(registry, 'table_row', ['table_cell', 'table_cell']);

The registerBuiltinSpecs function registers the built-in paragraph spec on a SchemaRegistry:

import { registerBuiltinSpecs, SchemaRegistry } from '@notectl/core';
const registry = new SchemaRegistry();
registerBuiltinSpecs(registry);

This is called automatically by the editor during initialization.


Creates the default schema with paragraph nodes and bold/italic/underline marks:

import { defaultSchema } from '@notectl/core';
const schema = defaultSchema();
// { nodeTypes: ['paragraph'], markTypes: ['bold', 'italic', 'underline'] }

Derives a Schema from a SchemaRegistry’s registered specs:

import { schemaFromRegistry } from '@notectl/core';
const schema = schemaFromRegistry(registry);

The Schema interface includes an optional getNodeSpec function:

interface Schema {
readonly nodeTypes: readonly string[];
readonly markTypes: readonly string[];
readonly getNodeSpec?: (type: string) => NodeSpec | undefined;
}

Checks if a node type is allowed by the schema:

import { isNodeTypeAllowed } from '@notectl/core';
isNodeTypeAllowed(schema, 'heading'); // true

Checks whether a mark type is allowed by the schema:

import { isMarkAllowed } from '@notectl/core';
isMarkAllowed(schema, 'bold'); // true

Creates an HTMLElement with the required data-block-id attribute. Use this in NodeSpec.toDOM() implementations:

import { createBlockElement, blockId } from '@notectl/core';
const el = createBlockElement('div', blockId('b1'));
// <div data-block-id="b1"></div>