authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-03-20 17:14:29-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-03-20 18:52:07-07:00
logb7690cda634c38b30367307cc546917adbda1064
treefa56b4f51193f053d2d1839d753c441cefa7fc56
parent80fb1789cd75eb917d402b010325e5d446b4b997
signaturebadge-check Signed by SSH key SHA256:cOKiuRFOeSRxne6EWgHtdQQSlBxjOXm2hOCFnCdLQbQ

chore: update readme


3 files changed, 115 insertions(+), 114 deletions(-)

README.md deleted-114
...@@ -1,114 +0,0 @@
1# `@clo/react-markdown`
2
3This package exports a React component to render Markdown using the [unified]
4ecosystem's Markdown tools (remark, rehype). The exported `<Markdown />`
5component is extremely memoized, making it suitable for streaming situations
6such as LLM chat interfaces.
7
8- **Bring your Existing Pipeline**: The primary option for configuration is
9 providing a `unified.Processor` with remark and rehype plugins configured.
10 Additionally, you can override the element renderers with the `components`
11 attribute. By default, `@clo/react-markdown` applies a bare bones config so
12 you can get started with just `<Markdown content="hi" />`. Configuration is
13 done either with a provider, or right at the component level.
14- **Handle Partial Markdown**: When the `predict` prop is set, the document
15 will be treated as an incomplete stream at the end of a document.
16 `hello **world` will be emitted as `hello <strong>world</strong>`, perfect for
17 the LLM case. (standalone predictor exported as `@clo/react-markdown/Predict`)
18
19The motivation for this package is to have an easy to understand version of
20[Streamdown]. With me banning all Vercel software at the company I work at,
21we needed a simple, trustable solution for this problem. Some differences
22to Streamdown:
23
24- **Better Memoization**: All inline components will preserve their state, even
25 as adjacent content changes. This is done to preserve remounts for things like
26 custom `<a>` tags or other components. (For example, if a custom `<a>` fetches
27 previewing data and provides a hover card, that card won't flicker).
28- **Better Prediction**: While optimizing edge cases, the prediction system became
29 better than the Streamdown one on accident.
30- **Headless UI**: No built in styles or components, bring your own CSS to blend
31 your markdown with your existing theme. `@clo/react-markdown` simply takes in
32 your existing `unified` pipeline and works off of that.
33- **Easy to Audit**: Only ~1000 lines of modern TypeScript. 4 total files.
34
35[Streamdown]: https://streamdown.ai
36[unified]: https://unifiedjs.com/
37
38## Getting Started
39
40Install off of the [JSR package](https://jsr.io/clo/react-markdown):
41
42```sh
43npx jsr add @clo/react-markdown
44pnpm add jsr:@clo/react-markdown
45```
46
47```tsx
48import { Markdown } from "@clo/react-markdown";
49import remarkGfm from "remark-gfm";
50import remarkParse from "remark-parse";
51import { unified } from "unified";
52
53// Define any unified processing pipeline. We expect that your
54// app already has one of these. @clo/react-markdown works with
55// most pipelines, but some important things to note:
56// - During parsing, all nodes have to have a source location.
57// - The pipeline must output a remark (Markdown) AST.
58// - You can also add `remarkRehype` and some rehype plugins.
59// - Do not include the `rehypeReact` plugin in here. It is added for you.
60const processor = unified().use(remarkParse).use(remarkGfm);
61
62// Define custom components. (maps to rehypeReact's option)
63const components = {
64 a: MarkdownLink,
65 code: InlineCode,
66};
67
68export function HelloWorld() {
69 return (
70 <Markdown
71 content="hello **world"
72 // Predict closing tags for streaming use case
73 predict
74 // These are optional, and can be set via `MarkdownOptionsProvider`
75 processor={processor}
76 components={components}
77 />
78 );
79}
80```
81
82If you want to play with the example project, which offers a comparison to
83Streamdown and the non-memoized `react-markdown` package.
84
85```
86git clone https://git.paperclover.net/clo/react-markdown
87cd react-markdown
88pnpm demo # auto-install
89```
90
91## Architecture
92
93The `Markdown` component is built out of a memoizer which uses the following tricks to improve performance:
94
95- Using proper `React.memo()` calls. Obviously.
96- Prediction is implemented in a stateful way that only parses the tail end of
97 the document, marking how much of the document is stable and where possible
98 incomplete syntax may live. Since prediction only applies at the end, changing
99 text midway through can invalidate the whole predictor.
100- Separate the parsed document into "blocks", noting the source location of
101 where each block lives.
102- Only start parsing after the first changed character, rounded to the nearest
103 block. In the append-only stream, this essentially means the last two blocks
104 are the only things being re-parsed.
105- Similarly, run AST transforms only on the changed data. This step has a couple
106 of slow paths for when reference link definitions are added or edited, since
107 it means any places that might have used a reference link may now have to
108 reflect it.
109- After all that, a special AST -> React node transform is used that diffs the
110 new ast with the last ast, reusing React nodes whenever possible. It supports
111 nested children as well as re-ordering top level blocks. This is what prevents
112 most rerenders and is the "secret sauce".
113
114All put together, basically nothing rerenders except what actually changed, and the document is beautiful.
readme.changes.md created+1
...@@ -0,0 +1 @@
1# notable changes in React Markdown
readme.md created+114
...@@ -0,0 +1,114 @@
1# `@clo/react-markdown`
2
3This package exports a React component to render Markdown using the [unified]
4ecosystem's Markdown tools (remark, rehype). The exported `<Markdown />`
5component is extremely memoized, making it suitable for streaming situations
6such as LLM chat interfaces.
7
8- **Bring your Existing Pipeline**: The primary option for configuration is
9 providing a `unified.Processor` with remark and rehype plugins configured.
10 Additionally, you can override the element renderers with the `components`
11 attribute. By default, `@clo/react-markdown` applies a bare bones config so
12 you can get started with just `<Markdown content="hi" />`. Configuration is
13 done either with a provider, or right at the component level.
14- **Handle Partial Markdown**: When the `predict` prop is set, the document
15 will be treated as an incomplete stream at the end of a document.
16 `hello **world` will be emitted as `hello <strong>world</strong>`, perfect for
17 the LLM case. (standalone predictor exported as `@clo/react-markdown/Predict`)
18
19The motivation for this package is to have an easy to understand version of
20[Streamdown]. With me banning all Vercel software at the company I work at,
21we needed a simple, trustable solution for this problem. Some differences
22to Streamdown:
23
24- **Better Memoization**: All inline components will preserve their state, even
25 as adjacent content changes. This is done to preserve remounts for things like
26 custom `<a>` tags or other components. (For example, if a custom `<a>` fetches
27 previewing data and provides a hover card, that card won't flicker).
28- **Better Prediction**: While optimizing edge cases, the prediction system became
29 better than the Streamdown one on accident.
30- **Headless UI**: No built in styles or components, bring your own CSS to blend
31 your markdown with your existing theme. `@clo/react-markdown` simply takes in
32 your existing `unified` pipeline and works off of that.
33- **Easy to Audit**: Only ~1000 lines of modern TypeScript. 4 total files.
34
35[Streamdown]: https://streamdown.ai
36[unified]: https://unifiedjs.com/
37
38## Getting Started
39
40Install off of the [JSR package](https://jsr.io/clo/react-markdown):
41
42```sh
43npx jsr add @clo/react-markdown
44pnpm add jsr:@clo/react-markdown
45```
46
47```tsx
48import { Markdown } from "@clo/react-markdown";
49import remarkGfm from "remark-gfm";
50import remarkParse from "remark-parse";
51import { unified } from "unified";
52
53// Define any unified processing pipeline. We expect that your
54// app already has one of these. @clo/react-markdown works with
55// most pipelines, but some important things to note:
56// - During parsing, all nodes have to have a source location.
57// - The pipeline must output a remark (Markdown) AST.
58// - You can also add `remarkRehype` and some rehype plugins.
59// - Do not include the `rehypeReact` plugin in here. It is added for you.
60const processor = unified().use(remarkParse).use(remarkGfm);
61
62// Define custom components. (maps to rehypeReact's option)
63const components = {
64 a: MarkdownLink,
65 code: InlineCode,
66};
67
68export function HelloWorld() {
69 return (
70 <Markdown
71 content="hello **world"
72 // Predict closing tags for streaming use case
73 predict
74 // These are optional, and can be set via `MarkdownOptionsProvider`
75 processor={processor}
76 components={components}
77 />
78 );
79}
80```
81
82If you want to play with the example project, which offers a comparison to
83Streamdown and the non-memoized `react-markdown` package.
84
85```
86git clone https://git.paperclover.net/clo/react-markdown
87cd react-markdown
88pnpm demo # auto-install
89```
90
91## Architecture
92
93The `Markdown` component is built out of a memoizer which uses the following tricks to improve performance:
94
95- Using proper `React.memo()` calls. Obviously.
96- Prediction is implemented in a stateful way that only parses the tail end of
97 the document, marking how much of the document is stable and where possible
98 incomplete syntax may live. Since prediction only applies at the end, changing
99 text midway through can invalidate the whole predictor.
100- Separate the parsed document into "blocks", noting the source location of
101 where each block lives.
102- Only start parsing after the first changed character, rounded to the nearest
103 block. In the append-only stream, this essentially means the last two blocks
104 are the only things being re-parsed.
105- Similarly, run AST transforms only on the changed data. This step has a couple
106 of slow paths for when reference link definitions are added or edited, since
107 it means any places that might have used a reference link may now have to
108 reflect it.
109- After all that, a special AST -> React node transform is used that diffs the
110 new ast with the last ast, reusing React nodes whenever possible. It supports
111 nested children as well as re-ordering top level blocks. This is what prevents
112 most rerenders and is the "secret sauce".
113
114All put together, basically nothing rerenders except what actually changed, and the document is beautiful.