├── .editorconfig ├── .github └── workflows │ ├── bb.yml │ └── main.yml ├── .gitignore ├── .mailmap ├── .npmrc ├── .prettierignore ├── index.d.ts ├── index.js ├── lib └── index.js ├── license ├── package.json ├── readme.md ├── test.js └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.github/workflows/bb.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | main: 3 | runs-on: ubuntu-latest 4 | steps: 5 | - uses: unifiedjs/beep-boop-beta@main 6 | with: 7 | repo-token: ${{secrets.GITHUB_TOKEN}} 8 | name: bb 9 | on: 10 | issues: 11 | types: [closed, edited, labeled, opened, reopened, unlabeled] 12 | pull_request_target: 13 | types: [closed, edited, labeled, opened, reopened, unlabeled] 14 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | main: 3 | name: ${{matrix.node}} 4 | runs-on: ubuntu-latest 5 | steps: 6 | - uses: actions/checkout@v4 7 | - uses: actions/setup-node@v4 8 | with: 9 | node-version: ${{matrix.node}} 10 | - run: npm install 11 | - run: npm test 12 | - uses: codecov/codecov-action@v5 13 | strategy: 14 | matrix: 15 | node: 16 | - lts/hydrogen 17 | - node 18 | name: main 19 | on: 20 | - pull_request 21 | - push 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.d.ts 2 | *.log 3 | *.map 4 | *.tsbuildinfo 5 | .DS_Store 6 | coverage/ 7 | node_modules/ 8 | yarn.lock 9 | !/index.d.ts 10 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | rhysd Linda_pp 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-scripts=true 2 | package-lock=false 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | *.md 3 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import type {Root} from 'hast' 2 | import type {Plugin} from 'unified' 3 | import type {Options} from './lib/index.js' 4 | 5 | /** 6 | * Conditional type for a node object. 7 | */ 8 | // @ts-ignore: conditionally defined; 9 | // it used to be possible to detect that with `any extends X ? X : Y` 10 | // but no longer. 11 | type JsxElement = JSX.Element 12 | 13 | export type {Components, Options} from 'hast-util-to-jsx-runtime' 14 | 15 | /** 16 | * Turn HTML into preact, react, solid, svelte, vue, etc. 17 | * 18 | * ###### Result 19 | * 20 | * This plugin registers a compiler that returns a `JSX.Element` where 21 | * compilers typically return `string`. 22 | * When using `.stringify` on `unified`, the result is such a `JSX.Element`. 23 | * When using `.process` (or `.processSync`), the result is available at 24 | * `file.result`. 25 | * 26 | * ###### Frameworks 27 | * 28 | * There are differences between what JSX frameworks accept, such as whether 29 | * they accept `class` or `className`, or `background-color` or 30 | * `backgroundColor`. 31 | * 32 | * For hast elements transformed by this project, this is be handled through 33 | * options: 34 | * 35 | * | Framework | `elementAttributeNameCase` | `stylePropertyNameCase` | 36 | * | --------- | -------------------------- | ----------------------- | 37 | * | Preact | `'html'` | `'dom'` | 38 | * | React | `'react'` | `'dom'` | 39 | * | Solid | `'html'` | `'css'` | 40 | * | Vue | `'html'` | `'dom'` | 41 | * 42 | * @param options 43 | * Configuration (required). 44 | * @returns 45 | * Nothing. 46 | */ 47 | declare const rehypeReact: Plugin<[Options], Root, JsxElement> 48 | export default rehypeReact 49 | 50 | // Register the result type. 51 | declare module 'unified' { 52 | interface CompileResultMap { 53 | JsxElement: JsxElement 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Note: types exposed from `index.d.ts`. 2 | export {default} from './lib/index.js' 3 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @import {Options} from 'hast-util-to-jsx-runtime' 3 | * @import {Root} from 'hast' 4 | * @import {Compiler, Processor} from 'unified' 5 | */ 6 | 7 | import {toJsxRuntime} from 'hast-util-to-jsx-runtime' 8 | 9 | /** 10 | * Turn HTML into preact, react, solid, svelte, vue, etc. 11 | * 12 | * @param {Options} options 13 | * Configuration (required). 14 | * @returns {undefined} 15 | * Nothing. 16 | */ 17 | export default function rehypeReact(options) { 18 | // eslint-disable-next-line unicorn/no-this-assignment 19 | const self = /** @type {Processor} */ ( 20 | // @ts-expect-error: TypeScript doesn’t handle `this` well. 21 | this 22 | ) 23 | 24 | self.compiler = compiler 25 | 26 | /** @type {Compiler} */ 27 | function compiler(tree, file) { 28 | return toJsxRuntime(tree, {filePath: file.path, ...options}) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) rhysd 4 | Copyright (c) Mapbox 5 | Copyright (c) Titus Wormer 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "rhysd ", 3 | "bugs": "https://github.com/rehypejs/rehype-react/issues", 4 | "contributors": [ 5 | "Artem Sapegin ", 6 | "Christian Murphy ", 7 | "Ciaran Wood ", 8 | "David Clark ", 9 | "Jason Trill ", 10 | "Jeremy Stucki ", 11 | "Juho Vepsalainen ", 12 | "Takuya Matsuyama ", 13 | "Titus Wormer (https://wooorm.com)", 14 | "Tsuyusato Kitsune ", 15 | "Tucker Whitehouse ", 16 | "Tom MacWright ", 17 | "kthjm ", 18 | "rhysd " 19 | ], 20 | "dependencies": { 21 | "@types/hast": "^3.0.0", 22 | "hast-util-to-jsx-runtime": "^2.0.0", 23 | "unified": "^11.0.0" 24 | }, 25 | "description": "rehype plugin to transform to React", 26 | "devDependencies": { 27 | "@types/node": "^22.0.0", 28 | "@types/react": "^19.0.0", 29 | "@types/react-dom": "^19.0.0", 30 | "c8": "^10.0.0", 31 | "hastscript": "^9.0.0", 32 | "prettier": "^3.0.0", 33 | "react": "^19.0.0", 34 | "react-dom": "^19.0.0", 35 | "remark-cli": "^12.0.0", 36 | "remark-preset-wooorm": "^11.0.0", 37 | "type-coverage": "^2.0.0", 38 | "typescript": "^5.0.0", 39 | "xo": "^0.60.0" 40 | }, 41 | "exports": "./index.js", 42 | "files": [ 43 | "index.d.ts", 44 | "index.js", 45 | "lib/" 46 | ], 47 | "funding": { 48 | "type": "opencollective", 49 | "url": "https://opencollective.com/unified" 50 | }, 51 | "keywords": [ 52 | "hast", 53 | "html", 54 | "plugin", 55 | "preact", 56 | "react", 57 | "rehype-plugin", 58 | "rehype", 59 | "solid", 60 | "svelte", 61 | "unified", 62 | "vue" 63 | ], 64 | "license": "MIT", 65 | "name": "rehype-react", 66 | "prettier": { 67 | "bracketSpacing": false, 68 | "singleQuote": true, 69 | "semi": false, 70 | "tabWidth": 2, 71 | "trailingComma": "none", 72 | "useTabs": false 73 | }, 74 | "remarkConfig": { 75 | "plugins": [ 76 | "remark-preset-wooorm" 77 | ] 78 | }, 79 | "repository": "rehypejs/rehype-react", 80 | "scripts": { 81 | "build": "tsc --build --clean && tsc --build && type-coverage", 82 | "format": "remark --frail --output --quiet -- . && prettier --log-level warn --write -- . && xo --fix", 83 | "test-api": "node --conditions development test.js", 84 | "test-coverage": "c8 --100 --reporter lcov -- npm run test-api", 85 | "test": "npm run build && npm run format && npm run test-coverage" 86 | }, 87 | "sideEffects": false, 88 | "typeCoverage": { 89 | "atLeast": 100, 90 | "strict": true 91 | }, 92 | "type": "module", 93 | "version": "8.0.0", 94 | "xo": { 95 | "overrides": [ 96 | { 97 | "files": [ 98 | "**/*.d.ts" 99 | ], 100 | "rules": { 101 | "@typescript-eslint/array-type": [ 102 | "error", 103 | { 104 | "default": "generic" 105 | } 106 | ], 107 | "@typescript-eslint/ban-ts-comment": 0, 108 | "@typescript-eslint/ban-types": [ 109 | "error", 110 | { 111 | "extendDefaults": true 112 | } 113 | ], 114 | "@typescript-eslint/consistent-type-definitions": [ 115 | "error", 116 | "interface" 117 | ] 118 | } 119 | } 120 | ], 121 | "prettier": true 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # rehype-react 2 | 3 | [![Build][badge-build-image]][badge-build-url] 4 | [![Coverage][badge-coverage-image]][badge-coverage-url] 5 | [![Downloads][badge-downloads-image]][badge-downloads-url] 6 | [![Size][badge-size-image]][badge-size-url] 7 | 8 | **[rehype][github-rehype]** 9 | plugin to turn HTML into preact, react, solid, svelte, vue, etc. 10 | 11 | ## Contents 12 | 13 | * [What is this?](#what-is-this) 14 | * [When should I use this?](#when-should-i-use-this) 15 | * [Install](#install) 16 | * [Use](#use) 17 | * [API](#api) 18 | * [`unified().use(rehypeReact, options)`](#unifieduserehypereact-options) 19 | * [`Components`](#components) 20 | * [`Options`](#options) 21 | * [Types](#types) 22 | * [Compatibility](#compatibility) 23 | * [Security](#security) 24 | * [Related](#related) 25 | * [Contribute](#contribute) 26 | * [License](#license) 27 | 28 | ## What is this? 29 | 30 | This package is a [unified][github-unified] 31 | ([rehype][github-rehype]) 32 | plugin that compiles HTML (hast) to any JSX runtime 33 | (preact, react, solid, svelte, vue, etc). 34 | 35 | **unified** is a project that transforms content with abstract syntax trees 36 | (ASTs). 37 | **rehype** adds support for HTML to unified. 38 | **hast** is the HTML AST that rehype uses. 39 | This is a rehype plugin that adds a compiler to compile hast to a JSX runtime. 40 | 41 | ## When should I use this? 42 | 43 | This plugin adds a compiler for rehype, 44 | which means that it turns the final HTML (hast) syntax tree into something else 45 | (in this case `JSX.Element`). 46 | It’s useful when you’re already using unified (whether remark or rehype) or are 47 | open to learning about ASTs (they’re powerful!) and want to render content in 48 | your app. 49 | 50 | If you’re not familiar with unified, 51 | then [`react-markdown`][github-react-markdown] 52 | might be a better fit. 53 | You can also use [`react-remark`][github-react-remark] instead, 54 | which is somewhere between `rehype-react` and `react-markdown`, 55 | as it does more that the former and is more modern (such as supporting hooks) 56 | than the latter, 57 | and also a good alternative. 58 | If you want to use JavaScript and JSX *inside* markdown files, 59 | use [MDX][github-mdx]. 60 | 61 | ## Install 62 | 63 | This package is [ESM only][github-gist-esm]. 64 | In Node.js (version 16+), 65 | install with [npm][npmjs-install]: 66 | 67 | ```sh 68 | npm install rehype-react 69 | ``` 70 | 71 | In Deno with [`esm.sh`][esmsh]: 72 | 73 | ```js 74 | import rehypeReact from 'https://esm.sh/rehype-react@8' 75 | ``` 76 | 77 | In browsers with [`esm.sh`][esmsh]: 78 | 79 | ```html 80 | 83 | ``` 84 | 85 | ## Use 86 | 87 | Say our React app `example.js` looks as follows: 88 | 89 | ```js 90 | import {Fragment, createElement, useEffect, useState} from 'react' 91 | import production from 'react/jsx-runtime' 92 | import rehypeParse from 'rehype-parse' 93 | import rehypeReact from 'rehype-react' 94 | import {unified} from 'unified' 95 | 96 | const text = ` 97 |

