From 8e7f246a0e706c2fcdf48be112d652492568b74d Mon Sep 17 00:00:00 2001 From: clover caruso Date: Thu, 19 Mar 2026 02:53:02 -0700 Subject: [PATCH] feat: initial demo with project goals --- .gitignore | 4 + README.md | 67 + example/App.tsx | 87 + example/index.css | 27 + example/main.tsx | 22 + example/markdown-components.tsx | 149 + example/markdown-demo.ts | 7 + example/vite-env.d.ts | 13 + index.html | 12 + package.json | 59 + pnpm-lock.yaml | 4623 +++++++++++++++++++++++++++++++ src/Markdown.d.ts | 15 + src/Markdown.tsx | 46 + src/Memoizer.d.ts | 12 + src/Memoizer.ts | 188 ++ src/mod.d.ts | 1 + src/mod.ts | 1 + src/strings.d.ts | 1 + src/strings.ts | 5 + tests/index.test.ts | 76 + tsconfig.json | 24 + vite.config.ts | 23 + 22 files changed, 5462 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 example/App.tsx create mode 100644 example/index.css create mode 100644 example/main.tsx create mode 100644 example/markdown-components.tsx create mode 100644 example/markdown-demo.ts create mode 100644 example/vite-env.d.ts create mode 100644 index.html create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 src/Markdown.d.ts create mode 100644 src/Markdown.tsx create mode 100644 src/Memoizer.d.ts create mode 100644 src/Memoizer.ts create mode 100644 src/mod.d.ts create mode 100644 src/mod.ts create mode 100644 src/strings.d.ts create mode 100644 src/strings.ts create mode 100644 tests/index.test.ts create mode 100644 tsconfig.json create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..75352116828e61ece4575fecef317792d8d3ab58 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules +dist +*.log +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..ad9645a578faf85284f331a3003f0aba80746a3b --- /dev/null +++ b/README.md @@ -0,0 +1,67 @@ +# `@clo/react-markdown` + +This package exports a React component to render markdown using the [unified] +ecosystem's markdown tools ([remark], [rehype]). The exported `` +component is extremely memoized, making it suitable for streaming situations +such as LLM chat interfaces. + +- **Bring your Existing Pipeline**: The primary option for configuration is + providing a `unified.Processor` with or without the `rehypeReact` plugin. + Additionally, you can override the element renderers with the `components` + attribute. By default, `@clo/react-markdown` applies a bare bones config so you + can get started with just ``. Configuration is done + either with a provider, or right at the component level. +- **Handle Partial Markdown**: When the `predict` prop is set, sequences like + `hello **world` will be emitted as `hello world`. + +The motivation for this package is to have an easy to understand version of +[Streamdown]. With me banning all Vercel software across the company I work at, +we needed a simple, trustable solution for this problem. Some other differences +with their library: + +- **Better Memoization**: All inline components will preserve their state, even + as adjacent content changes. This is done to preserve remounts for things like + custom `` tags or other components. (For example, if a custom `` fetches + previewing data and provides a hover card, that card won't flicker). +- **Not Drop-In Replacement**: This APIs is new, and targets most use cases with + a small migration. +- **Headless UI**: No built in styles or components, bring your own CSS to blend + your markdown with your existing theme. `@clo/react-markdown` simply takes in your + existing `unified` pipeline and works off of that. +- **Easy to Audit**: Just a few hundred lines of highly commented code + +[Streamdown]: https://streamdown.ai +[unified]: https://unifiedjs.com/ +[remark]: https://remark.js.org/ + +## Getting Started + +```tsx +import { Markdown } from "@clo/react-markdown"; +import remarkGfm from "remark-gfm"; +import remarkParse from "remark-parse"; +import { unified } from "unified"; + +// Define any unified processing pipeline. We expect that your +// app already has one of these. +const processor = unified().use(remarkParse).use(remarkGfm); + +// Define custom components. (maps to rehypeReact's option) +const components = { + a: MarkdownLink, + code: InlineCode, +}; + +export function HelloWorld() { + return ( + + ); +} +``` diff --git a/example/App.tsx b/example/App.tsx new file mode 100644 index 0000000000000000000000000000000000000000..9cd27a6d7cff082dd2f235789a03e594b688759f --- /dev/null +++ b/example/App.tsx @@ -0,0 +1,87 @@ +import { useDeferredValue, useState } from "react"; +import { Streamdown } from "streamdown"; +import { Markdown } from "../src/mod"; +import { components, streamdownComponents } from "./markdown-components"; +import { initialMarkdown, processor } from "./markdown-demo"; + +type Renderer = "memo" | "streamdown"; +const rendererOptions: Array<{ label: string; value: Renderer }> = [ + { label: "MemoMarkdown", value: "memo" }, + { label: "Streamdown", value: "streamdown" }, +]; + +function getRendererButtonClass(isActive: boolean) { + return isActive + ? "rounded bg-white px-2.5 py-1 text-xs font-medium text-stone-900 shadow-sm" + : "rounded px-2.5 py-1 text-xs text-stone-600 hover:text-stone-900"; +} + +export function App() { + const [content, setContent] = useState(initialMarkdown); + const deferredContent = useDeferredValue(content); + const [renderer, setRenderer] = useState("memo"); + + return ( +
+
+
+
+

Source

+ +
+