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 +[![npm version](https://img.shields.io/npm/v/react-render-to-markdown.svg)](https://www.npmjs.com/package/react-render-to-markdown) +[![license](https://img.shields.io/npm/l/react-render-to-markdown.svg)](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)` |
+| `` | `![alt](src)` |
+| `
    `, `
      `, `
    1. ` | 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 `![${props.alt || ''}](${props.src || ''})`;
           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();
         }
       }