Engine reference
quoin-editor/engine is the editing engine on its own, with no React and no DOM. Every name it exports is listed here with the signature the published types carry, read from the package’s own source each time the site is built.
Nothing is held back. An entry of the package is a contract, so a name that should not be public should stop being exported rather than be left off this page. Exports says in prose what each entry of the package is for, and Headless reference lists what quoin-editor/headless adds on top of this one.
The document
Blocks, the inline nodes inside them and the marks on those nodes: the shapes every other part reads and writes.
atom
function atom(type: string, attrs: Attrs, marks?: readonly Mark[]): AtomNodeAtomNode
type AtomNode = {
readonly kind: 'atom';
readonly type: string;
readonly attrs: Attrs;
readonly marks: readonly Mark[];
};Attrs
type Attrs = Readonly<Record<string, string>>;BlockAttrs
type BlockAttrs = Readonly<Record<string, unknown>>;BlockRangeSelection
type BlockRangeSelection = { readonly kind: 'blocks'; readonly anchor: string; readonly head: string };EngineBlock
type EngineBlock = {
readonly id: string;
readonly type: string;
readonly indent: number;
readonly attrs: BlockAttrs;
readonly regions: Readonly<Record<string, Inline>>;
};EngineDoc
type EngineDoc = {
readonly blocks: readonly EngineBlock[];
readonly index: ReadonlyMap<string, number>;
};Inline
type Inline = readonly InlineNode[];InlineNode
type InlineNode = TextNode | AtomNode;Mark
type Mark = { readonly type: string; readonly attrs?: Attrs };NodeSelection
type NodeSelection = { readonly kind: 'node'; readonly block: string };normalizeInline
function normalizeInline(nodes: readonly InlineNode[]): InlinePos
type Pos = { readonly block: string; readonly region: string; readonly offset: number };QuoinBlock
A document is an ordered array of these; type is looked up in the registered block definitions.
type QuoinBlock = {
id: string;
type: string;
data: Record<string, unknown>;
/** Nesting depth shared by every block type, such as list nesting and toggle children; 0 when absent. */
indent?: number;
};Selection
type Selection = TextSelection | NodeSelection | BlockRangeSelection;text
function text(value: string, marks?: readonly Mark[]): TextNodeTextNode
type TextNode = { readonly kind: 'text'; readonly text: string; readonly marks: readonly Mark[] };TextSelection
assoc: -1 draws a caret at a soft wrap at the end of the line before it.
type TextSelection = { readonly kind: 'text'; readonly anchor: Pos; readonly head: Pos; readonly assoc?: Assoc };The schema
What types of block, mark and atom a document may hold, and how each is read from and written back to markdown.
AtomSpec
type AtomSpec = {
readonly type: string;
plainText(attrs: Attrs): string;
};BlockContent
type BlockContent = { attrs: BlockAttrs; regions: Record<string, Inline> };BlockFlow
type BlockFlow = 'text' | 'void' | 'structured';BlockSpec
type BlockSpec = {
readonly type: string;
readonly flow: BlockFlow;
readonly primaryRegion?: string;
readonly enter?: 'split' | 'hardBreak';
fromData(data: Record<string, unknown>, ctx: CodecContext): BlockContent;
toData(block: EngineBlock, ctx: CodecContext): Record<string, unknown>;
initial(): BlockContent;
splitAttrs?(attrs: BlockAttrs): { type: string; attrs: BlockAttrs };
toMarkdown?(block: EngineBlock, ctx: MarkdownExportContext): string;
toPlainText?(block: EngineBlock): string;
};CodecContext
type CodecContext = {
readonly inline: InlineCodec;
newId(): string;
};createSchema
function createSchema(input?: SchemaInput): SchemaInlineCodec
type InlineCodec = {
parse(markdown: string, options?: ParseInlineOptions): Inline;
serialize(inline: Inline, options?: SerializeInlineOptions): string;
};MarkdownExportContext
type MarkdownExportContext = CodecContext & {
resolveAssetUrl(storageKey: string): string;
comments: boolean;
};MarkdownExportContext on GitHub
MarkSpec
type MarkSpec = {
readonly type: string;
readonly inclusive: boolean;
};ParseInlineOptions
type ParseInlineOptions = {
softBreak?: 'newline' | 'space';
refs?: ReadonlyMap<string, { href: string; title?: string }>;
};Schema
type Schema = {
readonly blocks: ReadonlyMap<string, BlockSpec>;
readonly marks: ReadonlyMap<string, MarkSpec>;
readonly atoms: ReadonlyMap<string, AtomSpec>;
readonly defaultTextBlock: string;
};SchemaInput
type SchemaInput = {
blocks?: BlockSpec[];
marks?: MarkSpec[];
atoms?: AtomSpec[];
defaultTextBlock?: string;
};SerializeInlineOptions
type SerializeInlineOptions = {
escapeLineStart?: boolean;
escapePipes?: boolean;
hardBreak?: 'backslash' | 'br';
};SerializeInlineOptions on GitHub
Blocks and the codec
The block types every document starts with, and the translation between stored blocks and the tree the engine edits.
Align
type Align = 'left' | 'center' | 'right' | 'justify';BlockCodec
type BlockCodec = {
/** `taken` holds the ids of the other blocks of the document, which a split piece never takes. */
toEngineBlocks(block: QuoinBlock, taken?: ReadonlySet<string>): EngineBlock[];
toQuoin(block: EngineBlock): QuoinBlock;
docFromBlocks(blocks: readonly QuoinBlock[]): EngineDoc;
blocksFromDoc(doc: EngineDoc): QuoinBlock[];
context: CodecContext;
};coreBlockSpecs
const coreBlockSpecs: BlockSpec[]createBlockCodec
function createBlockCodec(schema: Schema, options?: { newId?: () => string }): BlockCodecSteps
The smallest changes a document takes. Commands are built from them, history inverts them, and a collaboration adapter sends them over the wire.
addMarkStep
function addMarkStep(block: string, region: string, from: number, to: number, mark: Mark): StepAssoc
type Assoc = -1 | 1;AttrsPatch
type AttrsPatch = Readonly<Record<string, unknown>>;CreatedBlock
type CreatedBlock = {
id: string;
type: string;
attrs: BlockAttrs;
indent: number;
region: string;
regions?: Record<string, Inline>;
};insertBlocksStep
function insertBlocksStep(index: number, blocks: readonly EngineBlock[]): StepjoinBlocksStep
function joinBlocksStep(
target: string,
targetRegion: string,
source: string,
sourceRegion: string,
): StepmoveBlocksStep
function moveBlocksStep(from: number, count: number, to: number): StepPosMap
type PosMap = (pos: Pos, assoc?: Assoc) => Pos;removeBlocksStep
function removeBlocksStep(index: number, count: number): StepremoveMarkStep
function removeMarkStep(
block: string,
region: string,
from: number,
to: number,
mark: Mark | string,
): StepreplaceBlockStep
function replaceBlockStep(block: EngineBlock): StepreplaceDocStep
function replaceDocStep(next: EngineDoc): StepreplaceInlineStep
function replaceInlineStep(
block: string,
region: string,
from: number,
to: number,
slice: Inline,
): StepsetAttrsStep
function setAttrsStep(block: string, patch: AttrsPatch): StepsetIndentStep
function setIndentStep(block: string, indent: number): StepsplitBlockStep
function splitBlockStep(
block: string,
region: string,
offset: number,
created: CreatedBlock,
): StepStep
interface Step {
readonly stepType: string;
apply(doc: EngineDoc): StepResult;
invert(before: EngineDoc): Step;
toJSON(): StepJSON;
}stepFromJSON
function stepFromJSON(json: StepJSON): StepStepJSON
type StepJSON = { readonly stepType: string } & Readonly<Record<string, unknown>>;StepResult
type StepResult = { doc: EngineDoc; map: PosMap };State, transactions and plugins
The state a view draws, the transaction that carries steps and a selection into the next state, and the plugins that watch both.
applyTransaction
function applyTransaction(state: EditorState, tr: Transaction): EditorStateblockRangeSelection
function blockRangeSelection(anchor: string, head?: string): BlockRangeSelectioncreatePluginKey
function createPluginKey<T>(name: string): PluginKey<T>createState
function createState(config: { doc: EngineDoc; schema: Schema; selection?: Selection | null; plugins?: readonly PluginSpec<unknown>[]; clock?: () => number; }): EditorStateEditorState
type EditorState = {
readonly doc: EngineDoc;
readonly selection: Selection | null;
readonly storedMarks: readonly Mark[] | null;
readonly schema: Schema;
readonly plugins: readonly PluginSpec<unknown>[];
readonly pluginState: ReadonlyMap<string, unknown>;
readonly clock: () => number;
};getPluginState
function getPluginState<T>(state: EditorState, key: PluginKey<T>): TnodeSelection
function nodeSelection(block: string): NodeSelectionPluginKey
type PluginKey<T> = { readonly name: string; readonly __value?: T };PluginSpec
type PluginSpec<T> = {
readonly key: PluginKey<T>;
init(state: EditorState): T;
apply(tr: Transaction, value: T, oldState: EditorState, newState: EditorState): T;
};redo
const redo: CommandtextSelection
function textSelection(anchor: Pos, head?: Pos): TextSelectionTransaction
class Transaction {
readonly before: EditorState;
readonly time: number;
constructor(state: EditorState, time?: number);
get doc(): EngineDoc;
get docs(): readonly EngineDoc[];
get steps(): readonly Step[];
get maps(): readonly PosMap[];
get docChanged(): boolean;
get selection(): Selection | null;
get selectionSet(): boolean;
get storedMarks(): readonly Mark[] | null;
get storedMarksSet(): boolean;
get meta(): ReadonlyMap<string, unknown>;
get origin(): TransactionOrigin;
get addToHistory(): boolean;
step(step: Step): this;
maybeStep(step: Step): boolean;
setSelection(selection: Selection | null): this;
setStoredMarks(marks: readonly Mark[] | null): this;
setOrigin(origin: TransactionOrigin): this;
setMeta(key: string, value: unknown): this;
getMeta<T>(key: string): T | undefined;
mapPos(pos: Pos, assoc?: Assoc, fromStep?: number): Pos;
}TransactionOrigin
type TransactionOrigin = 'input' | 'command' | 'paste' | 'drop' | 'remote' | 'reconcile' | 'ai' | 'history';undo
const undo: CommandCommands
A command reads the state and either reports that it would apply or dispatches a transaction that applies it.
BlockStructureOptions
type BlockStructureOptions = { newId?: () => string };BlockStructureOptions on GitHub
BoundCommands
type BoundCommands<F extends CommandFactories> = { [K in keyof F]: (...args: Parameters<F[K]>) => boolean };Chain
type Chain<F extends CommandFactories> = {
[K in keyof F]: (...args: Parameters<F[K]>) => Chain<F>;
} & { run(): boolean };Command
type Command = (state: EditorState, dispatch?: (tr: Transaction) => void) => boolean;CommandFactories
type CommandFactories = Readonly<Record<string, (...args: never[]) => Command>>;insertBlockAfter
function insertBlockAfter(
type: string,
attrs?: BlockAttrs,
options?: BlockStructureOptions,
): CommandinsertText
Inserts value at the selection, in place of any selected text or blocks, as a command an extension can return. It is an undo step of its own unless options.origin is input, which groups it with the typing around it.
function insertText(value: string, options?: InsertTextOptions): CommandInsertTextOptions
type InsertTextOptions = { origin?: TransactionOrigin; newId?: () => string };setBlockType
function setBlockType(type: string, attrs?: BlockAttrs): CommandtoggleMark
function toggleMark(type: string, attrs?: Attrs): CommandInput rules
What typing turns into as it is typed, such as a heading from a hash and a space, or a dash pair into a dash.
blockInputRules
const blockInputRules: readonly InputRule[]InputRule
type InputRule = {
readonly name: string;
readonly match: RegExp;
readonly blockTypes?: readonly string[];
readonly kinds?: readonly string[];
handler(
state: EditorState,
match: RegExpExecArray,
range: InputRuleRange,
context?: InputRuleContext,
): Transaction | null;
};InputRuleContext
type InputRuleContext = { readonly newId: () => string };InputRuleRange
type InputRuleRange = { block: string; region: string; from: number; to: number };insertTextWithRules
function insertTextWithRules(
value: string,
rules: readonly InputRule[],
options?: { newId?: () => string },
): CommandmarkInputRules
const markInputRules: readonly InputRule[]typographyRules
function typographyRules(options?: { dashes?: 'em' | 'en' | 'none' }): readonly InputRule[]The clipboard
HTML a reader pastes, read into blocks, and the slice of a document a copy carries.
DOMParserLike
type DOMParserLike = { parseFromString(html: string, type: 'text/html'): { readonly body: object } };fromHTML
function fromHTML(html: string, options?: FromHTMLOptions): QuoinBlock[]FromHTMLOptions
type FromHTMLOptions = { parser?: DOMParserLike; newId?: () => string };Slice
type Slice = {
readonly blocks: readonly QuoinBlock[];
readonly openStart: boolean;
readonly openEnd: boolean;
/** Read from plain text: its first line takes the type of an empty block it is pasted into. */
readonly plainText?: boolean;
};Markdown in and out
The parser and the serialiser that make markdown the storage format, for whole documents and for inline runs.
fromMarkdown
A markdown file read back into blocks, one text block per line, ready to pass to the canvas as blocks.
function fromMarkdown(markdown: string, options?: FromMarkdownOptions): QuoinBlock[]FromMarkdownOptions
type FromMarkdownOptions = { softBreak?: 'newline' | 'space'; newId?: () => string };PALETTE
The colour names a writer can pick for text and for a background, each a --quoin-color-* token.
const PALETTE: readonly ["default", "gray", "brown", "orange", "yellow", "green", "blue", "purple", "pink", "red"]parseInline
function parseInline(markdown: string, options?: ParseInlineOptions): InlinesanitizeHref
function sanitizeHref(href: string): string | nullserializeInline
function serializeInline(inline: Inline, options?: SerializeInlineOptions): stringtoMarkdown
A whole document as one markdown file, each block spelled as the lines it is stored as.
function toMarkdown(blocks: readonly QuoinBlock[], options?: ToMarkdownOptions): stringToMarkdownOptions
type ToMarkdownOptions = {
schema?: Schema;
resolveAssetUrl?: (storageKey: string) => string;
comments?: boolean;
/**
* Write every block that markdown cannot hold in full as a quoin comment, so that fromMarkdown reads back all of its
* data. Plain export, the default, writes native markdown and drops what that form cannot carry: block-level align,
* colour, background and other extra keys, the indent no container explains, and the data of the other block types.
*/
lossless?: boolean;
};Rendering a document out
A document as HTML, as plain text or as counts, with no editor and no DOM.
countDoc
Counts a document from per-block counts kept for each block object, so a keystroke recounts one block.
function countDoc(doc: EngineDoc, options?: { schema?: Schema }): TextStatscountText
function countText(blocks: readonly QuoinBlock[], options?: { schema?: Schema }): TextStatsEmbedProvider
type EmbedProvider = {
readonly name: string;
/** Returns the https address of the frame for a page this provider embeds, or null for any other page. */
match(url: EmbedUrl): string | null;
};EmbedUrl
The parts of a URL that a provider reads. A WHATWG URL fits, and the declarations need no DOM types.
type EmbedUrl = {
readonly href: string;
readonly protocol: string;
readonly hostname: string;
readonly pathname: string;
readonly search: string;
readonly hash: string;
readonly searchParams: { get(name: string): string | null };
};HTMLOptions
type HTMLOptions = {
schema?: Schema;
/** The address of a stored picture or file. One that is no http, https, blob, data or relative address is left out. */
resolveAssetUrl?: (storageKey: string) => string;
renderMath?: (latex: string, options: { displayMode: boolean }) => string;
embeds?: 'iframe' | 'link';
/** Tried before the built in providers, in order. Only used when `embeds` is `'iframe'`. */
embedProviders?: readonly EmbedProvider[];
/** The title of a frame given its provider's name, the name itself by default. */
frameTitle?: (provider: string) => string;
/** The accessible name of a task checkbox whose item has no text, `'Task'` by default. */
taskLabel?: string;
};TextStats
type TextStats = { characters: number; words: number };toHTML
A whole document as HTML, for a page, an email or a feed. It loads no React and needs no DOM.
function toHTML(blocks: readonly QuoinBlock[], options?: HTMLOptions): stringtoPlainText
A whole document as plain text, its markup dropped, for a search index, a summary or a word count.
function toPlainText(blocks: readonly QuoinBlock[], options?: { schema?: Schema }): stringDifferences between documents
What changed between two versions, block by block and within a block, for a diff view or a review.
BlockChange
type BlockChange = {
op: 'change';
before: QuoinBlock;
after: QuoinBlock;
regions: Record<string, InlineChange[]>;
attrsChanged: boolean;
};BlockDiff
A moved block is deleted where it was and inserted where it went, its insertion holding its change if edited.
type BlockDiff =
| { op: 'equal'; block: QuoinBlock }
| { op: 'insert'; block: QuoinBlock; moved?: boolean; change?: BlockChange }
| { op: 'delete'; block: QuoinBlock; moved?: boolean }
| BlockChange;diffBlocks
A 0.4 block that holds several nodes is diffed as the pieces the editor splits it into, so an edit shows in one piece only, and the diff then carries those pieces. Blocks that are the same on both sides are never split.
function diffBlocks(
a: readonly QuoinBlock[],
b: readonly QuoinBlock[],
options?: { schema?: Schema },
): BlockDiff[]diffInline
function diffInline(a: Inline, b: Inline): InlineChange[]InlineChange
type InlineChange = { op: 'equal' | 'insert' | 'delete'; inline: Inline };Extensions and the editor
An extension adds commands, keys, rules and plugins; the editor is the set of them, resolved and bound to a state.
CoreCommands
type CoreCommands = typeof coreCommands;CoreExtensionOptions
type CoreExtensionOptions = {
typography?: { dashes?: 'em' | 'en' | 'none' } | false;
newId?: () => string;
};CoreExtensionOptions on GitHub
coreExtensions
function coreExtensions(options?: CoreExtensionOptions): Extension[]createEditor
The editing engine on its own, with no React and no DOM, for a host that drives it itself or runs it on a server.
function createEditor<F extends CommandFactories = CoreCommands>(options: EditorOptions): Editor<F>Decoration
type Decoration =
| {
readonly kind: 'inline';
readonly block: string;
readonly region: string;
readonly from: number;
readonly to: number;
readonly attrs: Attrs;
}
| {
readonly kind: 'widget';
readonly block: string;
readonly region: string;
readonly offset: number;
readonly key: string;
readonly widget: string;
readonly attrs: Attrs;
readonly side?: -1 | 1;
}
/** Attributes written on the element of a whole block: `data-*` and `aria-*` names only. */
| { readonly kind: 'block'; readonly block: string; readonly attrs: Attrs };defineExtension
function defineExtension<E extends Extension>(extension: E): EEditor
type Editor<F extends CommandFactories = CoreCommands> = {
readonly state: EditorState;
readonly schema: Schema;
dispatch(tr: Transaction): void;
createTransaction(): Transaction;
subscribe(listener: (state: EditorState, tr: Transaction | null) => void): () => void;
readonly commands: BoundCommands<F>;
chain(): Chain<F>;
can(): BoundCommands<F>;
isActive(name: string, attrs?: Record<string, unknown>): boolean;
insertText(text: string): boolean;
handleKey(event: KeyLike): boolean;
paste(data: { json?: string; html?: string; text?: string }, parser?: DOMParserLike): boolean;
copy(): { json: string; html: string; text: string } | null;
getBlocks(): QuoinBlock[];
setBlocks(blocks: readonly QuoinBlock[]): boolean;
/** Runs the extensions' `onMount`, as a view does when it attaches; returns the means to end that mount. */
mount(): () => void;
decorations(): readonly Decoration[];
toMarkdown(options?: MarkdownOptions): string;
stats(): TextStats;
destroy(): void;
};EditorOptions
type EditorOptions = {
blocks: readonly QuoinBlock[];
extensions?: readonly Extension[];
onChange?: (blocks: QuoinBlock[]) => void;
onTransaction?: (tr: Transaction, state: EditorState) => void;
newId?: () => string;
now?: () => number;
isMac?: boolean;
/** Turns a storage key into the url written for IMAGE and FILE blocks in copies and, by default, in toMarkdown. */
resolveAssetUrl?: (key: string) => string;
};Extension
type Extension = {
readonly name: string;
readonly priority?: number;
readonly blocks?: readonly BlockSpec[];
readonly marks?: readonly MarkSpec[];
readonly atoms?: readonly AtomSpec[];
readonly inputRules?: readonly InputRule[];
readonly pasteRules?: readonly PasteRule[];
readonly keymap?: Readonly<Record<string, Command>>;
readonly commands?: CommandFactories;
readonly plugins?: readonly PluginSpec<unknown>[];
readonly decorations?: (state: EditorState) => readonly Decoration[];
readonly onTransaction?: (tr: Transaction, state: EditorState) => void;
/**
* Refuses a transaction before it applies by returning false: the editor drops it whole. Undo and redo, the undo of an
* input rule, the blocks the host supplies and remote transactions are never filtered.
*/
readonly filterTransaction?: (tr: Transaction, state: EditorState) => boolean;
/** Returns false to refuse blocks the host supplies that differ from the document, which then stays as it is. */
readonly filterHostBlocks?: (blocks: readonly QuoinBlock[], state: EditorState) => boolean;
/** Runs on each `editor.mount()`; what it returns runs when that mount ends or the editor is destroyed. */
readonly onMount?: (editor: Editor<CommandFactories>) => (() => void) | void;
readonly ui?: unknown;
};KeyLike
type KeyLike = {
key: string;
code?: string;
altKey: boolean;
ctrlKey: boolean;
metaKey: boolean;
shiftKey: boolean;
};keyName
function keyName(event: KeyLike, isMac: boolean): stringMarkdownOptions
type MarkdownOptions = {
comments?: boolean;
/** Write what markdown cannot hold as quoin comments. Plain export drops block-level extras and other data. */
lossless?: boolean;
resolveAssetUrl?: (key: string) => string;
};normalizeKey
function normalizeKey(name: string, isMac: boolean): stringPasteRule
type PasteRule = { readonly name: string; match(text: string, state: EditorState): Command | null };