A simple, headless React streaming markdown renderer.
pnpm add @opticlm/streamup react react-domimport { Streamup } from '@opticlm/streamup'
// Basic
<Streamup>{'# Hello **world**'}</Streamup>
// Streaming (incomplete markdown)
<Streamup streaming>{'**bold text without clos'}</Streamup>
// Custom components
<Streamup
components={{
h1: ({ children }) => <h1 className="text-4xl font-bold">{children}</h1>,
p: ({ children }) => <p className="my-4 leading-relaxed">{children}</p>,
code: ({ children, className, ...props }) => {
const isBlock = 'data-block' in props
if (isBlock) {
return <pre className="bg-zinc-900 p-4 rounded"><code>{children}</code></pre>
}
return <code className="bg-zinc-100 px-1 rounded">{children}</code>
},
}}
>
{markdown}
</Streamup>All parsing is always enabled:
- GFM: tables, strikethrough, task lists, autolinks
- Math: block equtions
- CJK: proper emphasis, strikethrough, and autolink handling for Chinese/Japanese/Korean text
- HTML: raw HTML with sanitization
- Footnotes: reference and definition syntax
Math syntax is always parsed. To render it with KaTeX, add the plugin:
pnpm add katex rehype-kateximport { Streamup } from '@opticlm/streamup'
import { katex } from '@opticlm/streamup/katex'
import 'katex/dist/katex.min.css'
// Default options
<Streamup plugins={[katex()]}>{'$$E = mc^2$$'}</Streamup>
// Custom KaTeX options
<Streamup plugins={[katex({ errorColor: '#ff0000', strict: false })]}>
{'$$E = mc^2$$'}
</Streamup>pnpm add mermaidimport { Streamup } from '@opticlm/streamup'
import { withMermaid } from '@opticlm/streamup/mermaid'
// Default
<Streamup components={withMermaid()}>
{'```mermaid\ngraph TD\n A-->B\n```'}
</Streamup>
// With mermaid config (theme, flowchart direction, etc.)
<Streamup components={withMermaid({ config: { theme: 'dark' } })}>
{'```mermaid\ngraph TD\n A-->B\n```'}
</Streamup>
// With custom fallback code component
<Streamup components={withMermaid({
config: { theme: 'forest' },
fallbackCode: MyCodeBlock,
})}>
{markdown}
</Streamup>Or use the renderer directly:
import { MermaidRenderer } from '@opticlm/streamup/mermaid'
<MermaidRenderer code="graph TD; A-->B" config={{ theme: 'dark' }} />By default, only $$...$$ block math is enabled. Enable $...$ inline math:
<Streamup singleDollarTextMath>{markdown}</Streamup>Transform or remove URLs before rendering:
<Streamup urlTransform={(url) => url.replace('http:', 'https:')}>
{markdown}
</Streamup>
// Remove all links
<Streamup urlTransform={() => null}>{markdown}</Streamup>Filter which elements are allowed to render. Returning false removes the element but keeps its children:
<Streamup allowElement={(el) => el.tagName !== 'img'}>
{markdown}
</Streamup>Replace the default sanitization schema. Spread defaultSanitizeSchema to extend it:
import { Streamup, defaultSanitizeSchema } from '@opticlm/streamup'
<Streamup sanitizeSchema={{
...defaultSanitizeSchema,
attributes: {
...defaultSanitizeSchema.attributes,
div: ['className', 'style'],
},
}}>
{markdown}
</Streamup>Add custom remark or rehype plugins via the plugin system:
import type { StreamupPlugin } from '@opticlm/streamup'
import myRemarkPlugin from 'remark-my-plugin'
import myRehypePlugin from 'rehype-my-plugin'
const myPlugin: StreamupPlugin = {
remarkPlugins: [myRemarkPlugin],
rehypePlugins: [[myRehypePlugin, { option: true }]],
}
<Streamup plugins={[myPlugin]}>{markdown}</Streamup>import { remend, parseMarkdownIntoBlocks, processMarkdown } from '@opticlm/streamup'
// Heal incomplete markdown (streaming)
remend('**bold') // '**bold**'
remend('~~strike') // '~~strike~~'
remend('[link](http') // '[link](streamup:incomplete-link)'
// Split markdown into blocks for incremental rendering
parseMarkdownIntoBlocks('# Title\n\nParagraph')
// ['# Title\n\n', 'Paragraph\n']
// Low-level: process markdown to React elements
processMarkdown('# Hello', {
components: myComponents,
processorOptions: { singleDollarTextMath: true },
urlTransform: (url) => url,
})| Prop | Type | Default | Description |
|---|---|---|---|
children |
string |
'' |
Markdown content |
streaming |
boolean |
false |
Heal incomplete markdown before rendering |
components |
Partial<Components> |
unstyled HTML | Override rendering for any element |
plugins |
StreamupPlugin[] |
[] |
Add remark/rehype plugins (e.g. katex()) |
className |
string |
— | Class on the wrapper <div> |
singleDollarTextMath |
boolean |
false |
Enable $...$ inline math syntax |
sanitizeSchema |
SanitizeSchema |
built-in | Custom HTML sanitization schema |
urlTransform |
UrlTransform |
— | Transform or remove URLs |
allowElement |
AllowElement |
— | Filter elements (return false to remove) |
GFM task lists are rendered with class names from remark-gfm:
ul.contains-task-list— the list containerli.task-list-item— each task item (contains an<input type="checkbox" disabled>)
ul.contains-task-list {
list-style: none;
padding-left: 0;
}
li.task-list-item {
display: flex;
align-items: baseline;
gap: 0.5em;
}Every HTML element can be overridden. Each component receives its standard HTML props plus node (the HAST element).
Common overrides: h1–h6, p, a, img, code, pre, blockquote, table, thead, tbody, tr, th, td, ol, ul, li, hr, strong, em, del, sup, sub.
MIT