@latentic/live-markdown 0.0.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tosin Amuda
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,265 @@
1
+ # @latentic/live-markdown
2
+
3
+ A rich markdown editor for React — write like Notion, store as plain `.md`.
4
+
5
+ Built on [CodeMirror 6](https://codemirror.net/). Headings, bold, italic, lists, tables, images, math, footnotes, and code blocks render inline as you type. The file on disk is always standard markdown — no proprietary format, no AST translation layer, no lock-in.
6
+
7
+ ```tsx
8
+ import { CodeMirrorMarkdownEditor } from "@latentic/live-markdown";
9
+
10
+ <CodeMirrorMarkdownEditor
11
+ value={markdown}
12
+ onChange={setMarkdown}
13
+ mode="wysiwyg" // or "source" for raw markdown
14
+ />
15
+ ```
16
+
17
+ ---
18
+
19
+ ## Why @latentic/live-markdown
20
+
21
+ Most markdown editors fall into two camps:
22
+
23
+ | Camp | Examples | Trade-off |
24
+ |------|----------|-----------|
25
+ | **Raw editors** | CodeMirror, Monaco, Ace | Fast, plain text — but users see `## Heading`, not a heading |
26
+ | **Rich editors** | Tiptap, ProseMirror, Slate, Lexical | WYSIWYG — but the internal model is a custom AST, not markdown. Round-tripping to `.md` is lossy or fragile |
27
+
28
+ **@latentic/live-markdown sits in between.** The source of truth is the raw markdown string. CodeMirror parses it with Lezer, and a decoration engine replaces syntax tokens with rendered widgets in real time — `## Heading` becomes a styled heading, `- item` becomes a bullet, `![alt](src)` becomes an inline image. You get the editing experience of Notion or Google Docs, but `value` in and `onChange` out is always a plain markdown string. No AST translation, no serialization bugs, no format lock-in.
29
+
30
+ The boundary semantics this demands — what every keystroke does at every construct edge — are specified in [docs/interaction-spec.md](docs/interaction-spec.md) and enforced by its conformance matrix (`interactionMatrix.test.ts`); block-level behaviors are specified executably in `src/codemirror/features/*.feature`.
31
+
32
+ ### How it compares
33
+
34
+ | Feature | @latentic/live-markdown | Tiptap / ProseMirror | ink-mde | @mdxeditor/editor | Novel |
35
+ |---------|-----------|---------------------|---------|-------------------|-------|
36
+ | Source of truth | Markdown string | Custom AST | Markdown string | MDX AST | ProseMirror AST |
37
+ | Rich rendering | Inline decorations | DOM nodes | Syntax highlight only | DOM nodes | DOM nodes |
38
+ | Round-trip fidelity | Byte-for-byte | Lossy (serializer) | Byte-for-byte | MDX subset | Lossy |
39
+ | YAML frontmatter | Preserved, never stripped | Plugin (varies) | No | Plugin | No |
40
+ | LaTeX math | Inline KaTeX | Plugin | No | Plugin | No |
41
+ | Large files (1 MB+) | Fast (CodeMirror) | Slow (DOM-per-node) | Fast | Slow | Slow |
42
+ | Tables | GFM, cell navigation | Plugin | Basic | Plugin | Slash command |
43
+ | Image paste/drop | Built-in | Plugin | No | Plugin | Plugin |
44
+ | Framework | React | React / Vue / vanilla | Vanilla / adapters | React | React |
45
+ | Bundle size | ~80 KB (gzip, editor core) | ~120 KB+ | ~40 KB | ~150 KB+ | ~200 KB+ |
46
+
47
+ ### Key differentiators
48
+
49
+ - **Markdown in, markdown out.** No AST translation layer. The `value` prop is a markdown string; `onChange` returns a markdown string. What you put in is exactly what you get out, byte for byte. YAML frontmatter is preserved and never stripped.
50
+
51
+ - **Rich editing without leaving markdown.** Headings, bold, italic, strikethrough, inline code, block code, bullet lists, ordered lists, task lists, tables, horizontal rules, images, footnotes, LaTeX math, and wikilinks all render inline. The user never sees raw syntax unless they switch to source mode.
52
+
53
+ - **Fast on large files.** Built on CodeMirror 6's virtual viewport — only visible lines are rendered. A 1 MB document opens instantly and scrolls at 60 fps. ProseMirror/Tiptap-based editors create a DOM node per text node and degrade on large documents.
54
+
55
+ - **Per-tab state caching.** When used with tabbed interfaces, editor state (cursor position, scroll offset, undo history) is cached per file and restored instantly on tab switch — a pointer swap, not a re-parse.
56
+
57
+ - **Extension system.** Add custom markdown rendering by contributing `NodeRule`s — one function per syntax node deciding how it paints (line class, styled span, hidden marker, or widget). Built-in extensions: highlight marks, footnotes, math (KaTeX), tables (cell navigation + resize), wikilinks.
58
+
59
+ - **Toolbar slot, not toolbar opinions.** The editor renders a host-supplied `toolbar` around the live `EditorView`, and `selectionActions` around the current selection — both are render props (`(ctx) => ReactNode`). The host app owns Save, Export, Comments, or whatever actions it needs — the editor stays agnostic.
60
+
61
+ ---
62
+
63
+ ## Features
64
+
65
+ ### Inline rendering
66
+ - **Headings** (ATX `#` only — setext `---`/`===` disabled for predictability)
67
+ - **Bold**, **italic**, **strikethrough**, **inline code**
68
+ - **Block code** with language label
69
+ - **Bullet lists**, **ordered lists**, **task lists** (interactive checkboxes)
70
+ - **Tables** — GFM syntax, cell-by-cell Tab navigation, column resize
71
+ - **Images** — inline preview, drag-and-drop, clipboard paste
72
+ - **Horizontal rules**
73
+ - **Footnotes** — inline marker with hover preview
74
+ - **LaTeX math** — inline `$...$` and display `$$...$$` via KaTeX
75
+ - **Wikilinks** — `[[Page]]` and `[[Page|alias]]` with Cmd/Ctrl-click navigation
76
+ - **Links** — Cmd/Ctrl-click to open, auto-detection
77
+
78
+ ### Editing
79
+ - **Rich / Source mode toggle** — switch between rendered and raw markdown
80
+ - **Smart list continuation** — Enter continues the current list item tightly (no blank-line gap)
81
+ - **Cursor model** — arrow keys skip hidden syntax markers, land on visible content
82
+ - **Delete normalizer** — Backspace/Delete removes visible content, collapses empty format spans
83
+ - **Format commands** — Cmd+B (bold), Cmd+I (italic), Cmd+E (inline code)
84
+ - **Block commands** — Cmd+1..3 (heading levels), Cmd+Shift+7..9 (lists, blockquote)
85
+ - **Click model** — Cmd/Ctrl-click on links and wikilinks to navigate; single click places caret at visible content, never inside a hidden marker
86
+ - **Image insert** — toolbar button opens a file picker, inserts at caret
87
+
88
+ ### Data
89
+ - **Frontmatter** — YAML frontmatter is parsed, held aside during editing, and recombined on save. Never stripped, never corrupted, never shown in the editor body.
90
+ - **Autosave debounce** — `onChange` fires 500ms after the last keystroke (configurable)
91
+ - **State caching** — per-file `EditorState` cache for instant tab switching
92
+
93
+ ---
94
+
95
+ ## Installation
96
+
97
+ ```sh
98
+ npm install @latentic/live-markdown
99
+ # or
100
+ pnpm add @latentic/live-markdown
101
+ ```
102
+
103
+ Peer dependencies: `react` and `react-dom` (18+).
104
+
105
+ All *in-editor* styling (headings, code, lists, tables, image widgets, math, links) ships with the editor as a CodeMirror theme and applies automatically — no CSS import needed. For the outer container layout (so the editor fills its parent and scrolls), import the small stylesheet once:
106
+
107
+ ```ts
108
+ import "@latentic/live-markdown/styles.css";
109
+ ```
110
+
111
+ Skip it if your app already lays the editor out as a flex child.
112
+
113
+ ---
114
+
115
+ ## Usage
116
+
117
+ ### Basic
118
+
119
+ ```tsx
120
+ import { useState } from "react";
121
+ import { CodeMirrorMarkdownEditor } from "@latentic/live-markdown";
122
+
123
+ function Editor() {
124
+ const [doc, setDoc] = useState("# Hello\n\nStart writing...");
125
+
126
+ return (
127
+ <CodeMirrorMarkdownEditor
128
+ value={doc}
129
+ onChange={(newValue) => setDoc(newValue)}
130
+ />
131
+ );
132
+ }
133
+ ```
134
+
135
+ ### Source mode
136
+
137
+ ```tsx
138
+ <CodeMirrorMarkdownEditor value={doc} onChange={setDoc} mode="source" />
139
+ ```
140
+
141
+ ### With toolbar actions
142
+
143
+ The `toolbar` render prop receives the live `EditorView`, so host buttons can drive editor commands:
144
+
145
+ ```tsx
146
+ <CodeMirrorMarkdownEditor
147
+ value={doc}
148
+ onChange={setDoc}
149
+ toolbar={({ view }) => <button onClick={() => save(view.state.doc.toString())}>Save</button>}
150
+ />
151
+ ```
152
+
153
+ ### Wikilinks
154
+
155
+ ```tsx
156
+ <CodeMirrorMarkdownEditor
157
+ value={doc}
158
+ onChange={setDoc}
159
+ linkTargets={new Set(["notes/Daily.md", "notes/Ideas.md"])}
160
+ onNavigateToLink={(path) => openFile(path)}
161
+ />
162
+ ```
163
+
164
+ ---
165
+
166
+ ## Props
167
+
168
+ | Prop | Type | Default | Description |
169
+ |------|------|---------|-------------|
170
+ | `value` | `string` | — | The markdown content (controlled) |
171
+ | `onChange` | `(value: string, changes: DocumentTextChange[]) => void` | — | Called after edits, debounced |
172
+ | `mode` | `"wysiwyg" \| "source"` | `"wysiwyg"` | Rich rendering or raw markdown |
173
+ | `toolbar` | `(ctx: { view: EditorView }) => ReactNode` | — | Host-rendered toolbar, given the live editor view |
174
+ | `selectionActions` | `(ctx: { selection, dismiss }) => ReactNode` | — | Host-rendered actions for the current selection (e.g. a comment bubble) |
175
+ | `linkTargets` | `ReadonlySet<string>` | — | Known file paths for wikilink resolution |
176
+ | `onNavigateToLink` | `(path: string) => void` | — | Called on Cmd/Ctrl-click of an internal link |
177
+ | `workspaceRoot` | `string` | — | Root path for resolving relative image URLs |
178
+ | `filePath` | `string` | — | Current file path (for image resolution context) |
179
+ | `resolveImageSrc` | `ResolveImageSrc` | render as-is | Map a markdown image `src` to a loadable URL |
180
+ | `saveImageBytes` | `SaveImageBytes` | inline as `data:` | Persist a pasted/dropped image at a workspace-relative path |
181
+ | `onOpenExternalUrl` | `OpenExternalUrl` | new browser tab | Open a clicked external link |
182
+
183
+ ---
184
+
185
+ ## Extensions
186
+
187
+ A `MarkdownExtension` bundles everything a feature needs: `NodeRule`s (how the syntax nodes its grammar introduces should paint), CM6 `extensions`, a `keymap`, and optional `toolbar` contributions. Rules are merged into the painter through a facet, so an extension adds a construct without touching the core rules table.
188
+
189
+ Built-in extensions:
190
+ - `highlightExtension` — highlight/mark rendering
191
+ - `footnoteExtension` — footnote markers with inline preview
192
+ - `mathExtension` — LaTeX math via KaTeX
193
+ - `tableExtension` — GFM tables with cell navigation
194
+ - `wikilinkExtension` — `[[wikilink]]` rendering and navigation
195
+
196
+ ```tsx
197
+ import { composeExtensions, mathExtension, tableExtension } from "@latentic/live-markdown";
198
+
199
+ const composed = composeExtensions([mathExtension, tableExtension]);
200
+ // composed.extensions — CM6 Extension[] (each extension's node rules ride
201
+ // along via a facet, so this is all the editor needs)
202
+ // composed.toolbar — merged ToolbarContribution[]
203
+ ```
204
+
205
+ ### A custom extension
206
+
207
+ A rule is one function per Lezer node name, returning how that node paints. The `mark` combinator covers the common "style this span" case. Rules merge last-wins, so an extension can introduce a construct from its own grammar or deliberately restyle a built-in one:
208
+
209
+ ```tsx
210
+ import { composeExtensions, mark, type MarkdownExtension } from "@latentic/live-markdown";
211
+
212
+ const fancyEmphasis: MarkdownExtension = {
213
+ name: "fancy-emphasis",
214
+ version: "1.0.0",
215
+ rules: {
216
+ // override how the built-in Emphasis node paints
217
+ Emphasis: mark("cm-fancy-emphasis"),
218
+ },
219
+ };
220
+
221
+ const composed = composeExtensions([fancyEmphasis]);
222
+ ```
223
+
224
+ `Paint` is a closed set — line class, span mark, hide, widget, or nothing — while node names grow, so styling a construct is always one rule in one place.
225
+
226
+ ---
227
+
228
+ ## Architecture
229
+
230
+ ```
231
+ value (markdown string)
232
+
233
+ ├─ parseFrontmatter() ─→ frontmatter (held in ref) + body
234
+
235
+ body ─→ CodeMirror EditorState
236
+
237
+ ├─ Lezer markdown parser (tokenizes)
238
+ ├─ Paint engine (each node's NodeRule → Decoration)
239
+ ├─ Extension plugins (math, tables, footnotes, ...)
240
+ └─ EditorView (renders only the visible viewport)
241
+
242
+ onChange ─→ serializeMarkdown(frontmatter + body)
243
+
244
+ └─ markdown string out
245
+ ```
246
+
247
+ The decoration engine walks the Lezer syntax tree on every document change, asks each node's `NodeRule` how to render, and applies the returned paint as `Decoration.replace`, `Decoration.mark`, or `Decoration.line`. Widgets are stateless — they read from the document and write `dispatch` calls back. No intermediate AST, no custom document model.
248
+
249
+ ---
250
+
251
+ ## Roadmap
252
+
253
+ - [x] Host-injected toolbar and selection-action slots (bring your own UI)
254
+ - [x] Host-environment seams (image storage/resolution, link opening) with browser defaults
255
+ - [x] Self-themed editor surface (CodeMirror theme ships with the package)
256
+ - [x] Public extension API for contributing node rules (`MarkdownExtension.rules` → `nodeRulesFacet`)
257
+ - [ ] Frontmatter as a toggleable extension (default on, disable via prop)
258
+ - [ ] Collaborative editing (CM6 collab extension)
259
+ - [ ] Slash commands (`/` menu for inserting blocks)
260
+
261
+ ---
262
+
263
+ ## License
264
+
265
+ [MIT](LICENSE)