@rtif-sdk/formats
v3.2.0
Published
RTIF format codecs: HTML, Markdown, plaintext.
Readme
@rtif-sdk/formats
Format codecs for RTIF documents. Three subpath exports, each returning a
FormatCodec (from @rtif-sdk/core): serialize(doc) → string and
parse(input) → Document, where parse never throws and always returns a
valid normalized document.
npm install @rtif-sdk/formats @rtif-sdk/coreimport { createHtmlCodec } from '@rtif-sdk/formats/html';
import { createMarkdownCodec } from '@rtif-sdk/formats/markdown';
import { createPlaintextCodec } from '@rtif-sdk/formats/plaintext';There is no root export — import the codec you need.
HTML
const html = createHtmlCodec();
html.serialize(doc); // '<p>Hello <strong>world</strong></p>'
html.parse(clipboard); // DocumentDOM-free and SSR-safe. Parsing uses a hand-rolled tokenizer with zero DOM dependency — the codec behaves identically in Node, workers, and browsers, so you can parse untrusted HTML server-side.
Sanitization guarantees (applied during parse):
- Protocol allowlist —
http:,https:,mailto:, and relative URLs — on every URL-bearing attribute:hrefandsrc. Anything else (javascript:,data:,vbscript:, …) is dropped. <script>,<style>,<iframe>,<object>,<embed>subtrees stripped.- All
on*event-handler attributes removed.
Default rules cover the canonical types: p/div, h1–h6, blockquote,
pre > code (with language-* class), li (one list block per item),
hr, and the marks strong/b, em/i, u, s/strike/del, code,
a[href].
Extend per name with HtmlRules — this is how custom features join the
clipboard story:
const html = createHtmlCodec({
rules: {
marks: {
highlight: { tag: 'mark', parse: { tags: ['mark'] } },
},
blocks: {
callout: {
serialize: (block, inner) => `<aside class="callout">${inner}</aside>`,
parse: { tags: ['aside'] },
},
},
},
});HtmlMarkRule: { tag, attrs?(value), parse?: { tags, getValue?(attrs) } }.
HtmlBlockRule: { serialize(block, inner), parse?: { tags, getAttrs?(attrs, tag) } }.
User rules merge over the defaults per name. Structural tags (ul/ol/li,
blockquote, pre, br, hr) are handled by the parser itself and cannot be
remapped. Pass the codec to the editor via createEditor({ codecs: { html } }).
Markdown
const md = createMarkdownCodec();
md.serialize(doc); // '# Title\n\nHello **world**'
md.parse(source); // DocumentA pragmatic CommonMark subset matching the canonical types:
| RTIF | Markdown |
|---|---|
| heading (level 1–6) | #–###### |
| blockquote | > (consecutive > lines merge into one block) |
| code_block | fenced ``` (fence sized past inner backticks; attrs.language on the fence) |
| list bulleted / ordered | - / 1. 2. … (numbered per consecutive group) |
| horizontal_rule | --- |
| bold / italic / strikethrough / code | ** / * / ~~ / backticks |
| link | [text](href) — destinations pass the same protocol allowlist |
Known limits: underline has no markdown form and serializes as plain text; block ids are not preserved across a round trip; single-level only (no nested lists or quotes); no reference links, titles, or autolinks. Unparseable constructs degrade to literal text.
Plaintext
const txt = createPlaintextCodec();
txt.serialize(doc); // blocks joined by '\n'; marks and structure discarded
txt.parse('a\nb'); // one paragraph per line; '' → one empty paragraphHandles LF, CRLF, and lone CR on parse. That's the whole format.
Notes
- Parsers generate fresh block ids (
b1,b2, … per call). UseassertDocEqualfrom@rtif-sdk/test-kit(ids ignored by default) when testing round trips. - A custom codec is just an object satisfying
FormatCodec— nothing here is special-cased by the editor.
