@wingleeio/mugen-markdown 0.1.0

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 Wing Lee
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,183 @@
1
+ # @wingleeio/mugen-markdown
2
+
3
+ Measurable markdown for [`@wingleeio/mugen`](../mugen). Markdown is parsed with
4
+ [incremark](https://www.incremark.com/) into an mdast tree, then rendered with
5
+ **mugen primitives** — so mugen's tree walker computes each row's height
6
+ analytically, off-screen and never-mounted rows included, with no
7
+ measure-on-mount layout shift.
8
+
9
+ The hard part of markdown in an analytic virtualizer is **inline rich text**: a
10
+ sentence like “see `foo()` for **details**” is one wrapping flow of mixed fonts,
11
+ which a single-font `<Text>` can't measure. mugen-markdown introduces a
12
+ `RichText` primitive that measures mixed-font runs as one flow via
13
+ [`@chenglou/pretext`](https://github.com/chenglou/pretext)'s rich-inline layout —
14
+ the same layout the browser performs over the rendered spans, so the analytic
15
+ height matches the paint exactly (verified by a real-browser accuracy gate).
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ npm i @wingleeio/mugen-markdown @wingleeio/mugen
21
+ ```
22
+
23
+ Requires React 18.2 or 19. ESM + CJS, Node ≥22.
24
+
25
+ ## Quick start
26
+
27
+ ```tsx
28
+ import { MugenVList, useMugenVirtualizer } from '@wingleeio/mugen';
29
+ import { Markdown } from '@wingleeio/mugen-markdown';
30
+
31
+ function Thread({ messages }: { messages: Message[] }) {
32
+ const list = useMugenVirtualizer({ items: messages });
33
+ return (
34
+ <MugenVList
35
+ instance={list}
36
+ getKey={(m) => m.id}
37
+ maxW="3xl"
38
+ stickToBottom
39
+ render={(m) => <Markdown source={m.body} />}
40
+ />
41
+ );
42
+ }
43
+ ```
44
+
45
+ `<Markdown>` is a **pure, hook-free** component: it produces the identical
46
+ primitive tree in mugen's measure walk and in React's render, so heights can't
47
+ desync.
48
+
49
+ ### Streaming
50
+
51
+ Streaming a growing `source` (LLM output) just works — pass the new string each
52
+ tick and every row re-measures to its exact height as it grows. Parsing is
53
+ **incremental automatically**: when a `source` extends the one a row parsed last,
54
+ mugen-markdown appends only the new text to a retained incremark parser
55
+ (`O(delta)`), instead of re-parsing the whole prefix each tick (`O(n²)` over the
56
+ stream). Unchanged rows (older messages) are served from a parse cache and never
57
+ re-parsed; a non-extending edit falls back to a fresh parse. No API to learn — it
58
+ keys off the `source` value, so the same `<Markdown source={text} />` is fast
59
+ whether `text` is static or still being written.
60
+
61
+ ## Theming
62
+
63
+ Everything that affects height — fonts, line heights, paddings, gaps — lives in
64
+ the theme as concrete values (the measure walk only sees props, never React
65
+ context). Fonts are a **family** plus sizes/weights; inline variants (bold in a
66
+ heading, code in a paragraph) are composed automatically. Pass a deep-partial
67
+ theme:
68
+
69
+ ```tsx
70
+ <Markdown
71
+ source={md}
72
+ theme={{
73
+ fontFamily: 'Inter',
74
+ monoFamily: 'JetBrains Mono',
75
+ fontSize: 15,
76
+ lineHeight: 24,
77
+ link: { color: '#7c3aed' },
78
+ code: { background: '#0b1020', color: '#e5e7eb' },
79
+ }}
80
+ />
81
+ ```
82
+
83
+ Families must be measurable — a named web font (`"Inter"`) or a canvas-safe
84
+ generic (`"sans-serif"`, `"monospace"`). `"system-ui"` is rejected, because its
85
+ canvas metrics drift from what CSS paints.
86
+
87
+ ## Syntax highlighting
88
+
89
+ Fenced code blocks are syntax-highlighted by default — without ever touching
90
+ layout, and without ever blocking a frame. The `<pre><code>` renders its plain
91
+ text immediately (selectable, searchable, accessible) and stays the layout
92
+ source of truth; the language is tokenized off the critical path in time-sliced
93
+ chunks; and token colours are painted onto canvas tiles overlaying the text, at
94
+ which point the DOM text flips to `color: transparent` in the same frame.
95
+ Because highlighting is pure paint, it can't change a block's measured height —
96
+ `lines × lineHeight + padding` stays exact, tokens or not — and streaming
97
+ appends re-tokenize and repaint only the changed tail. Tiles allocate canvas
98
+ memory only while near the viewport, so huge blocks stay cheap.
99
+
100
+ Tune the palette (or turn it off) through the theme:
101
+
102
+ ```tsx
103
+ <Markdown
104
+ source={md}
105
+ theme={{
106
+ code: {
107
+ highlight: { keyword: '#c678dd', string: '#98c379' }, // deep-partial
108
+ // highlight: false, // or disable
109
+ },
110
+ }}
111
+ />
112
+ ```
113
+
114
+ A compact built-in tokenizer covers the common fence languages (ts/js, python,
115
+ rust, go, c/c++/c#, java, php, ruby, swift, kotlin, shell, sql, css/scss, html,
116
+ json, yaml, toml, dockerfile, …); unknown languages simply stay plain. Register
117
+ your own with `registerLanguage(['mylang'], profile)`.
118
+
119
+ ## Extending: typed components
120
+
121
+ Override any block-level node with a typed component. The `node` is the matching
122
+ mdast node, `children` is what the default would render (so you can wrap it), and
123
+ `ctx` exposes recursion + theme helpers. **Build overrides from mugen
124
+ primitives** (re-exported here) so they stay measurable:
125
+
126
+ ```tsx
127
+ import {
128
+ Markdown,
129
+ defineMarkdownComponents,
130
+ VStack,
131
+ RichText,
132
+ } from '@wingleeio/mugen-markdown';
133
+
134
+ const components = defineMarkdownComponents({
135
+ // `node` is `Heading` — `node.depth` is 1..6.
136
+ heading: ({ node, children, ctx }) =>
137
+ node.depth === 1 ? (
138
+ <VStack gap={4} padding={8} style={{ borderLeft: '3px solid #7c3aed' }}>
139
+ {children}
140
+ </VStack>
141
+ ) : (
142
+ children
143
+ ),
144
+
145
+ // `node` is `Code`.
146
+ code: ({ node, ctx }) => <MyHighlightedCode value={node.value} lang={node.lang} ctx={ctx} />,
147
+ });
148
+
149
+ <Markdown source={md} components={components} />;
150
+ ```
151
+
152
+ Inline marks (bold, italic, code, links, strikethrough) are styled through the
153
+ **theme**, not as components — inline content must collapse into one wrapping
154
+ flow to be measured exactly. For full inline control, override the block
155
+ component (e.g. `paragraph`) and build runs with `ctx.inlineRuns(...)`.
156
+
157
+ ### What's overridable
158
+
159
+ `paragraph`, `heading`, `thematicBreak`, `blockquote`, `list`, `code`, `table`,
160
+ `image`, `html`. GFM (tables, task lists, strikethrough, autolinks) is on by
161
+ default. Images have no intrinsic measurable height — the default renders the alt
162
+ text; override `image` for real images with known dimensions.
163
+
164
+ ## Primitives
165
+
166
+ - **`RichText`** — mixed-font inline that wraps as one flow; height is
167
+ `lines × lineHeight` from pretext's rich-inline layout.
168
+ - **`CodeBlock`** — non-wrapping code; height is `lines × lineHeight + padding`.
169
+ Highlights known languages via the non-blocking canvas overlay (see above).
170
+ - **`TableBlock`** — GFM tables with columns aligned across rows (widths
171
+ proportional to content, identical in measure and paint), header background,
172
+ hairline dividers, and a rounded outer ring — all height-neutral.
173
+
174
+ Both are built with mugen's `markPrimitive`, the same way you'd build your own.
175
+
176
+ ## Develop
177
+
178
+ ```bash
179
+ pnpm --filter @wingleeio/mugen-markdown test # node tests (happy-dom, pretext mocked)
180
+ pnpm --filter @wingleeio/mugen-markdown test:browser # real-browser accuracy gate (Playwright/Chromium)
181
+ pnpm --filter @wingleeio/mugen-markdown check-types
182
+ pnpm --filter @wingleeio/mugen-markdown build # ESM + CJS + d.ts via tsdown
183
+ ```