Bonjour!

98 |

Mercure est la planète la plus proche du Soleil et la moins massive du Système solaire.

99 | ` 100 | 101 | export default function App() { 102 | return useProcessor(text) 103 | } 104 | 105 | 106 | /** 107 | * @param {string} text 108 | * @returns {React.JSX.Element} 109 | */ 110 | function useProcessor(text) { 111 | const [Content, setContent] = useState(createElement(Fragment)) 112 | 113 | useEffect( 114 | function () { 115 | ;(async function () { 116 | const file = await unified() 117 | .use(rehypeParse, {fragment: true}) 118 | .use(rehypeReact, production) 119 | .process(text) 120 | 121 | setContent(file.result) 122 | })() 123 | }, 124 | [text] 125 | ) 126 | 127 | return Content 128 | } 129 | ``` 130 | 131 | …running that in Next.js or similar, 132 | we’d get: 133 | 134 | ```html 135 |

Bonjour!

136 |

Mercure est la planète la plus proche du Soleil et la moins massive du Système solaire.

137 | ``` 138 | 139 | ## API 140 | 141 | This package exports no identifiers. 142 | The default export is [`rehypeReact`][api-rehype-react]. 143 | 144 | ### `unified().use(rehypeReact, options)` 145 | 146 | Turn HTML into preact, react, solid, svelte, vue, etc. 147 | 148 | ###### Parameters 149 | 150 | * `options` 151 | ([`Options`][api-options], 152 | required) 153 | — configuration 154 | 155 | ###### Returns 156 | 157 | Nothing (`undefined`). 158 | 159 | ###### Result 160 | 161 | This plugin registers a compiler that returns a `JSX.Element` where compilers 162 | typically return `string`. 163 | When using `.stringify` on `unified`, 164 | the result is such a `JSX.Element`. 165 | When using `.process` (or `.processSync`), 166 | the result is available at `file.result`. 167 | 168 | ###### Frameworks 169 | 170 | There are differences between what JSX frameworks accept, 171 | such as whether they accept `class` or `className`, 172 | or `background-color` or `backgroundColor`. 173 | 174 | For hast elements transformed by this project, 175 | this is be handled through options: 176 | 177 | | Framework | `elementAttributeNameCase` | `stylePropertyNameCase` | 178 | | --------- | -------------------------- | ----------------------- | 179 | | Preact | `'html'` | `'dom'` | 180 | | React | `'react'` | `'dom'` | 181 | | Solid | `'html'` | `'css'` | 182 | | Vue | `'html'` | `'dom'` | 183 | 184 | ### `Components` 185 | 186 | Possible components to use (TypeScript type). 187 | 188 | See [`Components` from 189 | `hast-util-to-jsx-runtime`](https://github.com/syntax-tree/hast-util-to-jsx-runtime#components) 190 | for more info. 191 | 192 | ### `Options` 193 | 194 | Configuration (TypeScript type). 195 | 196 | ###### Fields 197 | 198 | * `Fragment` 199 | ([`Fragment` from 200 | `hast-util-to-jsx-runtime`][github-hast-util-to-jsx-runtime-fragment], 201 | required) 202 | — fragment 203 | * `jsx` 204 | ([`Jsx` from 205 | `hast-util-to-jsx-runtime`][github-hast-util-to-jsx-runtime-jsx], 206 | required in production) 207 | — dynamic JSX 208 | * `jsxs` 209 | ([`Jsx` from 210 | `hast-util-to-jsx-runtime`][github-hast-util-to-jsx-runtime-jsx], 211 | required in production) 212 | — static JSX 213 | * `jsxDEV` 214 | ([`JsxDev` from 215 | `hast-util-to-jsx-runtime`](https://github.com/syntax-tree/hast-util-to-jsx-runtime#jsxdev), 216 | required in development) 217 | — development JSX 218 | * `components` 219 | ([`Partial`][api-components], optional) 220 | — components to use 221 | * `development` 222 | (`boolean`, default: `false`) 223 | — whether to use `jsxDEV` when on or `jsx` and `jsxs` when off 224 | * `elementAttributeNameCase` 225 | (`'html'` or `'react'`, default: `'react'`) 226 | — specify casing to use for attribute names 227 | * `passNode` 228 | (`boolean`, default: `false`) 229 | — pass the hast element node to components 230 | * `space` 231 | (`'html'` or `'svg'`, default: `'html'`) 232 | — whether `tree` is in the `'html'` or `'svg'` space, 233 | when an `` element is found in the HTML space, 234 | this package already automatically switches to and from the SVG space when 235 | entering and exiting it 236 | * `stylePropertyNameCase` 237 | (`'css'` or `'dom'`, default: `'dom'`) 238 | — specify casing to use for property names in `style` objects 239 | * `tableCellAlignToStyle` 240 | (`boolean`, default: `true`) 241 | — turn obsolete `align` props on `td` and `th` into CSS `style` props 242 | 243 | ## Types 244 | 245 | This package is fully typed with [TypeScript][]. 246 | It exports the additional types [`Components`][api-components] and 247 | [`Options`][api-options]. 248 | More advanced types are exposed from 249 | [`hast-util-to-jsx-runtime`][github-hast-util-to-jsx-runtime]. 250 | 251 | ## Compatibility 252 | 253 | Projects maintained by the unified collective are compatible with maintained 254 | versions of Node.js. 255 | 256 | When we cut a new major release, 257 | we drop support for unmaintained versions of Node. 258 | This means we try to keep the current release line, 259 | `rehype-react@8`, 260 | compatible with Node.js 17. 261 | 262 | This plugin works with `rehype-parse` version 3+, 263 | `rehype` version 4+, 264 | and `unified` version 9+, 265 | and React 18+. 266 | 267 | ## Security 268 | 269 | Use of `rehype-react` can open you up to a 270 | [cross-site scripting (XSS)][wikipedia-xss] 271 | attack if the tree is unsafe. 272 | Use [`rehype-sanitize`][github-rehype-sanitize] to make the tree safe. 273 | 274 | ## Related 275 | 276 | * [`remark-rehype`](https://github.com/remarkjs/remark-rehype) 277 | — turn markdown into HTML to support rehype 278 | * [`rehype-remark`](https://github.com/rehypejs/rehype-remark) 279 | — turn HTML into markdown to support remark 280 | * [`rehype-retext`](https://github.com/rehypejs/rehype-retext) 281 | — rehype plugin to support retext 282 | * [`rehype-sanitize`][github-rehype-sanitize] 283 | — sanitize HTML 284 | 285 | ## Contribute 286 | 287 | See [`contributing.md`][health-contributing] 288 | in 289 | [`rehypejs/.github`][health] 290 | for ways to get started. 291 | See 292 | [`support.md`][health-support] 293 | for ways to get help. 294 | 295 | This project has a [code of conduct][health-coc]. 296 | By interacting with this repository, 297 | organization, 298 | or community you agree to abide by its terms. 299 | 300 | ## License 301 | 302 | [MIT][file-license] © [Titus Wormer][wooorm], 303 | modified by [Tom MacWright][macwright], 304 | [Mapbox][], 305 | and [rhysd][]. 306 | 307 | 308 | 309 | [api-components]: #components 310 | 311 | [api-options]: #options 312 | 313 | [api-rehype-react]: #unifieduserehypereact-options 314 | 315 | [badge-build-image]: https://github.com/rehypejs/rehype-react/workflows/main/badge.svg 316 | 317 | [badge-build-url]: https://github.com/rehypejs/rehype-react/actions 318 | 319 | [badge-coverage-image]: https://img.shields.io/codecov/c/github/rehypejs/rehype-react.svg 320 | 321 | [badge-coverage-url]: https://codecov.io/github/rehypejs/rehype-react 322 | 323 | [badge-downloads-image]: https://img.shields.io/npm/dm/rehype-react.svg 324 | 325 | [badge-downloads-url]: https://www.npmjs.com/package/rehype-react 326 | 327 | [badge-size-image]: https://img.shields.io/bundlejs/size/rehype-react 328 | 329 | [badge-size-url]: https://bundlejs.com/?q=rehype-react 330 | 331 | [esmsh]: https://esm.sh 332 | 333 | [file-license]: license 334 | 335 | [github-gist-esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c 336 | 337 | [github-hast-util-to-jsx-runtime]: https://github.com/syntax-tree/hast-util-to-jsx-runtime 338 | 339 | [github-hast-util-to-jsx-runtime-fragment]: https://github.com/syntax-tree/hast-util-to-jsx-runtime#fragment 340 | 341 | [github-hast-util-to-jsx-runtime-jsx]: https://github.com/syntax-tree/hast-util-to-jsx-runtime#jsx 342 | 343 | [github-mdx]: https://github.com/mdx-js/mdx/ 344 | 345 | [github-react-markdown]: https://github.com/remarkjs/react-markdown 346 | 347 | [github-react-remark]: https://github.com/remarkjs/react-remark 348 | 349 | [github-rehype]: https://github.com/rehypejs/rehype 350 | 351 | [github-rehype-sanitize]: https://github.com/rehypejs/rehype-sanitize 352 | 353 | [github-unified]: https://github.com/unifiedjs/unified 354 | 355 | [health]: https://github.com/rehypejs/.github 356 | 357 | [health-coc]: https://github.com/rehypejs/.github/blob/main/code-of-conduct.md 358 | 359 | [health-contributing]: https://github.com/rehypejs/.github/blob/main/contributing.md 360 | 361 | [health-support]: https://github.com/rehypejs/.github/blob/main/support.md 362 | 363 | [macwright]: https://macwright.org 364 | 365 | [mapbox]: https://www.mapbox.com 366 | 367 | [npmjs-install]: https://docs.npmjs.com/cli/install 368 | 369 | [rhysd]: https://rhysd.github.io 370 | 371 | [typescript]: https://www.typescriptlang.org 372 | 373 | [wikipedia-xss]: https://en.wikipedia.org/wiki/Cross-site_scripting 374 | 375 | [wooorm]: https://wooorm.com 376 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert/strict' 2 | import test from 'node:test' 3 | import {h} from 'hastscript' 4 | import React from 'react' 5 | import * as development from 'react/jsx-dev-runtime' 6 | import * as production from 'react/jsx-runtime' 7 | import server from 'react-dom/server' 8 | import rehypeReact from 'rehype-react' 9 | import {unified} from 'unified' 10 | 11 | test('React ' + React.version, async function (t) { 12 | await t.test('should expose the public api', async function () { 13 | assert.deepEqual(Object.keys(await import('rehype-react')).sort(), [ 14 | 'default' 15 | ]) 16 | }) 17 | 18 | await t.test( 19 | 'should fail without `Fragment`, `jsx`, `jsxs`', 20 | async function () { 21 | assert.throws(function () { 22 | unified() 23 | // @ts-expect-error: check how the runtime handles missing `options`. 24 | .use(rehypeReact) 25 | .stringify(h(undefined, [h('p')])) 26 | }, /Expected `Fragment` in options/) 27 | } 28 | ) 29 | 30 | await t.test('should transform a root', async function () { 31 | assert.deepEqual( 32 | unified() 33 | .use(rehypeReact, production) 34 | .stringify(h(undefined, [h('p')])), 35 | React.createElement( 36 | React.Fragment, 37 | {}, 38 | React.createElement('p', {key: 'p-0'}) 39 | ) 40 | ) 41 | }) 42 | 43 | await t.test( 44 | 'should transform an element with properties', 45 | async function () { 46 | assert.deepEqual( 47 | unified() 48 | .use(rehypeReact, production) 49 | .stringify(h(undefined, [h('h1.main-heading', {dataFoo: 'bar'})])), 50 | React.createElement( 51 | React.Fragment, 52 | {}, 53 | React.createElement('h1', { 54 | className: 'main-heading', 55 | 'data-foo': 'bar', 56 | key: 'h1-0' 57 | }) 58 | ) 59 | ) 60 | } 61 | ) 62 | 63 | await t.test( 64 | 'should transform an element with a text node', 65 | async function () { 66 | assert.deepEqual( 67 | unified() 68 | .use(rehypeReact, production) 69 | .stringify(h(undefined, [h('p', 'baz')])), 70 | React.createElement( 71 | React.Fragment, 72 | {}, 73 | React.createElement('p', {key: 'p-0'}, 'baz') 74 | ) 75 | ) 76 | } 77 | ) 78 | 79 | await t.test( 80 | 'should transform an element with a child element', 81 | async function () { 82 | assert.deepEqual( 83 | unified() 84 | .use(rehypeReact, production) 85 | .stringify(h(undefined, [h('p', h('strong', 'qux'))])), 86 | React.createElement( 87 | React.Fragment, 88 | {}, 89 | React.createElement( 90 | 'p', 91 | {key: 'p-0'}, 92 | React.createElement('strong', {key: 'strong-0'}, 'qux') 93 | ) 94 | ) 95 | ) 96 | } 97 | ) 98 | 99 | await t.test( 100 | 'should transform an element with mixed contents', 101 | async function () { 102 | assert.deepEqual( 103 | unified() 104 | .use(rehypeReact, production) 105 | .stringify( 106 | h(undefined, [h('p', [h('em', 'qux'), ' foo ', h('i', 'bar')])]) 107 | ), 108 | React.createElement( 109 | React.Fragment, 110 | {}, 111 | React.createElement('p', {key: 'p-0'}, [ 112 | React.createElement('em', {key: 'em-0'}, 'qux'), 113 | ' foo ', 114 | React.createElement('i', {key: 'i-0'}, 'bar') 115 | ]) 116 | ) 117 | ) 118 | } 119 | ) 120 | 121 | await t.test('should skip `doctype`s', async function () { 122 | assert.deepEqual( 123 | unified() 124 | .use(rehypeReact, production) 125 | .stringify(h(undefined, [{type: 'doctype'}])), 126 | React.createElement(React.Fragment, {}) 127 | ) 128 | }) 129 | 130 | await t.test('should transform trees', async function () { 131 | assert.deepEqual( 132 | unified() 133 | .use(rehypeReact, production) 134 | .stringify( 135 | h(undefined, [ 136 | h('section', [ 137 | h('h1.main-heading', {dataFoo: 'bar'}, [h('span', 'baz')]) 138 | ]) 139 | ]) 140 | ), 141 | React.createElement( 142 | React.Fragment, 143 | {}, 144 | React.createElement( 145 | 'section', 146 | {key: 'section-0'}, 147 | React.createElement( 148 | 'h1', 149 | { 150 | key: 'h1-0', 151 | className: 'main-heading', 152 | 'data-foo': 'bar' 153 | }, 154 | React.createElement('span', {key: 'span-0'}, 'baz') 155 | ) 156 | ) 157 | ) 158 | ) 159 | }) 160 | 161 | await t.test('should support components', async function () { 162 | assert.deepEqual( 163 | server.renderToStaticMarkup( 164 | unified() 165 | .use(rehypeReact, { 166 | ...production, 167 | components: { 168 | /** 169 | * @param {React.HTMLAttributes} properties 170 | */ 171 | h1(properties) { 172 | return React.createElement('h2', properties) 173 | } 174 | } 175 | }) 176 | .stringify(h(undefined, [h('h1')])) 177 | ), 178 | '

