@rtif-sdk/core
v3.3.0
Published
RTIF document model: operations, apply/invert, normalization, queries.
Readme
@rtif-sdk/core
The RTIF document model: types, operations, apply/invert, normalization,
offset math, queries, and validation. Pure functions, zero dependencies, runs
identically in browsers, Node, and workers.
npm install @rtif-sdk/coreThe model
interface Span { text: string; marks?: Marks } // marks absent when unmarked
interface Block { id: string; type: string; attrs?: Attrs; spans: readonly Span[] }
interface Document { blocks: readonly Block[] }
interface Selection { anchor: number; focus: number } // anchor > focus = backwardEverything is JSON-serializable and immutable (readonly throughout).
Offsets are absolute integers over the document text, where each block
boundary counts as one virtual \n:
blocks: [ "Hello" , "World" ]
offsets: 0..5 6..11 // 5 = end of block 0, 6 = start of block 1
docLength(doc) === 11 // 5 + 1 (virtual \n) + 5Operations
Ten types. at/from/to are absolute offsets; index is a block index.
| Op | Shape | Notes |
|---|---|---|
| insert_text | { at, text, marks? } | text must not contain \n |
| delete_text | { at, count } | range must stay inside one block |
| format | { at, count, marks } | null value removes a mark; may span blocks |
| split_block | { at, id } | id for the new block is required |
| merge_blocks | { at } | at = the boundary offset (the virtual \n) |
| insert_block | { index, block } | |
| remove_block | { index } | |
| set_block | { index, props: { type?, attrs? } } | attrs: null clears attrs |
| move_block | { from, to } | to = index in the doc without the moved block |
| replace_range | { from, to, spans? \| blocks? } | one atomic cross-block edit |
replace_range is the keystone: multi-block delete, paste, and autocomplete are
each one op with one correct inverse — compound commands never hand-stitch
split/merge/delete sequences.
apply / invert
import { apply, applyAll, invert } from '@rtif-sdk/core';
const next = apply(doc, op); // pure; throws RtifError on invalid ops
const inverse = invert(doc, op); // doc is the PRE-apply document
// The round-trip guarantee (property-tested for every op type):
apply(apply(doc, op), invert(doc, op)) // deep-equals docapplyAll(doc, ops) folds a sequence. apply normalizes only the blocks it
touched and never returns the same reference.
Errors are always RtifError with code: 'OUT_OF_RANGE' | 'INVALID_OP' |
'INVALID_DOC' | 'UNKNOWN_TYPE' and a message naming the offending block id,
offset, or index.
Queries
docLength(doc) // total length incl. virtual \n separators
blockTextLength(block)
blockStartOffset(doc, blockIndex)
resolve(doc, offset) // → { blockIndex, block, localOffset }
sliceText(doc, from, to) // plain text, \n at block boundaries
sliceRange(doc, from, to) // → Block[] (the document slice)
blocksInRange(doc, from, to) // → blocks intersecting the range
marksAt(doc, offset) // marks newly typed text would inherit
getBlock(doc, index); getBlockById(doc, id)Selection helpers: cursorAt(offset), isCollapsed(sel),
selectionRange(sel) (→ ordered { from, to }).
Position mapping (used by engine history and the web selection):
mapOffset(offset, op, doc, bias?) // bias: 'left' | 'right'
mapSelection(sel, op, doc)Normalization & validation
Six invariants hold after every apply: every block has ≥ 1 span; no empty-text
span beside others; no adjacent equal-mark spans; marks/attrs absent rather
than {}; unique block ids; ≥ 1 block. Helpers: normalizeBlock(block),
isBlockNormalized(block), marksEqual(a, b), attrsEqual(a, b).
validateDoc(doc); // throws RtifError INVALID_DOC naming the invariant
validateDoc(doc, schema); // additionally throws UNKNOWN_TYPE for unknown
// block types / mark namesSchema types & FormatCodec
Schema types live here (no DOM): MarkSpec (excludes?, exclusive?),
BlockSpec (atomic?, defaultAttrs?), Schema. Renderers extend them in
@rtif-sdk/web.
interface FormatCodec {
format: string; // 'html' | 'markdown' | 'plaintext' | custom
serialize(doc: Document): string;
parse(input: string): Document; // never throws; best-effort on messy input
}@rtif-sdk/formats ships implementations; anything satisfying this interface plugs
into the web editor's clipboard via createEditor({ codecs }).
