Version: 0.6-dev
Date: March 2026
Status: Core engine complete, 79/79 tests passing
RichRender is a compact binary format for rich text, with a modular JavaScript engine that handles encoding, inflation (rendering to HTML), interactive editing, and format conversion.
Rich text on the web relies on HTML — a verbose, parser-heavy format designed for documents, not data. A bold red word costs 50+ bytes in HTML. Email clients, chat systems, IoT devices, and editors all pay this tax. TinyMCE (the dominant open-source rich text editor) ships 500KB+ minified just to manipulate it.
RichRender encodes the same bold red word in 5 bytes: one control character, two style-pip bytes, and the text. The format uses bitwise flags — bold is a single bit, color is a 4-bit index, font size is a 3-bit index. No parsing, no tree walking, no configuration files.
The engine is six JavaScript files totaling 20KB minified (6KB gzipped). It provides:
| Bundle | Minified | Gzipped |
|---|---|---|
| Read-only viewer | 9.7KB | 3.7KB |
| Interactive editor engine | 15.9KB | 5.4KB |
| Full stack with MDR | 20KB | 5.9KB |
For comparison: TinyMCE is 500KB+ minified. CKEditor is 300KB+.
RichRender is one format at four depths. Each tier is a superset of the one below:
| Tier | What It Handles | Use Case |
|---|---|---|
| Bit-Wise | Style pips only (bytes 1–3). Bold, italic, colors, sizes, headings. | AI chat, IoT, email, smart devices |
| Lite-Wire | Adds block/inline elements: paragraphs, lists, links, blockquotes. | TinyMCE-competitive editors |
| Wire Format | Full element tables, meta regions, preamble, custom palettes. | Complete document authoring |
| Editor | Per-character pip arrays for cursor-addressable editing. | Interactive editors |
A Bit-Wise stream is valid Lite-Wire. A Lite-Wire stream is valid Wire Format. One inflator reads all tiers.
richrender-core.js Constants, element tables, stream builder, disassembler
rr-inflate.js Universal inflator (any tier → Render-HTML)
rr-editor.js Editor format: EditorDoc, painting API
rr-convert.js Format converters (wire ↔ editor ↔ HTML)
mdr-to-rich.js MDR Markdown → RichRender encoder
rich-to-html.js Bit-Wise-only inflator (minimal/legacy)Cherry-pick by use case:
core + rr-inflatecore + rr-inflate + rr-editor + rr-convertmdr-to-richRRThe shared foundation. All other modules depend on this.
| Constant | Value | Meaning |
|---|---|---|
RR.SOH | 0x01 | Block element OPEN |
RR.STX | 0x02 | Style span OPEN |
RR.ETX | 0x03 | Style span CLOSE |
RR.EOT | 0x04 | Block element CLOSE |
RR.SO | 0x0E | Inline element OPEN |
RR.SI | 0x0F | Inline element CLOSE |
RR.META_OPEN | 0xFE | Meta region OPEN |
RR.META_CLOSE | 0xFF | Meta region CLOSE |
RR.META_ATTR | 0x00 | Meta type: attributes |
RR.META_COMMENT | 0x01 | Meta type: comment |
| Constant | Value | Meaning |
|---|---|---|
RR.PIP_NESTED | 0x01 | Nested span (bit 0) |
RR.PIP_BOLD | 0x02 | Bold (bit 1) |
RR.PIP_ITALIC | 0x04 | Italic (bit 2) |
RR.PIP_STRIKETHROUGH | 0x08 | Strikethrough (bit 3) |
RR.PIP_UNDERLINE | 0x10 | Underline (bit 4) |
RR.PIP_VARIANT_MASK | 0x60 | Variant field mask (bits 6–5) |
RR.PIP_SUPER | 0x20 | Superscript (01) |
RR.PIP_SUB | 0x40 | Subscript (10) |
RR.PIP_HIGHLIGHT | 0x60 | Highlight (11) |
RR.PIP_CONTINUATION | 0x80 | Byte two follows (bit 7) |
RR.PIP_STYLE_MASK | 0x7E | All style bits (excluding nested/continuation) |
| Constant | Value | Meaning |
|---|---|---|
RR.P2_CONTINUATION | 0x80 | Byte three follows |
RR.P2_BG_MASK | 0x60 | Background color mask (bits 6–5) |
RR.P2_BG_NONE | 0x00 | No background |
RR.P2_BG_YELLOW | 0x20 | Yellow highlight |
RR.P2_BG_GREEN | 0x40 | Green highlight |
RR.P2_BG_PINK | 0x60 | Pink highlight |
RR.P2_FG_MASK | 0x1E | Foreground color mask (bits 4–1) |
RR.P2_FG_SHIFT | 1 | Shift right to get color index 0–15 |
RR.P2_FONT_BIT | 0x01 | 0=serif, 1=sans-serif |
| Constant | Value | Meaning |
|---|---|---|
RR.P3_CONTINUATION | 0x80 | Byte four follows (preamble-defined) |
RR.P3_SIZE_MASK | 0x70 | Font size mask (bits 6–4) |
RR.P3_SIZE_SHIFT | 4 | Shift right to get size index 0–7 |
RR.P3_DOUBLE_UNDERLINE | 0x04 | Double underline (bit 2) |
RR.P3_OVERLINE | 0x02 | Overline (bit 1) |
RR.P3_SMALLCAPS | 0x01 | Small-caps (bit 0) |
RR.DEFAULT_FG[0..15] // Foreground colors: inherit, black, dark gray, red,
// maroon, orange, green, dark green, blue, navy,
// purple, teal, brown, light gray, white, gold
RR.DEFAULT_BG[0..3] // Background colors: none, yellow, green, pink
RR.DEFAULT_SIZES[0..7] // Font sizes: inherit, 2em, 1.5em, 1.17em, 1em,
// 0.85em, 0.75em, 0.6emRR.BLOCK.p // 15 — paragraph
RR.BLOCK.h1 // 9 — through h6 (14)
RR.BLOCK.blockquote // 17
RR.BLOCK.ul // 18
RR.BLOCK.ol // 19
RR.BLOCK.li // 20
RR.BLOCK.hr // 40
// ... 43 block elements total (see BLOCK object)
RR.INLINE.code // 1
RR.INLINE.a // 15
// ... 26 inline elements total (see INLINE object)
RR.BLOCK_NAMES[idx] // Reverse lookup: index → tag name
RR.INLINE_NAMES[idx] // Reverse lookup: index → tag nameRR.HEADING_PATTERNS // { 1:'h1', 2:'h2', 3:'h3', 4:'h4' }
// Inflator matches: size index + bold flag → heading tagvar s = new RR.RichStream();
s.write(byte) // Write a single byte (number) or Uint8Array
s.writeText(string) // Write UTF-8 encoded text
s.openBlock(idx) // SOH + block index
s.closeBlock() // EOT
s.openInline(idx) // SO + inline index
s.closeInline() // SI
s.openStyle(pip) // STX + pip byte
s.closeStyle(resumePip?) // ETX + optional resuming pip
s.meta(type, content) // META_OPEN + type + text + META_CLOSE
s.toBytes() // → Uint8ArrayRR.describePip(b1, b2?, b3?) // → "bold+italic+fg:3+sans" (human-readable)
RR.hexDump(bytes, perLine?) // → hex dump string
RR.disassemble(bytes) // → annotated disassembly with indentationrrInflate(bytes)Universal inflator. Reads any RichRender byte stream and produces Render-HTML.
var html = rrInflate(bytes); // Uint8Array → HTML stringHandles all control characters: STX/ETX (style spans), SOH/EOT (blocks), SO/SI (inlines), META (attributes, comments).
Heading detection: Size + bold pattern → <h1> through <h4>.
Double underline: Overrides single underline when both set.
Highlight: Byte-one variant → <mark> with default yellow. Byte-two bg overrides color.
Round-trip classes: Every output element carries mdr- prefixed classes:
| Class | Source |
|---|---|
mdr-b, mdr-i, mdr-u, mdr-s | Byte one flags |
mdr-sup, mdr-sub, mdr-hl | Byte one variant |
mdr-fg, mdr-bg, mdr-font | Byte two |
mdr-sz, mdr-sc, mdr-ol, mdr-du | Byte three |
mdr-set-h1 … mdr-set-h4 | Heading presets |
mdr-p, mdr-ul, mdr-li, etc. | Block elements |
mdr-code, mdr-a, etc. | Inline elements |
Meta attributes: Parsed as key=value pairs (newline-separated). Emitted as HTML attributes.
RREditor.EditorDocPer-character pip array for cursor-addressable editing. All style operations are bitwise math.
var doc = RREditor.EditorDoc.fromText('hello world');
var doc = new RREditor.EditorDoc(charArray); // from existing char arrayEach character is { ch: string, p1: number, p2: number, p3: number }.
doc.length() // → number of characters
doc.text(from?, to?) // → plain text string
doc.charAt(pos) // → { ch, p1, p2, p3 } or null
doc.insertAt(pos, text, p1?, p2?, p3?) // insert text with optional style
doc.deleteRange(from, to) // delete characters in range
doc.slice(from, to) // → new EditorDoc (deep copy)doc.applyFlag(from, to, flag) // Set bits: pip |= flag
doc.removeFlag(from, to, flag) // Clear bits: pip &= ~flag
doc.toggleFlag(from, to, flag) // Toggle: if all set → remove, else apply
doc.hasFlag(pos, flag) // → boolean
doc.rangeHasFlag(from, to, flag) // → boolean (all chars in range)Flags: RR.PIP_BOLD, RR.PIP_ITALIC, RR.PIP_UNDERLINE, RR.PIP_STRIKETHROUGH
doc.setVariant(from, to, variant) // RR.PIP_SUPER, RR.PIP_SUB, RR.PIP_HIGHLIGHT
doc.clearVariant(from, to) // Reset to normaldoc.setFgColor(from, to, index) // 0–15 (see DEFAULT_FG)
doc.getFgColor(pos) // → index
doc.setBgColor(from, to, index) // 0–3 (see DEFAULT_BG)
doc.getBgColor(pos) // → index
doc.setFont(from, to, sans) // true=sans-serif, false=serifdoc.setSize(from, to, index) // 0–7 (see DEFAULT_SIZES)
doc.getSize(pos) // → index
doc.applyP3Flag(from, to, flag) // RR.P3_SMALLCAPS, RR.P3_OVERLINE, RR.P3_DOUBLE_UNDERLINE
doc.removeP3Flag(from, to, flag)
doc.toggleP3Flag(from, to, flag)doc.styleAt(pos) // → { bold, italic, underline, strikethrough, variant,
// fgColor, bgColor, sans, size, smallCaps,
// overline, doubleUnder, p1, p2, p3 }All byte-two and byte-three setters automatically maintain continuation bits. Setting a foreground color automatically sets PIP_CONTINUATION on byte one. Setting a font size automatically sets P2_CONTINUATION on byte two. Clearing all higher-byte content clears the continuation chain. The caller never manages continuation bits manually.
RRConvertFormat converters between wire/Lite-Wire/Bit-Wise streams and the editor format.
var doc = RRConvert.wireToEditor(bytes); // Uint8Array → EditorDoc
var bytes = RRConvert.editorToWire(doc); // EditorDoc → Uint8Array
var html = RRConvert.editorToHtml(doc); // EditorDoc → HTML (shortcut)wireToEditor expands a byte stream into per-character pip tuples. Style spans (STX/ETX) set the current pip state; text characters inherit it. Nested spans (PIP_NESTED) merge delta bits into the current state; resuming pips after ETX are consumed. Block markers become newline characters with metadata. Inline markers become zero-width entries with metadata.
editorToWire compresses an editor document into a byte stream. Detects pip transitions and emits optimal span structure:
editorToHtml is a convenience: editorToWire → rrInflate.
mdrToRich(text)MDR (Markdown) to RichRender encoder. Currently handles byte-one style markers:
var bytes = mdrToRich('This is **bold** and *italic*.'); // → Uint8ArraySupported markers:
| Marker | Style |
|---|---|
***text*** | Bold + Italic |
**text** | Bold |
*text* | Italic |
~~text~~ | Strikethrough |
__text__ | Underline |
Linear scanner — walks text left-to-right, toggles pip bits on marker boundaries. Handles nesting (italic inside bold), adjacent markers, and combined markers.
richToHtml(bytes)Bit-Wise-only inflator. Handles STX/ETX style spans with all three pip bytes. Does NOT handle SOH/EOT blocks or SO/SI inlines.
Use rrInflate instead unless you need the smallest possible file for a Bit-Wise-only deployment. This file exists for size-constrained environments where block/inline support is unnecessary.
<meta name="richrender" content="full" data-tier="bitwise|litewire|wire"><div data-mdr="root" data-mdr-tier="bitwise">
<!-- Clean Render-HTML, mdr- classes throughout -->
</div>Three strategies for non-Render-HTML content (implementation guidance, not mandated):
Tab (U+0009) is a normal text byte in the wire format — 0x09 sits outside the control character range. In the editor format it is stored as { ch: '\t', p1, p2, p3 }.
Render-HTML (TODO): The inflator will emit <span class="mdr-tab" style="display:inline-block;width:2em"></span>. Not yet implemented in rrInflate. The mdr-tab class enables CSS customization. Default width is 2em, configurable via future config file.
MDR round-trip: No special syntax. The 0x09 byte survives encoding/decoding as plain text. The text editor is responsible for rendering; RichRender stores it faithfully.
Interactive editors may maintain a document-specific mutable palette separate from the spec defaults. The demo editor (richrender-demo.html) implements this pattern.
spec defaults: [inherit, #000, #555, #CC0000, ...] (immutable, 16 FG / 4 BG)
editor palette: [ —, —, —, #51653f, ...] (mutable, sparse)
resolved: [inherit, #000, #555, #51653f, ...] (editor wins, default fallback)resolveFg(idx) / resolveBg(idx) check the editor palette first, then fall back to RR.DEFAULT_FG / RR.DEFAULT_BG.
When HTML is pasted, getComputedStyle() is read per text node and mapped to pip bytes:
| CSS Property | Pip Mapping |
|---|---|
font-weight ≥ 700 | PIP_BOLD |
font-style: italic | PIP_ITALIC |
text-decoration: underline | PIP_UNDERLINE |
text-decoration: line-through | PIP_STRIKETHROUGH |
color | Nearest FG palette match or new editor slot |
background-color | Nearest BG palette match (near-white → transparent) |
font-size | Nearest DEFAULT_SIZES match |
font-family | Serif/sans detection → P2_FONT_BIT |
Color matching uses Euclidean RGB distance with:
rgba with alpha ≈ 0, transparent, inherit → bg index 0The editor palette is per-document, editor-only state. It is not encoded in the wire format. Documents saved as wire streams use spec default colors (or preamble overrides). Future preamble extensions may bridge this gap.
Reference implementation of an interactive RichRender editor. Includes all engine files and demonstrates the full editing pipeline.
beforeinput-driven. The browser never modifies the DOM directly. All input events are intercepted, applied to the EditorDoc, and re-rendered.EditorDoc (from rr-editor.js). The DOM is a projection of the doc state.selectionchange event syncs DOM selection to a JS caret object. A _rendering guard prevents overwrite during re-render.editorDocToEditableHtml() builds HTML with inline styles from pip bytes. Cursor markers (zero-width spans) are injected to preserve caret position across re-renders.| Panel | Content |
|---|---|
| Editor | contenteditable area, white-space: pre-wrap |
| Toolbar | Style buttons, color swatches (2 rows), font/size selectors, paint/clear tools |
| Wire view | Hex dump of editorToWire() output |
| Disassembly | RR.disassemble() of wire bytes |
| Render-HTML | rrInflate() output with editor palette colors applied |
| Preview | Live HTML rendering of inflator output |
| Function | Purpose |
|---|---|
editorDocToEditableHtml(doc, cursor, selEnd) | EditorDoc → HTML with cursor markers |
pipToInlineStyle(p1, p2, p3) | Pip bytes → CSS inline style string |
cssToPips(computedStyle) | CSS computed style → {p1, p2, p3} |
pasteHtmlToChars(html) | HTML clipboard → per-character pip array |
matchFgColor(cssColor) | CSS color → palette index (with chromatic guard) |
matchBgColor(cssColor) | CSS background → palette index (with near-white filter) |
applyEditorPalette(html) | Post-process rrInflate output with editor palette colors |
resolveFg(idx) / resolveBg(idx) | Resolve palette index through editor→default chain |
79 tests across five sections:
| Section | Tests | What It Covers |
|---|---|---|
| One-Byte | 14 | Style flags vs MDR reference |
| Two-Byte | 12 | Color, font, highlight, background |
| Three-Byte | 15 | Size, headings, small-caps, overline, double underline |
| Lite-Wire | 15 | Paragraphs, lists, links, blockquotes, meta, mixed content |
| Editor | 23 | EditorDoc ops, painting, continuation sync, round-trip |
All tests in richrender-test.html.