' 179 | ) 180 | }) 181 | 182 | await t.test('should support `development: true`', async function () { 183 | assert.deepEqual( 184 | unified() 185 | .use(rehypeReact, {...development, development: true}) 186 | .stringify(h(undefined, [h('h1')])), 187 | React.createElement( 188 | React.Fragment, 189 | {}, 190 | React.createElement('h1', {key: 'h1-0'}) 191 | ) 192 | ) 193 | }) 194 | 195 | await t.test( 196 | 'should transform an element with align property', 197 | async function () { 198 | assert.deepEqual( 199 | unified() 200 | .use(rehypeReact, production) 201 | .stringify( 202 | h(undefined, [ 203 | h('table', {}, [h('thead', h('th', {align: 'right'}))]) 204 | ]) 205 | ), 206 | React.createElement( 207 | React.Fragment, 208 | {}, 209 | React.createElement( 210 | 'table', 211 | {key: 'table-0'}, 212 | React.createElement( 213 | 'thead', 214 | {key: 'thead-0'}, 215 | React.createElement('th', { 216 | style: {textAlign: 'right'}, 217 | key: 'th-0' 218 | }) 219 | ) 220 | ) 221 | ) 222 | ) 223 | } 224 | ) 225 | 226 | await t.test('should transform a table with whitespace', async function () { 227 | assert.deepEqual( 228 | unified() 229 | .use(rehypeReact, production) 230 | .stringify( 231 | h(undefined, [ 232 | h('table', {}, [ 233 | '\n ', 234 | h('tbody', {}, [ 235 | '\n ', 236 | h('tr', {}, [ 237 | '\n ', 238 | h('th', {}, ['\n ']), 239 | h('td', {}, ['\n ']) 240 | ]) 241 | ]) 242 | ]) 243 | ]) 244 | ), 245 | React.createElement( 246 | React.Fragment, 247 | {}, 248 | React.createElement( 249 | 'table', 250 | {key: 'table-0'}, 251 | React.createElement( 252 | 'tbody', 253 | {key: 'tbody-0'}, 254 | React.createElement('tr', {key: 'tr-0'}, [ 255 | React.createElement('th', {key: 'th-0'}, '\n '), 256 | React.createElement('td', {key: 'td-0'}, '\n ') 257 | ]) 258 | ) 259 | ) 260 | ) 261 | ) 262 | }) 263 | 264 | await t.test('should expose node from node prop', async function () { 265 | const headingNode = h('h1') 266 | 267 | const Component = function () { 268 | return 'x' 269 | } 270 | 271 | assert.deepEqual( 272 | unified() 273 | .use(rehypeReact, { 274 | ...production, 275 | components: {h1: Component}, 276 | passNode: true 277 | }) 278 | .stringify(h(undefined, [headingNode, h('p')])), 279 | React.createElement(React.Fragment, {}, [ 280 | React.createElement(Component, {key: 'h1-0', node: headingNode}), 281 | React.createElement('p', {key: 'p-0'}) 282 | ]) 283 | ) 284 | }) 285 | 286 | await t.test( 287 | 'should respect `tableCellAlignToStyle: false`', 288 | async function () { 289 | assert.deepEqual( 290 | unified() 291 | .use(rehypeReact, {...production, tableCellAlignToStyle: false}) 292 | .stringify( 293 | h(undefined, [h('tr', {}, [h('th'), h('td', {align: 'center'})])]) 294 | ), 295 | React.createElement( 296 | React.Fragment, 297 | {}, 298 | React.createElement('tr', {key: 'tr-0'}, [ 299 | React.createElement('th', {key: 'th-0'}), 300 | React.createElement('td', {key: 'td-0', align: 'center'}) 301 | ]) 302 | ) 303 | ) 304 | } 305 | ) 306 | }) 307 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "checkJs": true, 4 | "customConditions": ["development"], 5 | "declarationMap": true, 6 | "declaration": true, 7 | "emitDeclarationOnly": true, 8 | "exactOptionalPropertyTypes": true, 9 | "lib": ["es2022"], 10 | "module": "node16", 11 | // To do: sadly, `@types/react-dom` is broken. 12 | "skipLibCheck": true, 13 | "strict": true, 14 | "target": "es2022" 15 | }, 16 | "exclude": ["coverage/", "node_modules/"], 17 | "include": ["**/*.js", "index.d.ts"] 18 | } 19 | --------------------------------------------------------------------------------