typewright 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +168 -0
  3. package/SPEC.md +327 -0
  4. package/dist/chunk-CZQO7TTL.js +1533 -0
  5. package/dist/chunk-CZQO7TTL.js.map +1 -0
  6. package/dist/chunk-KNEKNZXK.js +286 -0
  7. package/dist/chunk-KNEKNZXK.js.map +1 -0
  8. package/dist/chunk-M6IVEMXO.js +1398 -0
  9. package/dist/chunk-M6IVEMXO.js.map +1 -0
  10. package/dist/chunk-NJ7PJKHN.js +652 -0
  11. package/dist/chunk-NJ7PJKHN.js.map +1 -0
  12. package/dist/commands-Z3ndUSza.d.cts +221 -0
  13. package/dist/commands-Z3ndUSza.d.ts +221 -0
  14. package/dist/core/index.cjs +2952 -0
  15. package/dist/core/index.cjs.map +1 -0
  16. package/dist/core/index.d.cts +410 -0
  17. package/dist/core/index.d.ts +410 -0
  18. package/dist/core/index.js +4 -0
  19. package/dist/core/index.js.map +1 -0
  20. package/dist/mdx/index.cjs +662 -0
  21. package/dist/mdx/index.cjs.map +1 -0
  22. package/dist/mdx/index.d.cts +252 -0
  23. package/dist/mdx/index.d.ts +252 -0
  24. package/dist/mdx/index.js +3 -0
  25. package/dist/mdx/index.js.map +1 -0
  26. package/dist/react/index.cjs +7439 -0
  27. package/dist/react/index.cjs.map +1 -0
  28. package/dist/react/index.d.cts +140 -0
  29. package/dist/react/index.d.ts +140 -0
  30. package/dist/react/index.js +3644 -0
  31. package/dist/react/index.js.map +1 -0
  32. package/dist/streaming/index.cjs +1494 -0
  33. package/dist/streaming/index.cjs.map +1 -0
  34. package/dist/streaming/index.d.cts +64 -0
  35. package/dist/streaming/index.d.ts +64 -0
  36. package/dist/streaming/index.js +4 -0
  37. package/dist/streaming/index.js.map +1 -0
  38. package/dist/types-CX1GOx0Y.d.cts +319 -0
  39. package/dist/types-CX1GOx0Y.d.ts +319 -0
  40. package/package.json +128 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Luke Rhodes
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,168 @@
1
+ <div align="center">
2
+
3
+ # Typewright
4
+
5
+ **A blazing-fast, from-scratch GitHub Flavored Markdown + MDX editor and streaming previewer for the web.**
6
+
7
+ Zero-runtime-dependency engine · Obsidian-style unified live preview · semantic folding · in-place tables · and an LLM token-stream renderer that *anticipates formatting as it arrives*.
8
+
9
+ [![npm](https://img.shields.io/badge/npm-typewright-cb3837)](https://www.npmjs.com/package/typewright)
10
+ [![license](https://img.shields.io/badge/license-MIT-blue)](./LICENSE)
11
+ [![status](https://img.shields.io/badge/status-alpha-orange)](./SPEC.md)
12
+
13
+ </div>
14
+
15
+ > [!NOTE]
16
+ > **Alpha (v0.2.1).** The engine is real and tested: a from-scratch,
17
+ > zero-runtime-dependency GFM+MDX parser → offset-exact AST → **sanitizing** HTML
18
+ > renderer, an **incremental reparse**, unified-mode + fold services, and a
19
+ > streaming **anticipation** renderer — all wired into working
20
+ > `<TypewrightEditor>` and `<StreamingPreview>` components (extensive unit +
21
+ > Playwright e2e coverage; an independent adversarial security/correctness review
22
+ > passed after fixes). **v0.2 lights up the surfaces v0.1 deferred:** comments &
23
+ > presence, a settings panel + **⌘K command palette**, native **syntax
24
+ > colouring**, **sandboxed MDX execution** (opaque-origin iframe), **Mermaid +
25
+ > math** engine hooks, the **in-place table grid**, the **fold menu**, **footnotes
26
+ > + definition lists**, streaming link/list/table **anticipation + smoothing**,
27
+ > and **threshold-gated virtualization** — with a reproducible **benchmark
28
+ > harness** ([docs/BENCHMARKS.md](./docs/BENCHMARKS.md)) and a gzip **size budget**.
29
+ >
30
+ > **v0.2.1 closes the remaining items:** the Obsidian-exact **per-caret marker
31
+ > reveal** now ships as an opt-in mode (`unifiedReveal: 'caret'`) — a managed
32
+ > `contentEditable` surface that reveals only the markers around the caret;
33
+ > block-level stays the default. IME/composition works through the platform
34
+ > (`contentEditable`/native textareas), so the SPEC §4.4 hidden-sink is a
35
+ > **documented architectural divergence**, not a gap. Also shipped: the published
36
+ > **CodeMirror-6 baseline**, an **axe-core a11y sweep**, and **reparse-span
37
+ > tightening** (mid-doc large-file keystrokes). Documented coverage boundaries: the
38
+ > deep CJK/dead-key/soft-keyboard IME tail and Home/End line-nav in the caret
39
+ > surface are exercised as far as headless e2e reaches, not exhaustively. The
40
+ > public API (`src/types.ts`) is stable and semver-versioned.
41
+
42
+ ---
43
+
44
+ ## Why Typewright
45
+
46
+ Most web Markdown editors sit on a generic editing framework (CodeMirror, ProseMirror, Lexical) and a generic parser (Lezer, `@mdx-js`). That is the right call for most teams — and the reason no one is meaningfully *faster* than anyone else: they share the same engines.
47
+
48
+ Typewright takes the other path. The **document model, the incremental parser, and the rendering layer are written from scratch with zero runtime dependencies**, tuned for one grammar (GFM + MDX) and one interaction model (unified live-preview editing). Markdown is block-structured, which makes incremental re-parsing genuinely cheap — an edit re-tokenizes *one block*, not the document — and keeping React out of the per-keystroke path removes the reconciliation tax that generic React-markdown editors pay.
49
+
50
+ The result is designed to win where it matters: **keystroke-to-paint latency on real documents**, not throughput on 20 MB files nobody edits. See [SPEC.md §10](./SPEC.md#10-performance-targets--benchmarking) for the honest performance thesis.
51
+
52
+ ## Highlights
53
+
54
+ - ⚡ **From-scratch, zero-runtime-dependency engine.** String-is-state model, viewport-virtualized DOM rendering, hand-written incremental block parser with exact source offsets.
55
+ - 👁 **Unified source-revealing mode.** Formatting renders inline; click any block to reveal and edit its raw Markdown (`**`, `` ` ``, `#`) in place, then blur to re-render — the Obsidian "Live Preview" idiom, native rather than bolted on. (Per-**caret** reveal ships opt-in via `unifiedReveal:'caret'` — see [FEATURES.md](./docs/FEATURES.md).)
56
+ - 🧩 **Full GFM + MDX v3.** Tables, task lists, strikethrough, autolinks, footnotes; MDX JSX, ESM `import`/`export`, and `{expressions}`.
57
+ - 🌊 **Streaming preview with formatting anticipation.** Feed it an LLM token stream and it renders word-by-word while *predicting* incomplete formatting — a partial `*bo` shows as in-progress bold, an unterminated fence opens a code block, partial JSX renders a component skeleton. ([demo pattern](https://elements.ai-sdk.dev/components/jsx-preview))
58
+ - 📁 **Semantic heading folding.** Fold a section and everything under it collapses to the next same-or-higher heading, with an H1–H6 fold menu and fold/unfold-all.
59
+ - ▦ **In-place table editing.** Edit GFM tables as a grid; the Markdown source stays the source of truth.
60
+ - 📊 **Mermaid & math.** Rendered inline, executed in an isolated sandbox.
61
+ - 🔒 **Electron-safe by design.** MDX/Mermaid execute in an opaque-origin sandboxed iframe — the XSS→RCE path is closed.
62
+ - 🎛 **Drop-in and highly configurable.** One React component, a headless core (`typewright/core`), and a streaming module (`typewright/streaming`).
63
+
64
+ ## Demo
65
+
66
+ A live demo that **runs the real library** lives in [`demo/`](./demo/): `pnpm demo` (Vite) serves it at http://localhost:5178. It mounts the actual `<TypewrightEditor>` and `<StreamingPreview>` — the four modes, unified block-level editing, GFM rendering, folding, and streaming anticipation — and is the target of the Playwright e2e suite (`pnpm e2e`).
67
+
68
+ To open the real demo **without a server**, run `pnpm demo:build` and open the self-contained, single-file [`demo/standalone.html`](./demo/standalone.html) directly (`file://`) — everything (React included) is inlined. (`demo/index.html` is the Vite source entry and needs the dev server; opening it raw shows instructions.)
69
+
70
+ The full **design vision**, including designed-but-deferred surfaces (inline comments + presence, the floating formatting toolbar, in-place tables, Mermaid), is preserved as the self-contained [`demo/design-prototype.html`](./demo/design-prototype.html) — open it directly in a browser. The macOS-style app icon + showcase are in [`assets/`](./assets/) ([`icon.svg`](./assets/icon.svg) · [`icon.html`](./assets/icon.html)).
71
+
72
+ ## Install
73
+
74
+ ```sh
75
+ npm install typewright
76
+ # or
77
+ pnpm add typewright
78
+ ```
79
+
80
+ `react` and `react-dom` (≥18) are optional peers — only needed for the React component and MDX widget islands. The headless core has none.
81
+
82
+ ## Quick start
83
+
84
+ ### Drop-in editor
85
+
86
+ ```tsx
87
+ import { TypewrightEditor } from 'typewright';
88
+
89
+ export function Doc() {
90
+ const [md, setMd] = React.useState('# Hello\n\nType **markdown** here.');
91
+ return (
92
+ <TypewrightEditor
93
+ value={md}
94
+ onChange={setMd}
95
+ mode="unified" // live preview with source revealed at the caret
96
+ extensions={{ gfm: true, mdx: true, mermaid: true }}
97
+ folding
98
+ />
99
+ );
100
+ }
101
+ ```
102
+
103
+ ### Streaming from an LLM
104
+
105
+ ```tsx
106
+ import { TypewrightEditor } from 'typewright';
107
+ import { createStreamController, pipeStream } from 'typewright/streaming';
108
+
109
+ const [text, setText] = React.useState('');
110
+ const controller = React.useMemo(
111
+ () => createStreamController(setText, { anticipate: true, smooth: true }),
112
+ [],
113
+ );
114
+
115
+ // e.g. Vercel AI SDK: await pipeStream(result.textStream, controller)
116
+ await pipeStream(llmTextStream, controller);
117
+
118
+ <TypewrightEditor value={text} mode="preview" readOnly />;
119
+ ```
120
+
121
+ ### Headless (no React)
122
+
123
+ The zero-dependency core parses, renders (sanitized), and reparses incrementally — no DOM required, so it runs in Node too:
124
+
125
+ ```ts
126
+ import { parse, renderToHtml, parseIncremental } from 'typewright/core';
127
+
128
+ const src = '# Doc\n\nType **markdown** here.';
129
+ const doc = parse(src);
130
+ const html = renderToHtml(doc); // sanitized HTML string
131
+
132
+ // a single-keystroke edit reparses by reusing the block prefix:
133
+ const next = src + '!';
134
+ const doc2 = parseIncremental(doc, src, { from: src.length, to: src.length, insert: '!' }, next);
135
+ ```
136
+
137
+ ## Configuration
138
+
139
+ `<TypewrightEditor>` accepts the full [`EditorConfig`](./src/types.ts) surface — `mode`, `toolbar`, `extensions` (`gfm` / `mdx` / `mermaid` / `math` / `syntaxHighlight`), `folding`, `keymap`, `theme`, `readOnly`, `overscan`, and the `onChange` / `onSelectionChange` / `onModeChange` events. The complete, documented contract is `src/types.ts`; the behaviour behind each option is specified in [SPEC.md §9](./SPEC.md#9-public-api).
140
+
141
+ > 📚 **The complete feature catalogue + API reference — every capability with honest shipped/planned status — is [`docs/FEATURES.md`](./docs/FEATURES.md).**
142
+
143
+ ## Package layout
144
+
145
+ | Import | What |
146
+ |---|---|
147
+ | `typewright` | The drop-in React editor component. |
148
+ | `typewright/core` | The headless, framework-agnostic engine. |
149
+ | `typewright/streaming` | The LLM stream controller + anticipation options. |
150
+
151
+ ## Status & roadmap
152
+
153
+ Typewright is being built spec-first. See **[SPEC.md](./SPEC.md)** for the architecture, **[docs/FEATURES.md](./docs/FEATURES.md)** for the honest per-feature status, and **[docs/BENCHMARKS.md](./docs/BENCHMARKS.md)** for measured performance. Broadly, as of **v0.2**:
154
+
155
+ 1. ✅ **Foundation** — document model, incremental GFM block parser, threshold-gated virtualized view.
156
+ 2. ✅ **Unified mode** — decoration culling, rebindable keybindings + ⌘K palette, folding + fold menu.
157
+ 3. ✅ **Rich editing** — in-place tables, native syntax highlighting, Mermaid, math (engines host-supplied).
158
+ 4. ✅ **MDX** — markup parser, wasm transform boundary, sandboxed execution.
159
+ 5. ✅ **Streaming** — the anticipation engine (links/lists/tables/smoothing) + partial JSX.
160
+ 6. ✅ **Hardening** — comments/presence, benchmarks + gzip size budget, opt-in per-caret marker reveal (contentEditable; IME via the platform), reparse-span tightening for mid-doc large-file edits, the published CodeMirror-6 baseline, and an axe-core a11y sweep are all shipped. The SPEC §4.4 custom hidden-sink is a documented architectural divergence (the per-caret + IME goals are met via contentEditable), not a remaining gap.
161
+
162
+ ## Contributing
163
+
164
+ Early days — issues and design discussion welcome. See [CONTRIBUTING.md](./CONTRIBUTING.md).
165
+
166
+ ## License
167
+
168
+ [MIT](./LICENSE) © Luke Rhodes
package/SPEC.md ADDED
@@ -0,0 +1,327 @@
1
+ # Typewright — Architecture Specification
2
+
3
+ **Status:** Draft v1 (pre-implementation) · **Owner:** Luke Rhodes ([@lprhodes](https://github.com/lprhodes)) · **License:** MIT
4
+
5
+ A highly performant, from-scratch GitHub Flavored Markdown + MDX editor and streaming previewer for the web, shipped as a drop-in React component with a headless core. This document is the authoritative design; the code is built to it.
6
+
7
+ ---
8
+
9
+ ## 1. Goals & non-goals
10
+
11
+ ### 1.1 Goals
12
+
13
+ 1. **Fastest interactive editing loop of any web Markdown editor** — measured as keystroke-to-paint latency (INP) on realistic documents, not batch throughput.
14
+ 2. **Full GitHub Flavored Markdown + full MDX v3** — tables, task lists, strikethrough, autolinks, footnotes; MDX JSX, ESM `import`/`export`, and `{expressions}`.
15
+ 3. **Unified source-revealing edit mode** — formatting renders inline, raw syntax reveals around the caret (the Obsidian "Live Preview" idiom), plus fully-editable rich preview and read-only render modes.
16
+ 4. **Rich editing UI** — in-place table editing, semantic heading folding with an H1–H6 menu, inline Mermaid, math, syntax-highlighted code, and standard text-editing keyboard shortcuts.
17
+ 5. **Streaming preview with formatting anticipation** — ingest an AI/LLM token stream and render incrementally while predicting incomplete formatting (partial `*bo` → in-progress bold; open fence → code block; partial JSX → component skeleton).
18
+ 6. **Drop-in and highly configurable** — one React component, a headless core, a streaming module; every behaviour toggleable.
19
+ 7. **Zero third-party runtime dependencies** in the engine (one *conceded* boundary: the JSX/TS→JS transform for MDX execution — see §7).
20
+ 8. **Web + Electron safe** — MDX/Mermaid execution cannot escalate to host access.
21
+ 9. **Collaboration-ready** — the architecture must not foreclose real-time CRDT co-editing (not a v1 feature).
22
+
23
+ ### 1.2 Non-goals (v1)
24
+
25
+ - Real-time collaborative co-editing (kept *possible*, not built — §14).
26
+ - A WYSIWYG model that abstracts the Markdown away (Typewright is always string-is-state).
27
+ - Beating batch Markdown-to-HTML converters on multi-MB throughput (the wrong metric — §10).
28
+ - Bundling a full JavaScript/TypeScript compiler (§7).
29
+
30
+ ---
31
+
32
+ ## 2. Design principles
33
+
34
+ 1. **The string is the source of truth.** The document is a flat text string. Rich rendering is a *non-destructive overlay* of decorations, never a mutation of the model. This is the single decision that makes the unified source-revealing mode native instead of a fight (it is why CodeMirror-class editors can do it and ProseMirror/Lexical-class ones cannot).
35
+ 2. **React out of the hot path.** The per-keystroke loop — parse, decorate, render viewport — is vanilla DOM against the model. React appears *only* to mount interactive widget islands (tables, Mermaid, MDX components) via a portal registry (§5.4). This removes the reconciliation cost generic React-markdown editors pay.
36
+ 3. **Own the model, parser, and view; concede only input capture and JS execution to the platform.** Reimplementing IME/composition or a JS engine is a trap; everything else is ours (§4.4, §7).
37
+ 4. **Incrementality follows Markdown's block structure.** An edit dirties one block; re-tokenize that block, reuse every other node. Block boundaries are a natural, cheap incrementality unit a generic parser doesn't exploit as cleanly (§4.2).
38
+ 5. **Viewport-bounded work.** Parsing detail, decoration computation, and DOM materialization are bounded by what's visible, so latency is independent of document length.
39
+ 6. **Configurable by composition.** Features are extensions over a small core; a consumer enables only what they need, and the core has no hard dependency on any of them.
40
+
41
+ ---
42
+
43
+ ## 3. System architecture
44
+
45
+ ```
46
+ ┌───────────────────────────────────────────────────────────────────────┐
47
+ │ React binding (typewright) — thin wrapper, controlled/uncontrolled │
48
+ ├───────────────────────────────────────────────────────────────────────┤
49
+ │ Headless engine (typewright/core) │
50
+ │ │
51
+ │ Document model ──► Incremental parser ──► Decoration builder ──► View │
52
+ │ (piece table) (block + inline, (mark/replace/ (virtualized │
53
+ │ ▲ offset-exact) widget/line) DOM) │
54
+ │ │ │ │ │
55
+ │ Input substrate (hidden input sink: keys+IME) │ Widget registry │
56
+ │ │ │ │ │
57
+ │ Command / keymap ◄─────────────────────────────┘ React portals ┘ │
58
+ │ │
59
+ ├───────────────────────────────────────────────────────────────────────┤
60
+ │ Async side-channels (Web Workers) │
61
+ │ • MDX compile (markup parse = ours; JSX/TS transform = wasm) → iframe │
62
+ │ • Mermaid render → iframe │
63
+ │ • (optional) heavy syntax highlight for read/export views │
64
+ ├───────────────────────────────────────────────────────────────────────┤
65
+ │ Streaming layer (typewright/streaming) — anticipation renderer │
66
+ └───────────────────────────────────────────────────────────────────────┘
67
+ ```
68
+
69
+ ### Layer summary
70
+
71
+ | Layer | Responsibility | Thread | Deps |
72
+ |---|---|---|---|
73
+ | Document model | Immutable text + edit transactions + position mapping | main | none |
74
+ | Incremental parser | GFM + MDX markup → offset-exact tree, dirty-block reparse | main | none |
75
+ | Decoration builder | Map tree nodes → mark/replace/widget/line decorations, viewport-scoped | main | none |
76
+ | View | Virtualized DOM materialization, selection/caret rendering | main | none |
77
+ | Input substrate | Capture keystrokes/IME/composition via a hidden sink | main | platform |
78
+ | Widget registry | Mount React islands into decoration holes via portals | main | react (peer) |
79
+ | MDX compile | Markup parse (ours) + JSX/TS transform (wasm) + evaluate | worker + iframe | wasm transform |
80
+ | Streaming | Accumulate tokens, anticipate formatting, drive the view | main | none |
81
+
82
+ ---
83
+
84
+ ## 4. The editing engine (from scratch)
85
+
86
+ ### 4.1 Document model
87
+
88
+ - A **piece table** (or gap buffer) over the immutable original string plus appended edit buffers. Edits are `{from, to, insert}` transactions; the model never mutates in place, enabling cheap undo/redo and position mapping.
89
+ - **Position mapping**: every transaction produces a change description that maps prior offsets to new offsets, so decorations, selections, and fold ranges survive edits without a full recompute.
90
+ - **UTF-16 offsets** throughout (matching the DOM/`Selection` API); grapheme-cluster awareness is handled at the cursor-motion layer, not the model.
91
+
92
+ ### 4.2 Incremental parser
93
+
94
+ The parser is hand-written, offset-exact, and specialized for GFM + MDX. It is a **two-phase block-then-inline** design:
95
+
96
+ 1. **Block phase** — line-oriented scan that partitions the document into blocks (headings, paragraphs, fenced code, blockquotes, lists, tables, HTML/JSX blocks, MDX ESM lines, thematic breaks). Each block records `{from, to, type, meta}`.
97
+ 2. **Inline phase** — within each block that permits inline content, a delimiter-stack scan resolves emphasis, code spans, links, images, autolinks, strikethrough, footnote refs, and MDX expression/JSX spans.
98
+
99
+ **Incrementality.** On an edit, only the block(s) overlapping the change region are re-scanned (plus a small look-around for constructs that can merge/split across block boundaries — e.g. a blank line splitting a paragraph, a ` ``` ` opening/closing a fence). Unchanged blocks keep their nodes and are re-offset via position mapping. This is `O(edited block)`, not `O(document)`.
100
+
101
+ **Offset exactness** is the contract everything downstream depends on: every node exposes precise `{from, to}` for its syntax markers *and* its content, which is what lets decorations hide/reveal delimiters and what defines foldable ranges and table cell boundaries.
102
+
103
+ **Error tolerance.** Incomplete/malformed input (mid-typing, mid-stream) never throws; it produces a best-effort tree with open nodes. This property is what the streaming anticipation engine (§8) builds on.
104
+
105
+ **GFM coverage.** Tables, task list items, strikethrough (`~~`), extended autolinks, and footnotes are first-class block/inline node types, gated by the `gfm` extension.
106
+
107
+ **MDX coverage (markup only).** The parser recognizes ESM `import`/`export` statement lines, JSX element boundaries (open/close/self-closing, nested), and `{expression}` spans — enough to highlight, fold, select, and delimit them. It does **not** evaluate JS; that is §7.
108
+
109
+ ### 4.3 Decoration system
110
+
111
+ Four decoration kinds, applied over `{from, to}` ranges, recomputed only for the visible viewport on each transaction:
112
+
113
+ - **Mark** — inline styling of a range (emphasis, code, links).
114
+ - **Replace** — hide a range and optionally substitute a DOM node (collapsing `**` markers; rendering an image; replacing a table block with its grid widget).
115
+ - **Widget** — insert a DOM node at a point without altering text (fold placeholders, the heading fold affordance).
116
+ - **Line** — attributes on a whole line's element (active line, blockquote indent, heading level).
117
+
118
+ Decorations are held in a range set that is **mapped through** each transaction's changes (indices shift, no recompute) and only *rebuilt* for blocks that actually changed within the viewport. This is what prevents the flicker/GC/layout-thrash of naive rebuild-everything approaches.
119
+
120
+ ### 4.4 Input substrate — the line we do not cross
121
+
122
+ Raw text input is the genuinely hard, platform-specific layer: IME/CJK composition, dead keys, Android soft keyboards, autocorrect, spellcheck, bidi, grapheme-cluster cursor motion, screen-reader semantics.
123
+
124
+ **Decision:** own the caret, selection rendering, model, and *everything visible*, but route **keystroke and composition capture through a single hidden/overlaid contentEditable-or-input sink** (the technique CodeMirror and Monaco use). We do **not** reimplement composition events. This one concession is the difference between a fast custom editor and a broken one.
125
+
126
+ ### 4.5 View — virtualization
127
+
128
+ Only lines intersecting `[viewportTop − overscan, viewportBottom + overscan]` are materialized as DOM; off-screen lines are represented by height-reserving placeholders. Line heights are measured and cached; scroll anchoring keeps the caret stable across reflow. Memory and per-keystroke DOM work are bounded by viewport size, not document size.
129
+
130
+ ---
131
+
132
+ ## 5. Modes & rich editing
133
+
134
+ ### 5.1 Modes
135
+
136
+ `edit` (raw + highlight) · `unified` (live preview, source revealed at caret — flagship) · `preview` (fully rendered, still editable) · `read` (rendered, non-editable). Modes are a decoration-policy switch over the same model, not different engines.
137
+
138
+ ### 5.2 Unified source-revealing mode
139
+
140
+ The signature interaction. Algorithm, per transaction:
141
+
142
+ 1. The parser yields the syntax nodes intersecting the viewport with exact marker offsets.
143
+ 2. A state field computes the **active ranges**: nodes whose `{from, to}` (widened by one position on each side) contain any selection range.
144
+ 3. For each formatting node:
145
+ - **Caret outside** → `Replace` the syntax markers (hide `**`/`` ` ``/`#`), `Mark` or `Widget` the content as rendered.
146
+ - **Caret inside/adjacent** → omit the replace decoration so the raw markers render back into the DOM, with a subtle monospace `Mark` signalling "you are editing raw syntax."
147
+ 4. A short **click-settle debounce** (~30–50 ms) defers the render→raw transition on click so the DOM shift doesn't land mid-click and mis-place the caret / trigger stray selection (a documented failure of naive implementations).
148
+
149
+ ### 5.3 Heading folding
150
+
151
+ Semantic, not indentation-based. A fold on an `H2` collapses everything until the next heading of equal-or-higher level. Implemented as a fold service walking the parse tree (never regex, which breaks on headings inside code blocks). The UI (per the reference screenshots): a per-heading affordance opening a menu with **Heading 1–6** level controls, **Toggle Folding**, **Fold All Headers**, **Unfold All Headers**, and **Copy Link to here**. Fold state is optionally persisted (`folding.persistKey`). Fold placeholders are `Widget` decorations that can summarize the collapsed section (child count / word count).
152
+
153
+ ### 5.4 In-place tables & widget islands
154
+
155
+ GFM tables (and Mermaid/MDX components) render as **widget islands**:
156
+
157
+ 1. The parser detects the block; a `Replace` decoration hides the raw source and mounts a widget in its place.
158
+ 2. The widget's DOM hole is registered with a **central portal registry**; the top-level React tree `createPortal`s the interactive component (a grid editor, a Mermaid frame, an MDX component) into it. No isolated React roots.
159
+ 3. **Bidirectional sync**: editing a cell dispatches a scoped `{from, to, insert}` transaction against exactly that cell's source range — the Markdown stays canonical.
160
+ 4. **Atomic ranges + transaction filters**: the block's source range is registered atomic so the keyboard caret treats it as one indivisible unit; arrow-key motion into it hands focus to the widget, and motion past the last cell returns focus to the text. This prevents the caret getting "trapped" in hidden source.
161
+ 5. Off-screen widgets are destroyed (`WidgetType.destroy()` → portal unmount), preserving the frame budget.
162
+
163
+ ### 5.5 Syntax highlighting, Mermaid, math
164
+
165
+ - **Editing view** highlighting is native (the parser already tokenizes fenced code via nested sub-grammars) — no third-party highlighter on the hot path.
166
+ - **Read/export** views may use a heavier, more accurate highlighter, offloaded to a worker; optional.
167
+ - **Mermaid** renders in the isolated sandbox (§7), memoized by diagram source, re-rendered on edit debounce.
168
+ - **Math** via a pluggable engine (KaTeX-compatible interface); optional.
169
+
170
+ ---
171
+
172
+ ## 6. GFM & MDX support matrix
173
+
174
+ | Feature | Parse | Edit | Render | Notes |
175
+ |---|---|---|---|---|
176
+ | Headings, paragraphs, lists, blockquotes, thematic breaks | ✓ | ✓ | ✓ | CommonMark core |
177
+ | Emphasis / strong / inline code / links / images | ✓ | ✓ | ✓ | delimiter stack |
178
+ | Fenced & indented code | ✓ | ✓ | ✓ | nested highlight |
179
+ | GFM tables | ✓ | ✓ (grid) | ✓ | §5.4 |
180
+ | Task lists | ✓ | ✓ | ✓ | checkbox toggles source |
181
+ | Strikethrough | ✓ | ✓ | ✓ | `~~` |
182
+ | Autolinks (extended) | ✓ | ✓ | ✓ | |
183
+ | Footnotes | ✓ | ✓ | ✓ | |
184
+ | MDX ESM `import`/`export` | ✓ | ✓ | n/a | highlighted; feed the sandbox |
185
+ | MDX JSX elements | ✓ | ✓ | ✓ | render = §7 |
186
+ | MDX `{expressions}` | ✓ | ✓ | ✓ | render = §7 |
187
+ | Mermaid (```` ```mermaid ````) | ✓ | ✓ | ✓ | sandbox |
188
+ | Math (`$`/`$$`) | ✓ | ✓ | ✓ | optional |
189
+
190
+ ---
191
+
192
+ ## 7. MDX: the markup/execution split
193
+
194
+ MDX = Markdown + JSX + JS expressions. The two jobs are handled by different layers, and **neither the browser nor Electron parses JSX/TS for you** — the platform only *executes* plain JS.
195
+
196
+ 1. **Markup parse (ours).** Typewright's parser finds where JSX elements/expressions begin and end — enough for highlighting, folding, selection, and delimiting. Zero dependency.
197
+ 2. **Transform (the one conceded boundary).** Turning JSX/TS into runnable plain JS is delegated, because a correct JS+JSX parser/transformer is a multi-year project on its own. Options via `MdxOptions.transform`:
198
+ - `wasm-esbuild` — `esbuild-wasm` in a worker (smallest, fastest transform-only).
199
+ - `wasm-swc` — `@swc/wasm-web`.
200
+ - `constrained` — a small built-in transform for a *restricted* MDX-JS subset (no dependency), for hosts that want true zero-dep and accept a limited surface.
201
+ - a host-supplied `(code) => js` function.
202
+ 3. **Execution (the platform).** The transformed plain JS runs inside an **`iframe srcdoc sandbox="allow-scripts"` with an opaque origin** (no `allow-same-origin`). It cannot reach the host DOM, storage, or session — the same V8 in a browser tab and an Electron renderer, so behaviour is identical; Electron's Node-bearing main process is never in the execution path. This closes the XSS→RCE escalation. Host/network access a component legitimately needs is brokered by `postMessage` to the host, which proxies it (`SandboxOptions.onHostMessage`).
203
+
204
+ Pipeline: `debounce → worker: (our markup parse → transform) → postMessage compiled module → iframe evaluate → render`. Only the transform+eval are off the main thread; the editor never blocks on MDX.
205
+
206
+ ---
207
+
208
+ ## 8. Streaming preview with formatting anticipation
209
+
210
+ The novel capability. A token stream (typically an LLM) is rendered incrementally, and **incomplete formatting is optimistically resolved** so the preview reads as finished prose while it's still arriving. Reference behaviour: [ai-sdk JSX preview](https://elements.ai-sdk.dev/components/jsx-preview).
211
+
212
+ ### 8.1 Controller
213
+
214
+ `createStreamController(onUpdate, options)` (see `typewright/streaming`) accumulates chunks and exposes `push` / `replace` / `end` / `reset`. `pipeStream(source, controller)` drives it from an `AsyncIterable<string>` or `ReadableStream<string>` (e.g. `result.textStream` from the Vercel AI SDK). *(Accumulation + piping are implemented today; the anticipation renderer below is the pending work.)*
215
+
216
+ ### 8.2 Anticipation algorithm
217
+
218
+ The engine relies on the error-tolerant incremental parser (§4.2), which already yields **open nodes** for unterminated constructs:
219
+
220
+ 1. Parse the running buffer after each chunk (incremental — only the tail block is dirty).
221
+ 2. For each **open node** at the buffer's end, apply an **anticipation policy** (`AnticipationOptions`) that renders it as if closed:
222
+ - `*bo` / `**bo` → in-progress emphasis/strong (render bold, style the open marker as pending).
223
+ - unterminated `` ` `` or ```` ``` ```` → open inline-code / code block.
224
+ - `[text`(no `]`) / `[text](ur` → link forming.
225
+ - `#`…(no newline) → heading forming.
226
+ - a partially-typed table row → grid with a ghost row.
227
+ - `<Comp pr` → JSX element forming → render `componentFallback` skeleton until the tag + required props resolve.
228
+ 3. On the next chunk, the open node either **completes** (promote to a real node, drop the pending styling) or **extends** (keep anticipating). On `end()`, all still-open nodes resolve to their best-effort final form.
229
+ 4. **Stability rule:** anticipation only ever *adds* rendering ahead of confirmation; it never reflows already-confirmed content, so text doesn't jump as it streams.
230
+ 5. **Smoothing** (`smooth`) optionally reveals characters at a target rate rather than in raw chunk jumps, decoupling visual cadence from network cadence.
231
+
232
+ ### 8.3 Partial JSX / components
233
+
234
+ When `anticipate.jsx` is on and the transform supports it, a syntactically-complete-enough JSX subtree is speculatively transformed and rendered in the sandbox; an incomplete one shows `componentFallback`. Failed speculative transforms are swallowed (the tolerant path), never surfaced as errors mid-stream.
235
+
236
+ ---
237
+
238
+ ## 9. Public API
239
+
240
+ The full, documented contract is [`src/types.ts`](./src/types.ts). Shape:
241
+
242
+ - **`<TypewrightEditor>`** (`typewright`) — `EditorConfig` + `EditorEvents` + controlled/uncontrolled `value`/`defaultValue` + `className`/`style`.
243
+ - **`EditorView`** (`typewright/core`) — headless mount: `new EditorView({ parent, ...config })`, `dispatch`, `setSelection`, `destroy`.
244
+ - **`createStreamController` / `pipeStream`** (`typewright/streaming`).
245
+
246
+ Key config surfaces: `mode`, `extensions` (`gfm`/`mdx`/`mermaid`/`math`/`syntaxHighlight`, each a boolean or an options object), `folding`, `keymap` (preset + binding overrides), `theme` (appearance + token overrides), `readOnly`, `overscan`. Every feature is independently toggleable; nothing is mandatory but the core editor.
247
+
248
+ **Stability policy:** the type surface is the contract and is versioned semver; internal engine modules are private and may change freely.
249
+
250
+ ---
251
+
252
+ ## 10. Performance targets & benchmarking
253
+
254
+ **Thesis:** the metric that matters is **keystroke-to-paint latency (INP)** on realistic documents (5–50 KB, some containing tables / a Mermaid block / MDX), *not* Markdown-to-HTML throughput on multi-MB files nobody edits. WebAssembly Markdown parsers win on the latter and lose on the former (JS↔WASM marshalling dominates below ~256 KB), so the engine is JS + incremental, not WASM, on the hot path. WASM is used only where it genuinely wins: the off-thread MDX transform (§7).
255
+
256
+ **Targets (to be validated, not asserted):**
257
+
258
+ | Metric | Target | Workload |
259
+ |---|---|---|
260
+ | Keystroke → paint (p95) | < 8 ms | 50 KB doc, mixed content, mid-document edit |
261
+ | Cold parse | < 16 ms | 50 KB doc |
262
+ | Large-doc typing (p95) | < 8 ms | 1 MB doc (viewport-bounded) |
263
+ | Memory | viewport-bounded | 1 MB doc vs 50 KB doc within a small constant factor |
264
+ | Bundle (core, gzip) | small, tracked in CI | headless core, no extensions |
265
+
266
+ **Method:** a reproducible harness (documents + edit scripts) comparing against named incumbents (CodeMirror-6-based editors, ProseMirror/TipTap, Lexical) on the *same* workloads. A speed claim without a named workload and a named competitor is not shippable. Be honest: incumbents built on CodeMirror share its engine, so the win is in the tailored parser, the React-free hot path, MDX, and streaming — not a magic constant on plain typing.
267
+
268
+ ---
269
+
270
+ ## 11. Security model
271
+
272
+ - **MDX/Mermaid isolation** — opaque-origin sandboxed iframe, `allow-scripts` only, no `allow-same-origin`; host access brokered via `postMessage`. Electron-safe (§7).
273
+ - **No `dangerouslySetInnerHTML`** of compiled output into the host tree.
274
+ - **URL discipline** — every rendered link/image `href`/`src` passes a scheme allowlist.
275
+ - **CSP** — the sandbox document carries a strict CSP; hosts may extend it (`SandboxOptions.csp`).
276
+ - **Resource bounds** — parse/render guards against pathological inputs (deeply nested structures, giant tables) to avoid main-thread stalls.
277
+
278
+ ---
279
+
280
+ ## 12. Configurability & theming
281
+
282
+ - **Extensions** are opt-in modules; the core ships nothing it doesn't need.
283
+ - **Theming** via CSS custom properties (`--tw-*`) with `light`/`dark`/`auto` and per-token overrides; no CSS-in-JS runtime.
284
+ - **Keymap** is data-driven (command ids), fully rebindable, with a `none` preset for hosts that supply their own.
285
+ - **Commands** are a public registry, so hosts can add toolbar actions/slash-commands against the same transaction API the built-ins use.
286
+
287
+ ---
288
+
289
+ ## 13. Build / no-build boundaries
290
+
291
+ | Concern | Decision |
292
+ |---|---|
293
+ | Document model, parser, decorations, view | **From scratch, zero deps.** The moat and the speed. |
294
+ | Text-input capture (keys/IME/composition) | **Platform** (hidden sink). Do not reimplement. |
295
+ | JSX/TS → JS transform (MDX) | **One conceded dependency** (`esbuild-wasm`/`swc-wasm`) or a constrained hand-roll. |
296
+ | JS execution | **Platform** (V8 in a sandboxed iframe). Never a dependency. |
297
+ | CRDT (future) | A *shape* kept viable, not built in v1 (§14). |
298
+
299
+ ---
300
+
301
+ ## 14. Collaboration-readiness (future)
302
+
303
+ Not a v1 feature, but the architecture must not foreclose it. Because every change is an immutable `{from, to, insert}` transaction, a CRDT text type (Yjs-`Y.Text`-shaped) can bind by translating local transactions to CRDT ops and applying remote ops as transactions that flow through the *same* parse → decorate → render pipeline. Remote cursors/selections render as ordinary widget/line decorations. The v1 requirement is only that transactions stay the sole mutation path and positions stay mappable — both already true.
304
+
305
+ ---
306
+
307
+ ## 15. Roadmap
308
+
309
+ - **Phase 0 — Foundation.** Document model (piece table + position mapping), input substrate (hidden sink, IME), virtualized view, selection/caret rendering. *De-risk first: prove IME + a11y + scrolling before anything else.*
310
+ - **Phase 1 — Parser.** Incremental block+inline GFM parser with exact offsets and dirty-block reparse. Benchmark keystroke latency vs a CodeMirror-6 baseline (validates the thesis).
311
+ - **Phase 2 — Rendering & unified mode.** Decoration system, active-line culling, standard keymap, semantic heading folding + fold menu.
312
+ - **Phase 3 — Rich editing.** In-place tables, native code highlighting, Mermaid, math.
313
+ - **Phase 4 — MDX.** Markup parser extension, wasm transform boundary, sandboxed execution, component map.
314
+ - **Phase 5 — Streaming.** Anticipation engine, partial JSX, smoothing.
315
+ - **Phase 6 — Hardening.** Accessibility, IME/bidi edge cases, benchmark suite + published numbers, collaboration-readiness proof.
316
+
317
+ Each phase ships behind the stable API in [`src/types.ts`](./src/types.ts); the `<textarea>` scaffold is replaced incrementally as phases land.
318
+
319
+ ---
320
+
321
+ ## 16. Open questions
322
+
323
+ - **Constrained transform surface** — exactly which MDX-JS subset the zero-dep `constrained` transform supports, and how it degrades to `wasm-*`.
324
+ - **Widget island cost at scale** — pooling/virtualization strategy when a document has many inline Mermaid/MDX islands (per-island iframe budget vs a shared sandbox).
325
+ - **Anticipation policy defaults** — which constructs anticipate by default vs opt-in, tuned against real LLM streams to avoid distracting flicker.
326
+ - **Read/export highlighter** — whether to ship a heavier accurate highlighter for read mode or keep everything on the native tokenizer.
327
+ - **Collaboration timing** — when (if) to promote CRDT from "kept possible" to a shipped feature.