diff --git a/README.md b/README.md
index b9bd974..fa86544 100644
--- a/README.md
+++ b/README.md
@@ -1,31 +1,143 @@
# react-render-to-markdown
+[](https://www.npmjs.com/package/react-render-to-markdown)
+[](https://github.com/SoonIter/react-render-to-markdown/blob/main/LICENSE)
+
+Render React components to Markdown strings — like `renderToString` in `react-dom`, but outputs **Markdown** instead of HTML.
+
+Built on top of `react-reconciler`, this library creates a custom React renderer that traverses the React element tree and produces well-formatted Markdown. It follows **SSR-like behavior**: `useEffect`, `useLayoutEffect`, and `useInsertionEffect` are suppressed (as no-ops), while `useState`, `useMemo`, `useRef`, `useContext`, and other synchronous hooks work as expected.
+
+## Installation
+
+The major version of `react-render-to-markdown` follows the React version. Install the one that matches your project:
+
+```bash
+# React 19
+npm install react-render-to-markdown@19
+
+# React 18
+npm install react-render-to-markdown@18
+```
+
+## Quick Start
+
```tsx
import { renderToMarkdownString } from 'react-render-to-markdown';
-const markdown = renderToMarkdownString(
Hello, World!
);
+const markdown = await renderToMarkdownString(Hello, World!
);
console.log(markdown); // # Hello, World!
```
-## Installation
+## Usage
-```bash
-npm install react-render-to-markdown
+### Basic HTML Elements
+
+```tsx
+import { renderToMarkdownString } from 'react-render-to-markdown';
+
+await renderToMarkdownString(
+
+ foo
+ bar
+
,
+);
+// Output: '**foo**bar'
```
+### React Components & Hooks
+
+Synchronous hooks (`useState`, `useMemo`, `useRef`, `useContext`, etc.) work as expected. Client-side effects (`useEffect`, `useLayoutEffect`) are automatically suppressed:
+
+```tsx
+import { createContext, useContext, useMemo, useState } from 'react';
+import { renderToMarkdownString } from 'react-render-to-markdown';
+
+const ThemeContext = createContext('light');
+
+const Article = () => {
+ const [count] = useState(42);
+ const theme = useContext(ThemeContext);
+ const doubled = useMemo(() => count * 2, [count]);
+
+ return (
+ <>
+ Hello World
+ Count: {count}, Doubled: {doubled}, Theme: {theme}
+ >
+ );
+};
+
+await renderToMarkdownString(
+
+
+ ,
+);
+// Output:
+// # Hello World
+//
+// Count: 42, Doubled: 84, Theme: dark
+```
+
+### Code Blocks
+
+Fenced code blocks with language and title support:
+
+```tsx
+await renderToMarkdownString(
+
+ {'const a = 1;\n'}
+
,
+);
+// Output:
+// ```ts title=rspress.config.ts
+// const a = 1;
+// ```
+```
+
+For languages that may contain triple backticks (like `markdown`, `mdx`, `md`), four backticks (``````) are automatically used as delimiters.
+
+## Supported Elements
+
+| HTML Element | Markdown Output |
+| --- | --- |
+| `` – `` | `#` – `######` headings |
+| `
` | Paragraph with trailing newlines |
+| ``, `` | `**bold**` |
+| ``, `` | `*italic*` |
+| `` | `` `inline code` `` |
+| `` + `` | Fenced code block (` ``` `) |
+| `` | `[text](url)` |
+| `
` | `` |
+| ``, ``, `- ` | Unordered / ordered lists |
+| `
` | `> blockquote` |
+| `
` | Line break |
+| `
` | `---` horizontal rule |
+| `
+ Content
+ ,
+ ),
+ ).toMatchInlineSnapshot(`
+ "# Title
+
+ Content
+
+ "
+ `);
+ });
+
it('renders two row correctly', async () => {
const Comp1 = () => {
return (
diff --git a/src/react/render.ts b/src/react/render.ts
index 1ade16c..8d50d05 100644
--- a/src/react/render.ts
+++ b/src/react/render.ts
@@ -113,46 +113,47 @@ function toMarkdown(root: MarkdownNode): string {
const { type, props, children } = root;
// Get children's Markdown
- const childrenMd = children
- .map((child) => {
- if (child instanceof TextNode) {
- return child.text;
- }
- return toMarkdown(child);
- })
- .join('');
+ const childrenMd = () =>
+ children
+ .map((child) => {
+ if (child instanceof TextNode) {
+ return child.text;
+ }
+ return toMarkdown(child);
+ })
+ .join('');
// Generate corresponding Markdown based on element type
switch (type) {
case 'root':
- return childrenMd;
+ return childrenMd();
case 'h1':
- return `# ${childrenMd}\n\n`;
+ return `# ${childrenMd()}\n\n`;
case 'h2':
- return `## ${childrenMd}\n\n`;
+ return `## ${childrenMd()}\n\n`;
case 'h3':
- return `### ${childrenMd}\n\n`;
+ return `### ${childrenMd()}\n\n`;
case 'h4':
- return `#### ${childrenMd}\n\n`;
+ return `#### ${childrenMd()}\n\n`;
case 'h5':
- return `##### ${childrenMd}\n\n`;
+ return `##### ${childrenMd()}\n\n`;
case 'h6':
- return `###### ${childrenMd}\n\n`;
+ return `###### ${childrenMd()}\n\n`;
case 'p':
- return `${childrenMd}\n\n`;
+ return `${childrenMd()}\n\n`;
case 'strong':
case 'b':
- return `**${childrenMd}**`;
+ return `**${childrenMd()}**`;
case 'em':
case 'i':
- return `*${childrenMd}*`;
+ return `*${childrenMd()}*`;
case 'code':
// When is nested inside , it represents the code block body,
// so we must not wrap it with inline backticks (would create nested fences).
if (root.parent?.type === 'pre') {
- return childrenMd;
+ return childrenMd();
}
- return `\`${childrenMd}\``;
+ return `\`${childrenMd()}\``;
case 'pre': {
const _language =
props['data-lang'] || props.language || props.lang || '';
@@ -163,33 +164,35 @@ function toMarkdown(root: MarkdownNode): string {
? '````'
: '```';
- return `\n${block}${language}${title ? ` title=${title}` : ''}\n${childrenMd}\n${block}\n`;
+ return `\n${block}${language}${title ? ` title=${title}` : ''}\n${childrenMd()}\n${block}\n`;
}
case 'a':
- return `[${childrenMd}](${props.href || '#'})`;
+ return `[${childrenMd()}](${props.href || '#'})`;
case 'img':
return ``;
case 'ul':
- return `${childrenMd}\n`;
+ return `${childrenMd()}\n`;
case 'ol':
- return `${childrenMd}\n`;
+ return `${childrenMd()}\n`;
case 'li': {
const isOrdered = root.parent && root.parent.type === 'ol';
const prefix = isOrdered ? '1. ' : '- ';
- return `${prefix}${childrenMd}\n`;
+ return `${prefix}${childrenMd()}\n`;
}
case 'blockquote':
- return `> ${childrenMd.split('\n').join('\n> ')}\n\n`;
+ return `> ${childrenMd().split('\n').join('\n> ')}\n\n`;
case 'br':
return '\n';
case 'hr':
return '---\n\n';
+ case 'style':
+ return '';
case 'table':
- return `${childrenMd}\n`;
+ return `${childrenMd()}\n`;
case 'thead':
- return childrenMd;
+ return childrenMd();
case 'tbody':
- return childrenMd;
+ return childrenMd();
case 'tr': {
const cells = children
.filter((child): child is MarkdownNode => child instanceof MarkdownNode)
@@ -205,9 +208,9 @@ function toMarkdown(root: MarkdownNode): string {
}
case 'th':
case 'td':
- return childrenMd;
+ return childrenMd();
default:
- return childrenMd;
+ return childrenMd();
}
}