@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 +21 -0
- package/README.md +183 -0
- package/dist/index.cjs +1903 -0
- package/dist/index.d.cts +498 -0
- package/dist/index.d.mts +498 -0
- package/dist/index.mjs +1861 -0
- package/package.json +79 -0
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
|
+
```
|