| author | |
| committer | |
| log | b7690cda634c38b30367307cc546917adbda1064 |
| tree | fa56b4f51193f053d2d1839d753c441cefa7fc56 |
| parent | 80fb1789cd75eb917d402b010325e5d446b4b997 |
| signature |
3 files changed, 115 insertions(+), 114 deletions(-)
README.md deleted-114| ... | ... | @@ -1,114 +0,0 @@ |
| 1 | # `@clo/react-markdown` | |
| 2 | ||
| 3 | This package exports a React component to render Markdown using the [unified] | |
| 4 | ecosystem's Markdown tools (remark, rehype). The exported `<Markdown />` | |
| 5 | component is extremely memoized, making it suitable for streaming situations | |
| 6 | such 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 | ||
| 19 | The 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, | |
| 21 | we needed a simple, trustable solution for this problem. Some differences | |
| 22 | to 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 | ||
| 40 | Install off of the [JSR package](https://jsr.io/clo/react-markdown): | |
| 41 | ||
| 42 | ```sh | |
| 43 | npx jsr add @clo/react-markdown | |
| 44 | pnpm add jsr:@clo/react-markdown | |
| 45 | ``` | |
| 46 | ||
| 47 | ```tsx | |
| 48 | import { Markdown } from "@clo/react-markdown"; | |
| 49 | import remarkGfm from "remark-gfm"; | |
| 50 | import remarkParse from "remark-parse"; | |
| 51 | import { 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. | |
| 60 | const processor = unified().use(remarkParse).use(remarkGfm); | |
| 61 | ||
| 62 | // Define custom components. (maps to rehypeReact's option) | |
| 63 | const components = { | |
| 64 | a: MarkdownLink, | |
| 65 | code: InlineCode, | |
| 66 | }; | |
| 67 | ||
| 68 | export 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 | ||
| 82 | If you want to play with the example project, which offers a comparison to | |
| 83 | Streamdown and the non-memoized `react-markdown` package. | |
| 84 | ||
| 85 | ``` | |
| 86 | git clone https://git.paperclover.net/clo/react-markdown | |
| 87 | cd react-markdown | |
| 88 | pnpm demo # auto-install | |
| 89 | ``` | |
| 90 | ||
| 91 | ## Architecture | |
| 92 | ||
| 93 | The `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 | ||
| 114 | All 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 | ||
| 3 | This package exports a React component to render Markdown using the [unified] | |
| 4 | ecosystem's Markdown tools (remark, rehype). The exported `<Markdown />` | |
| 5 | component is extremely memoized, making it suitable for streaming situations | |
| 6 | such 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 | ||
| 19 | The 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, | |
| 21 | we needed a simple, trustable solution for this problem. Some differences | |
| 22 | to 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 | ||
| 40 | Install off of the [JSR package](https://jsr.io/clo/react-markdown): | |
| 41 | ||
| 42 | ```sh | |
| 43 | npx jsr add @clo/react-markdown | |
| 44 | pnpm add jsr:@clo/react-markdown | |
| 45 | ``` | |
| 46 | ||
| 47 | ```tsx | |
| 48 | import { Markdown } from "@clo/react-markdown"; | |
| 49 | import remarkGfm from "remark-gfm"; | |
| 50 | import remarkParse from "remark-parse"; | |
| 51 | import { 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. | |
| 60 | const processor = unified().use(remarkParse).use(remarkGfm); | |
| 61 | ||
| 62 | // Define custom components. (maps to rehypeReact's option) | |
| 63 | const components = { | |
| 64 | a: MarkdownLink, | |
| 65 | code: InlineCode, | |
| 66 | }; | |
| 67 | ||
| 68 | export 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 | ||
| 82 | If you want to play with the example project, which offers a comparison to | |
| 83 | Streamdown and the non-memoized `react-markdown` package. | |
| 84 | ||
| 85 | ``` | |
| 86 | git clone https://git.paperclover.net/clo/react-markdown | |
| 87 | cd react-markdown | |
| 88 | pnpm demo # auto-install | |
| 89 | ``` | |
| 90 | ||
| 91 | ## Architecture | |
| 92 | ||
| 93 | The `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 | ||
| 114 | All put together, basically nothing rerenders except what actually changed, and the document is beautiful. |