npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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/core

The 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 = backward

Everything 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) + 5

Operations

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 doc

applyAll(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 names

Schema 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 }).