├── .editorconfig ├── .github └── workflows │ ├── bb.yml │ └── main.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── changelog.md ├── index.js ├── lib └── index.js ├── license ├── package.json ├── readme.md ├── script └── load-jsx.js ├── test.jsx └── 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 | react-markdown.min.js 9 | yarn.lock 10 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-scripts=true 2 | package-lock=false 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | *.md 3 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Changelog 4 | 5 | All notable changes will be documented in this file. 6 | 7 | ## 10.0.0 - 2025-02-20 8 | 9 | * [`aaaa40b`](https://github.com/remarkjs/react-markdown/commit/aaaa40b) 10 | Remove support for `className` prop 11 | **migrate**: see “Remove `className`” below 12 | 13 | ### Remove `className` 14 | 15 | The `className` prop was removed. 16 | If you want to add classes to some element that wraps the markdown 17 | you can explicitly write that element and add the class to it. 18 | You can then choose yourself which tag name to use and whether to add other 19 | props. 20 | 21 | Before: 22 | 23 | ```js 24 | {markdown} 25 | ``` 26 | 27 | After: 28 | 29 | ```js 30 |
31 | {markdown} 32 |
33 | ``` 34 | 35 | ## 9.1.0 - 2025-02-20 36 | 37 | * [`6ce120e`](https://github.com/remarkjs/react-markdown/commit/6ce120e) 38 | Add support for async plugins 39 | 40 | ## 9.0.3 - 2025-01-06 41 | 42 | (same as 9.0.2 but now with d.ts files) 43 | 44 | ## 9.0.2 - 2025-01-06 45 | 46 | * [`b151a90`](https://github.com/remarkjs/react-markdown/commit/b151a90) 47 | Fix types for React 19 48 | * [`6962af7`](https://github.com/remarkjs/react-markdown/commit/6962af7) 49 | Add declaration maps 50 | * [`aa5933b`](https://github.com/remarkjs/react-markdown/commit/aa5933b) 51 | Refactor to use `@import` to import types 52 | 53 | ## 9.0.1 - 2023-11-13 54 | 55 | * [`d8e3787`](https://github.com/remarkjs/react-markdown/commit/d8e3787) 56 | Fix double encoding in new url transform 57 | 58 | ## 9.0.0 - 2023-09-27 59 | 60 | * [`b67d714`](https://github.com/remarkjs/react-markdown/commit/b67d714) 61 | Change to require Node.js 16\ 62 | **migrate**: update too 63 | * [`ec2b134`](https://github.com/remarkjs/react-markdown/commit/ec2b134) 64 | Change to require React 18\ 65 | **migrate**: update too 66 | * [`bf5824f`](https://github.com/remarkjs/react-markdown/commit/bf5824f) 67 | Change to use `exports`\ 68 | **migrate**: don’t use private APIs 69 | * [`c383a45`](https://github.com/remarkjs/react-markdown/commit/c383a45) 70 | Update `@types/hast`, utilities, plugins, etc\ 71 | **migrate**: update too 72 | * [`eca5e6b`](https://github.com/remarkjs/react-markdown/commit/eca5e6b) 73 | [`08ead9e`](https://github.com/remarkjs/react-markdown/commit/08ead9e) 74 | Replace `transformImageUri`, `transformLinkUri` w/ `urlTransform`\ 75 | **migrate**: see “Add `urlTransform`” below 76 | * [`de29396`](https://github.com/remarkjs/react-markdown/commit/de29396) 77 | Remove `linkTarget` option\ 78 | **migrate**: see “Remove `linkTarget`” below 79 | * [`4346276`](https://github.com/remarkjs/react-markdown/commit/4346276) 80 | Remove support for passing custom props to components\ 81 | **migrate**: see “Remove `includeElementIndex`”, “Remove `rawSourcePos`”, 82 | “Remove `sourcePos`”, “Remove extra props passed to certain components” 83 | below 84 | * [`c0dfbd6`](https://github.com/remarkjs/react-markdown/commit/c0dfbd6) 85 | Remove UMD bundle from package\ 86 | **migrate**: use `esm.sh` or a CDN or so 87 | * [`e12b5e9`](https://github.com/remarkjs/react-markdown/commit/e12b5e9) 88 | Remove `prop-types`\ 89 | **migrate**: use TypeScript 90 | * [`4eb7aa0`](https://github.com/remarkjs/react-markdown/commit/4eb7aa0) 91 | Change to throw errors for removed props\ 92 | **migrate**: don’t pass options that don’t do things 93 | * [`8aabf74`](https://github.com/remarkjs/react-markdown/commit/8aabf74) 94 | Change to improve error messages\ 95 | **migrate**: expect better messages 96 | 97 | ### Add `urlTransform` 98 | 99 | The `transformImageUri` and `transformLinkUri` were removed. 100 | Having two functions is a bit much, particularly because there are more URLs 101 | you might want to change (or which might be unsafe so *we* make them safe). 102 | And their name and APIs were a bit weird. 103 | You can use the new `urlTransform` prop instead to change all your URLs. 104 | 105 | ### Remove `linkTarget` 106 | 107 | The `linkTarget` option was removed; you should likely not set targets. 108 | If you want to, use 109 | [`rehype-external-links`](https://github.com/rehypejs/rehype-external-links). 110 | 111 | ### Remove `includeElementIndex` 112 | 113 | The `includeElementIndex` option was removed, so `index` is never passed to 114 | components. 115 | Write a plugin to pass `index`: 116 | 117 |
118 | Show example of plugin 119 | 120 | ```js 121 | import {visit} from 'unist-util-visit' 122 | 123 | function rehypePluginAddingIndex() { 124 | /** 125 | * @param {import('hast').Root} tree 126 | * @returns {undefined} 127 | */ 128 | return function (tree) { 129 | visit(tree, function (node, index) { 130 | if (node.type === 'element' && typeof index === 'number') { 131 | node.properties.index = index 132 | } 133 | }) 134 | } 135 | } 136 | ``` 137 | 138 |
139 | 140 | ### Remove `rawSourcePos` 141 | 142 | The `rawSourcePos` option was removed, so `sourcePos` is never passed to 143 | components. 144 | All components are passed `node`, so you can get `node.position` from them. 145 | 146 | ### Remove `sourcePos` 147 | 148 | The `sourcePos` option was removed, so `data-sourcepos` is never passed to 149 | elements. 150 | Write a plugin to pass `index`: 151 | 152 |
153 | Show example of plugin 154 | 155 | ```js 156 | import {stringifyPosition} from 'unist-util-stringify-position' 157 | import {visit} from 'unist-util-visit' 158 | 159 | function rehypePluginAddingIndex() { 160 | /** 161 | * @param {import('hast').Root} tree 162 | * @returns {undefined} 163 | */ 164 | return function (tree) { 165 | visit(tree, function (node) { 166 | if (node.type === 'element') { 167 | node.properties.dataSourcepos = stringifyPosition(node.position) 168 | } 169 | }) 170 | } 171 | } 172 | ``` 173 | 174 |
175 | 176 | ### Remove extra props passed to certain components 177 | 178 | When overwriting components, these props are no longer passed: 179 | 180 | * `inline` on `code` 181 | — create a plugin or use `pre` for the block 182 | * `level` on `h1`, `h2`, `h3`, `h4`, `h5`, `h6` 183 | — check `node.tagName` instead 184 | * `checked` on `li` 185 | — check `task-list-item` class or check `props.children` 186 | * `index` on `li` 187 | — create a plugin 188 | * `ordered` on `li` 189 | — create a plugin or check the parent 190 | * `depth` on `ol`, `ul` 191 | — create a plugin 192 | * `ordered` on `ol`, `ul` 193 | — check `node.tagName` instead 194 | * `isHeader` on `td`, `th` 195 | — check `node.tagName` instead 196 | * `isHeader` on `tr` 197 | — create a plugin or check children 198 | 199 | ## 8.0.7 - 2023-04-12 200 | 201 | * [`c289176`](https://github.com/remarkjs/react-markdown/commit/c289176) 202 | Fix performance for keys 203 | by [**@wooorm**](https://github.com/wooorm) 204 | in [#738](https://github.com/remarkjs/react-markdown/pull/738) 205 | * [`9034dbd`](https://github.com/remarkjs/react-markdown/commit/9034dbd) 206 | Fix types in syntax highlight example 207 | by [**@dlqqq**](https://github.com/dlqqq) 208 | in [#736](https://github.com/remarkjs/react-markdown/pull/736) 209 | 210 | **Full Changelog**: 211 | 212 | ## 8.0.6 - 2023-03-20 213 | 214 | * [`33ab015`](https://github.com/remarkjs/react-markdown/commit/33ab015) 215 | Update to TS 5\ 216 | by [**@Methuselah96**](https://github.com/Methuselah96) 217 | in [#734](https://github.com/remarkjs/react-markdown/issues/734) 218 | 219 | ## 8.0.5 - 2023-01-17 220 | 221 | * [`d640d40`](https://github.com/remarkjs/react-markdown/commit/d640d40) 222 | Update to use `node16` module resolution in `tsconfig.json`\ 223 | by [**@ChristianMurphy**](https://github.com/ChristianMurphy) 224 | in [#723](https://github.com/remarkjs/react-markdown/pull/723) 225 | * [`402fea3`](https://github.com/remarkjs/react-markdown/commit/402fea3) 226 | Fix typo in `plugins` deprecation message\ 227 | by [**@marc2332**](https://github.com/marc2332) 228 | in [#719](https://github.com/remarkjs/react-markdown/pull/719) 229 | * [`4f98f73`](https://github.com/remarkjs/react-markdown/commit/4f98f73) 230 | Remove deprecated and unneeded `defaultProps`\ 231 | by [**@Lepozepo**](https://github.com/Lepozepo) 232 | in [#718](https://github.com/remarkjs/react-markdown/pull/718) 233 | 234 | ## 8.0.4 - 2022-12-01 235 | 236 | * [`9b20440`](https://github.com/remarkjs/react-markdown/commit/9b20440) 237 | Fix type of `td`, `th` props\ 238 | by [**@lucasassisrosa**](https://github.com/lucasassisrosa) 239 | in [#714](https://github.com/remarkjs/react-markdown/pull/714) 240 | * [`cfe075b`](https://github.com/remarkjs/react-markdown/commit/cfe075b) 241 | Add clarification of `alt` on `img` in docs\ 242 | by [**@cballenar**](https://github.com/cballenar) 243 | in [#692](https://github.com/remarkjs/react-markdown/pull/692) 244 | 245 | ## 8.0.3 - 2022-04-20 246 | 247 | * [`a2fb833`](https://github.com/remarkjs/react-markdown/commit/a2fb833) 248 | Fix prop types of plugins\ 249 | by [**@starpit**](https://github.com/starpit) 250 | in [#683](https://github.com/remarkjs/react-markdown/pull/683) 251 | 252 | ## 8.0.2 - 2022-03-31 253 | 254 | * [`2712227`](https://github.com/remarkjs/react-markdown/commit/2712227) 255 | Update `react-is` 256 | * [`704c3c6`](https://github.com/remarkjs/react-markdown/commit/704c3c6) 257 | Fix TypeScript bug by adding workaround\ 258 | by [**@Methuselah96**](https://github.com/Methuselah96) 259 | in [#676](https://github.com/remarkjs/react-markdown/pull/676) 260 | 261 | ## 8.0.1 - 2022-03-14 262 | 263 | * [`c23ecf6`](https://github.com/remarkjs/react-markdown/commit/c23ecf6) 264 | Add missing dependency for types\ 265 | by [**@Methuselah96**](https://github.com/Methuselah96) 266 | in [#675](https://github.com/remarkjs/react-markdown/pull/675) 267 | 268 | ## 8.0.0 - 2022-01-17 269 | 270 | 271 | 272 | * [`cd845c9`](https://github.com/remarkjs/react-markdown/commit/cd845c9) 273 | Remove deprecated `plugins` option\ 274 | (**migrate by renaming it to `remarkPlugins`**) 275 | * [`36e4916`](https://github.com/remarkjs/react-markdown/commit/36e4916) 276 | Update [`remark-rehype`](https://github.com/remarkjs/remark-rehype), 277 | add support for passing it options\ 278 | by [**@peolic**](https://github.com/peolic) 279 | in [#669](https://github.com/remarkjs/react-markdown/pull/669)\ 280 | (**migrate by removing `remark-footnotes` and updating `remark-gfm` if you 281 | were using them, otherwise you shouldn’t notice this**) 282 | 283 | ## 7.1.2 - 2022-01-02 284 | 285 | * [`656a4fa`](https://github.com/remarkjs/react-markdown/commit/656a4fa) 286 | Fix `ref` in types\ 287 | by [**@ChristianMurphy**](https://github.com/ChristianMurphy) 288 | in [#668](https://github.com/remarkjs/react-markdown/pull/668) 289 | 290 | ## 7.1.1 - 2021-11-29 291 | 292 | * [`4185f06`](https://github.com/remarkjs/react-markdown/commit/4185f06) 293 | Add improved docs\ 294 | by [**@wooorm**](https://github.com/wooorm) 295 | in [#657](https://github.com/remarkjs/react-markdown/pull/657) 296 | 297 | ## 7.1.0 - 2021-10-21 298 | 299 | * [`7b8a829`](https://github.com/remarkjs/react-markdown/commit/7b8a829) 300 | Add support for `SpecialComponents` to be any `ComponentType`\ 301 | by [**@Methuselah96**](https://github.com/Methuselah96) 302 | in [#640](https://github.com/remarkjs/react-markdown/pull/640) 303 | * [`a7c26fc`](https://github.com/remarkjs/react-markdown/commit/a7c26fc) 304 | Remove warning on whitespace in tables 305 | 306 | ## 7.0.1 - 2021-08-26 307 | 308 | * [`ec387c2`](https://github.com/remarkjs/react-markdown/commit/ec387c2) 309 | Add improved type for `linkTarget` as string 310 | * [`5af6bc7`](https://github.com/remarkjs/react-markdown/commit/5af6bc7) 311 | Fix to correctly compile intrinsic types 312 | 313 | ## 7.0.0 - 2021-08-13 314 | 315 | Welcome to version 7. 316 | This a major release and therefore contains breaking changes. 317 | 318 | ### Breaking changes 319 | 320 | * [`01b11fe`](https://github.com/remarkjs/react-markdown/commit/01b11fe) 321 | [`c613efd`](https://github.com/remarkjs/react-markdown/commit/c613efd) 322 | [`a1e1c3f`](https://github.com/remarkjs/react-markdown/commit/a1e1c3f) 323 | [`aeee9ac`](https://github.com/remarkjs/react-markdown/commit/aeee9ac) 324 | Use ESM 325 | (please [read this](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c)) 326 | * [`3dffd6a`](https://github.com/remarkjs/react-markdown/commit/3dffd6a) 327 | Update dependencies 328 | (upgrade all your plugins and this should go fine) 329 | 330 | ### Internals 331 | 332 | * [`8b5481c`](https://github.com/remarkjs/react-markdown/commit/8b5481c) 333 | [`fb1b512`](https://github.com/remarkjs/react-markdown/commit/fb1b512) 334 | [`144af79`](https://github.com/remarkjs/react-markdown/commit/144af79) 335 | Replace `jest` with `uvu` 336 | * [`8c572df`](https://github.com/remarkjs/react-markdown/commit/8c572df) 337 | Replace `rollup` with `esbuild` 338 | * [`8737eac`](https://github.com/remarkjs/react-markdown/commit/8737eac) 339 | [`28d4c75`](https://github.com/remarkjs/react-markdown/commit/28d4c75) 340 | [`b2dd046`](https://github.com/remarkjs/react-markdown/commit/b2dd046) 341 | Refactor code-style 342 | 343 | ## 6.0.3 - 2021-07-30 344 | 345 | * [`13367ed`](https://github.com/remarkjs/react-markdown/commit/13367ed) 346 | Fix types to include each element w/ its properties 347 | * [`0a1931a`](https://github.com/remarkjs/react-markdown/commit/0a1931a) 348 | Fix to add min version of `property-information` 349 | 350 | ## 6.0.2 - 2021-05-06 351 | 352 | * [`cefc02d`](https://github.com/remarkjs/react-markdown/commit/cefc02d) 353 | Add string type for `className`s 354 | * [`6355e45`](https://github.com/remarkjs/react-markdown/commit/6355e45) 355 | Fix to pass `vfile` to plugins 356 | * [`5cf6e1b`](https://github.com/remarkjs/react-markdown/commit/5cf6e1b) 357 | Fix to add warning when non-strings are given as `children` 358 | 359 | ## 6.0.1 - 2021-04-23 360 | 361 | * [`2e956be`](https://github.com/remarkjs/react-markdown/commit/2e956be) 362 | Fix whitespace in table elements 363 | * [`d36048a`](https://github.com/remarkjs/react-markdown/commit/d36048a) 364 | Add architecture section to readme 365 | 366 | ## 6.0.0 - 2021-04-15 367 | 368 | Welcome to version 6. 369 | This a major release and therefore contains breaking changes. 370 | 371 | ### Change `renderers` to `components` 372 | 373 | `react-markdown` used to let you define components for *markdown* constructs 374 | (`link`, `delete`, `break`, etc). 375 | This proved complex as users didn’t know about those names or markdown 376 | peculiarities (such as that there are fully formed links *and* link references). 377 | 378 | See [**GH-549**](https://github.com/remarkjs/react-markdown/issues/549) for more 379 | on why this changed. 380 | See [**Appendix B: Components** in 381 | `readme.md`](https://github.com/remarkjs/react-markdown#appendix-b-components) 382 | for more on components. 383 | 384 |
385 | Show example of needed change 386 | 387 | Before (**broken**): 388 | 389 | ```js 390 | 394 | }} 395 | >{`***`} 396 | ``` 397 | 398 | Now (**fixed**): 399 | 400 | ```js 401 | 405 | }} 406 | >{`***`} 407 | ``` 408 | 409 |
410 | 411 |
412 | Show conversion table 413 | 414 | | Type (`renderers`) | Tag names (`components`) | 415 | | ----------------------------------- | --------------------------------------- | 416 | | `blockquote` | `blockquote` | 417 | | `break` | `br` | 418 | | `code`, `inlineCode` | `code`, `pre`**​\*​** | 419 | | `definition` | **†** | 420 | | `delete` | `del`**‡** | 421 | | `emphasis` | `em` | 422 | | `heading` | `h1`, `h2`, `h3`, `h4`, `h5`, `h6`**§** | 423 | | `html`, `parsedHtml`, `virtualHtml` | **‖** | 424 | | `image`, `imageReference` | `img`**†** | 425 | | `link`, `linkReference` | `a`**†** | 426 | | `list` | `ol`, `ul`**¶** | 427 | | `listItem` | `li` | 428 | | `paragraph` | `p` | 429 | | `root` | **​\*\*​** | 430 | | `strong` | `strong` | 431 | | `table` | `table`**‡** | 432 | | `tableHead` | `thead`**‡** | 433 | | `tableBody` | `tbody`**‡** | 434 | | `tableRow` | `tr`**‡** | 435 | | `tableCell` | `td`, `th`**‡** | 436 | | `text` | | 437 | | `thematicBreak` | `hr` | 438 | 439 | * **​\*​** It’s possible to differentiate between code based on the `inline` 440 | prop. 441 | Block code is also wrapped in a `pre` 442 | * **†** Resource (`[text](url)`) and reference (`[text][id]`) style links and 443 | images (and their definitions) are now resolved and treated the same 444 | * **‡** Available when using 445 | [`remark-gfm`](https://github.com/remarkjs/remark-gfm) 446 | * **§** It’s possible to differentiate between heading based on the `level` 447 | prop 448 | * **‖** When using `rehype-raw` (see below), components for those elements 449 | can also be used (for example, `abbr` for 450 | `HTML`) 451 | * **¶** It’s possible to differentiate between lists based on the `ordered` 452 | prop 453 | * **​\*\*​** Wrap `ReactMarkdown` in a component instead 454 | 455 |
456 | 457 | ### Add `rehypePlugins` 458 | 459 | We’ve added another plugin system: 460 | [**rehype**](https://github.com/rehypejs/rehype). 461 | It’s similar to remark (what we’re using for markdown) but for HTML. 462 | 463 | There are many rehype plugins. 464 | Some examples are 465 | [`@mapbox/rehype-prism`](https://github.com/mapbox/rehype-prism) 466 | (syntax highlighting with Prism), 467 | [`rehype-katex`](https://github.com/remarkjs/remark-math/tree/HEAD/packages/rehype-katex) 468 | (rendering math with KaTeX), or 469 | [`rehype-autolink-headings`](https://github.com/rehypejs/rehype-autolink-headings) 470 | (adding links to headings). 471 | 472 | See [List of plugins](https://github.com/rehypejs/rehype/blob/main/doc/plugins.md) 473 | for more plugins. 474 | 475 |
476 | Show example of feature 477 | 478 | ```js 479 | import rehypeHighlight from 'rehype-highlight' 480 | 481 | {`~~~js 482 | console.log(1) 483 | ~~~`} 484 | ``` 485 | 486 |
487 | 488 | ### Remove buggy HTML in markdown parser 489 | 490 | In a lot of cases, you should not use HTML in markdown: it’s most always unsafe. 491 | And it defeats much of the purpose of this project (not relying on 492 | `dangerouslySetInnerHTML`). 493 | 494 | `react-markdown` used to have an opt-in HTML parser with a bunch of bugs. 495 | As we now support rehype plugins, we can defer that work to a rehype plugin. 496 | To support HTML in markdown with `react-markdown`, use 497 | [`rehype-raw`](https://github.com/rehypejs/rehype-raw). 498 | The `astPlugins` and `allowDangerousHtml` (previously called `escapeHtml`) props 499 | are no longer needed and were removed. 500 | 501 | When using `rehype-raw`, you should probably use 502 | [`rehype-sanitize`](https://github.com/rehypejs/rehype-sanitize) 503 | too. 504 | 505 |
506 | Show example of needed change 507 | 508 | Before (**broken**): 509 | 510 | ```js 511 | import MarkdownWithHtml from 'react-markdown/with-html' 512 | 513 | {`# Hello, world!`} 514 | ``` 515 | 516 | Now (**fixed**): 517 | 518 | ```js 519 | import Markdown from 'react-markdown' 520 | import rehypeRaw from 'rehype-raw' 521 | import rehypeSanitize from 'rehype-sanitize' 522 | 523 | {`# Hello, world!`} 524 | ``` 525 | 526 |
527 | 528 | ### Change `source` to `children` 529 | 530 | Instead of passing a `source` pass `children` instead: 531 | 532 |
533 | Show example of needed change 534 | 535 | Before (**broken**): 536 | 537 | ```js 538 | 539 | ``` 540 | 541 | Now (**fixed**): 542 | 543 | ```js 544 | {`some 545 | markdown`} 546 | ``` 547 | 548 | Or (**also fixed**): 549 | 550 | ```js 551 | 553 | ``` 554 | 555 |
556 | 557 | ### Replace `allowNode`, `allowedTypes`, and `disallowedTypes` 558 | 559 | Similar to the `renderers` to `components` change, the filtering options 560 | also changed from being based on markdown names towards being based on HTML 561 | names: `allowNode` to `allowElement`, `allowedTypes` to `allowedElements`, and 562 | `disallowedTypes` to `disallowedElements`. 563 | 564 |
565 | Show example of needed change 566 | 567 | Before (**broken**): 568 | 569 | ```js 570 | {`![alt text](./image.url)`} 574 | ``` 575 | 576 | Now (**fixed**): 577 | 578 | ```js 579 | {`![alt text](./image.url)`} 583 | ``` 584 | 585 | *** 586 | 587 | Before (**broken**): 588 | 589 | ```js 590 | node.type !== 'heading' || node.depth !== 1} 593 | >{`# main heading`} 594 | ``` 595 | 596 | Now (**fixed**): 597 | 598 | ```js 599 | element.tagName !== 'h1'} 602 | >{`# main heading`} 603 | ``` 604 | 605 |
606 | 607 | ### Change `includeNodeIndex` to `includeElementIndex` 608 | 609 | Similar to the `renderers` to `components` change, this option to pass more info 610 | to components also changed from being based on markdown to being based on HTML. 611 | 612 |
613 | Show example of needed change 614 | 615 | Before (**broken**): 616 | 617 | ```js 618 | 622 | }} 623 | >{`Some text`} 624 | ``` 625 | 626 | Now (**fixed**): 627 | 628 | ```js 629 | 633 | }} 634 | >{`Some text`} 635 | ``` 636 | 637 |
638 | 639 | ### Change signature of `transformLinkUri`, `linkTarget` 640 | 641 | The second parameter of these functions (to rewrite `href` on `a` or to define 642 | `target` on `a`) are now [hast](https://github.com/syntax-tree/hast) (HTML AST) 643 | instead of [mdast](https://github.com/syntax-tree/mdast) (markdown AST). 644 | 645 | ### Change signature of `transformImageUri` 646 | 647 | The second parameter of this function was always `undefined` and the fourth was 648 | the `alt` (`string`) on the image. 649 | The second parameter is now that `alt`. 650 | 651 | ### Remove support for React 15, IE11 652 | 653 | We now use ES2015 (such as `Object.assign`) and removed certain hacks to work 654 | with React 15 and older. 655 | 656 | ## 5.0.3 - 2020-10-23 657 | 658 | * [`bb0bdde`](https://github.com/remarkjs/react-markdown/commit/bb0bdde) 659 | Unlock peer dependency on React to allow v17 660 | * [`24e42bd`](https://github.com/remarkjs/react-markdown/commit/24e42bd) 661 | Fix exception on missing element from `html-to-react` 662 | * [`3d363e9`](https://github.com/remarkjs/react-markdown/commit/3d363e9) 663 | Fix umd browser build 664 | 665 | ## 5.0.2 - 2020-10-23 666 | 667 | * [`4dadaba`](https://github.com/remarkjs/react-markdown/commit/4dadaba) 668 | Fix to allow combining `allowedTypes`, `unwrapDisallowed` in types 669 | 670 | ## 5.0.1 - 2020-10-21 671 | 672 | * [`c3dc5ee`](https://github.com/remarkjs/react-markdown/commit/c3dc5ee) 673 | Fix to not crash on empty text nodes 674 | 675 | ## 5.0.0 - 2020-10-19 676 | 677 | ### BREAKING 678 | 679 | #### Maintained by [unified](https://unifiedjs.com) 680 | 681 | This project is now maintained by the unified collective, which also houses the 682 | underlying tools used in `react-markdown`: hundreds of projects for working with 683 | markdown and markup related things (including MDX). 684 | We have cleaned the project: updated dependencies, improved 685 | docs/tests/coverage/types, cleaned the issue tracker, and fixed a couple of 686 | bugs, but otherwise *much should be the same*. 687 | 688 | #### Upgrade `remark-parse` 689 | 690 | The parser used in `react-markdown` has been upgraded to the latest version. 691 | It is now 100% CommonMark compliant: that means it works the same as in other 692 | places, such as Discourse, Reddit, Stack Overflow, and GitHub. 693 | Note that GitHub does extend CommonMark: to match how Markdown works on GitHub, 694 | use the [`remark-gfm`](https://github.com/remarkjs/remark-gfm) plugin. 695 | 696 | * [`remark-parse@9.0.0`](https://github.com/remarkjs/remark/releases/tag/remark-parse%409.0.0) 697 | * [`remark-parse@8.0.0`](https://github.com/remarkjs/remark/releases/tag/remark-parse%408.0.0) 698 | * [`remark-parse@7.0.0`](https://github.com/remarkjs/remark/releases/tag/remark-parse%407.0.0) 699 | * [`remark-parse@6.0.0`](https://github.com/remarkjs/remark/releases/tag/remark-parse%406.0.0) 700 | 701 | #### New serializer property: `node` 702 | 703 | A new `node` prop is passed to all non-tag/non-fragment renderers. 704 | This contains the raw [mdast](https://github.com/syntax-tree/mdast) AST node, 705 | which opens up a number of interesting possibilities. 706 | The breaking change is for renderers which blindly spread their props to an 707 | underlying component/tag. 708 | For instance: 709 | 710 | ```js 711 | }} … /> 712 | ``` 713 | 714 | Should now be written as: 715 | 716 | ```js 717 | }} … /> 718 | ``` 719 | 720 | #### List/list item `tight` property replaced by `spread` 721 | 722 | Previously, the `tight` property would hint as to whether or not list items 723 | should be wrapped in paragraphs. 724 | This logic has now been replaced by a new `spread` property, which behaves 725 | slightly differently. 726 | [Read more](https://github.com/remarkjs/remark/pull/364). 727 | 728 | ## 4.3.1 - 2020-01-05 729 | 730 | ### Fixes 731 | 732 | * (Typings) Fix incorrect typescript definitions (Peng Guanwen) 733 | 734 | ## 4.3.0 - 2020-01-02 735 | 736 | ### Fixes 737 | 738 | * (Typings) Add typings for `react-markdown/html-parser` (Peng Guanwen) 739 | 740 | ## 4.2.2 - 2019-09-03 741 | 742 | ### Fixes 743 | 744 | * (Typings) Inline `RemarkParseOptions` for now (Espen Hovlandsdal) 745 | 746 | ## 4.2.1 - 2019-09-01 747 | 748 | ### Fixes 749 | 750 | * (Typings) Fix incorrect import - `RemarkParseOptions` (Jakub Chrzanowski) 751 | 752 | ## 4.2.0 - 2019-09-01 753 | 754 | ### Added 755 | 756 | * Add support for plugins that use AST transformations (Frankie Ali) 757 | 758 | ### Fixes 759 | 760 | * (Typings) Add `parserOptions` to type definitions (Ted Piotrowski) 761 | * Allow renderer to be any React element type (Nathan Bierema) 762 | 763 | ## 4.1.0 - 2019-06-24 764 | 765 | ### Added 766 | 767 | * Add prop `parserOptions` to specify options for remark-parse (Kelvin Chan) 768 | 769 | ## 4.0.9 - 2019-06-22 770 | 771 | ### Fixes 772 | 773 | * (Typings) Make transformLinkUri & transformImageUri actually nullable 774 | (Florentin Luca Rieger) 775 | 776 | ## 4.0.8 - 2019-04-14 777 | 778 | ### Fixes 779 | 780 | * Fix HTML parsing of elements with a single child vs. multiple children 781 | (Nicolas Venegas) 782 | 783 | ## 4.0.7 - 2019-04-14 784 | 785 | ### Fixes 786 | 787 | * Fix matching of replaced non-void elements in HTML parser plugin (Nicolas 788 | Venegas) 789 | * Fix HTML parsing of multiple void elements (Nicolas Venegas) 790 | * Fix void element children invariant violation (Nicolas Venegas) 791 | 792 | ## 4.0.6 - 2019-01-04 793 | 794 | ### Fixes 795 | 796 | * Mitigate regex ddos by upgrading html-to-react (Christoph Werner) 797 | * Update typings to allow arbitrary node types (Jesse Pinho) 798 | * Readme: Add note about only parsing plugins working (Vincent Tunru) 799 | 800 | ## 4.0.4 - 2018-11-30 801 | 802 | ### Changed 803 | 804 | * Upgrade dependencies (Espen Hovlandsdal) 805 | 806 | ## 4.0.3 - 2018-10-11 807 | 808 | ### Fixes 809 | 810 | * Output paragraph element for last item in loose list (Jeremy Moseley) 811 | 812 | ## 4.0.2 - 2018-10-05 813 | 814 | ### Fixes 815 | 816 | * Fix text rendering in React versions lower than or equal to 15 (Espen 817 | Hovlandsdal) 818 | 819 | ## 4.0.1 - 2018-10-03 820 | 821 | ### Fixes 822 | 823 | * \[TypeScript] Fix TypeScript index signature for renderers (Linus Unnebäck) 824 | 825 | ## 4.0.0 - 2018-10-03 826 | 827 | ### BREAKING 828 | 829 | * `text` is now a first-class node + renderer 830 | — if you are using `allowedNodes`, it needs to be included in this list. 831 | Since it is now a React component, it will be passed an object of props 832 | instead of the old approach where a string was passed. 833 | `children` will contain the actual text string. 834 | * On React >= 16.2, if no `className` prop is provided, a fragment will be 835 | used instead of a div. 836 | To always render a div, pass `'div'` as the `root` renderer. 837 | * On React >= 16.2, escaped HTML will no longer be rendered with div/span 838 | containers 839 | * The UMD bundle now exports the component as `window.ReactMarkdown` instead 840 | of `window.reactMarkdown` 841 | 842 | ### Added 843 | 844 | * HTML parser plugin for full HTML compatibility (Espen Hovlandsdal) 845 | 846 | ### Fixes 847 | 848 | * URI transformer allows uppercase http/https URLs (Liam Kennedy) 849 | * \[TypeScript] Strongly type the keys of `renderers` (Linus Unnebäck) 850 | 851 | ## 3.6.0 - 2018-09-05 852 | 853 | ### Added 854 | 855 | * Add support for passing index info to renderers (Beau Roberts) 856 | 857 | ## 3.5.0 - 2018-09-03 858 | 859 | ### Added 860 | 861 | * Allow specifying `target` attribute for links (Marshall Smith) 862 | 863 | ## 3.4.1 - 2018-07-25 864 | 865 | ### Fixes 866 | 867 | * Bump dependency for mdast-add-list-metadata as it was using ES6 features 868 | (Espen Hovlandsdal) 869 | 870 | ## 3.4.0 - 2018-07-25 871 | 872 | ### Added 873 | 874 | * Add more metadata props to list and listItem (André Staltz) 875 | * list: `depth` 876 | * listItem: `ordered`, `index` 877 | 878 | ### Fixes 879 | 880 | * Make `source` property optional in typescript definition (gRoberts84) 881 | 882 | ## 3.3.4 - 2018-06-19 883 | 884 | ### Fixes 885 | 886 | * Fix bug where rendering empty link references (`[][]`) would fail (Dennis S) 887 | 888 | ## 3.3.3 - 2018-06-14 889 | 890 | ### Fixes 891 | 892 | * Fix bug where unwrapping certain disallowed nodes would fail (Petr Gazarov) 893 | 894 | ## 3.3.2 - 2018-05-07 895 | 896 | ### Changes 897 | 898 | * Add `rawSourcePos` property for passing structured source position info to 899 | renderers (Espen Hovlandsdal) 900 | 901 | ## 3.3.1 - 2018-05-07 902 | 903 | ### Changes 904 | 905 | * Pass properties of unknown nodes directly to renderer (Jesse Pinho) 906 | * Update TypeScript definition and prop types (ClassicDarkChocolate) 907 | 908 | ## 3.3.0 - 2018-03-06 909 | 910 | ### Added 911 | 912 | * Add support for fragment renderers (Benjamim Sonntag) 913 | 914 | ## 3.2.2 - 2018-02-26 915 | 916 | ### Fixes 917 | 918 | * Fix language escaping in code blocks (Espen Hovlandsdal) 919 | 920 | ## 3.2.1 - 2018-02-21 921 | 922 | ### Fixes 923 | 924 | * Pass the React key into an overridden text renderer (vanchagreen) 925 | 926 | ## 3.2.0 - 2018-02-12 927 | 928 | ### Added 929 | 930 | * Allow overriding text renderer (Thibaud Courtoison) 931 | 932 | ## 3.1.5 - 2018-02-03 933 | 934 | ### Fixes 935 | 936 | * Only use first language from code block (Espen Hovlandsdal) 937 | 938 | ## 3.1.4 - 2017-12-30 939 | 940 | ### Fixes 941 | 942 | * Enable transformImageUri for image references (evoye) 943 | 944 | ## 3.1.3 - 2017-12-16 945 | 946 | ### Fixes 947 | 948 | * Exclude babel config from npm package (Espen Hovlandsdal) 949 | 950 | ## 3.1.2 - 2017-12-16 951 | 952 | ### Fixes 953 | 954 | * Fixed partial table exception (Alexander Wong) 955 | 956 | ## 3.1.1 - 2017-12-11 957 | 958 | ### Fixes 959 | 960 | * Add readOnly property to checkboxes (Phil Rajchgot) 961 | 962 | ## 3.1.0 - 2017-11-30 963 | 964 | ### Added 965 | 966 | * Support for checkbox lists (Espen Hovlandsdal) 967 | 968 | ### Fixes 969 | 970 | * Better typings (Igor Kamyshev) 971 | 972 | ## 3.0.1 - 2017-11-21 973 | 974 | ### Added 975 | 976 | * *Experimental* support for plugins (Espen Hovlandsdal) 977 | 978 | ### Changes 979 | 980 | * Provide more arguments to `transformLinkUri`/`transformImageUri` (children, 981 | title, alt) (mudrz) 982 | 983 | ## 3.0.0 - 2017-11-20 984 | 985 | ### Notes 986 | 987 | * **FULL REWRITE**. 988 | Changed parser from CommonMark to Markdown. 989 | Big, breaking changes. 990 | See *BREAKING* below. 991 | 992 | ### Added 993 | 994 | * Table support! 995 | * New types: `table`, `tableHead`, `tableBody`, `tableRow`, `tableCell` 996 | * New type: `delete` (`~~foo~~`) 997 | * New type: `imageReference` 998 | * New type: `linkReference` 999 | * New type: `definition` 1000 | * Hacky, but basic support for React-native rendering of attributeless HTML 1001 | nodes (``, ``, etc) 1002 | 1003 | ### BREAKING 1004 | 1005 | * Container props removed (`containerTagName`, `containerProps`), override 1006 | `root` renderer instead 1007 | * `softBreak` option removed. 1008 | New solution will be added at some point in the future. 1009 | * `escapeHtml` is now TRUE by default 1010 | * `HtmlInline`/`HtmlBlock` are now named `html` (use `isBlock` prop to check\ 1011 | if inline or block) 1012 | * Renderer names are camelcased and in certain cases, renamed. 1013 | For instance: 1014 | * `Emph` => `emphasis` 1015 | * `Item` => `listItem` 1016 | * `Code` => `inlineCode` 1017 | * `CodeBlock` => `code` 1018 | * `linebreak`/`hardbreak` => `break` 1019 | * All renderers: `literal` prop is now called `value`\* List renderer: `type` 1020 | prop is now a boolean named `ordered` (`Bullet` => `false`, `Ordered` => 1021 | `true`) 1022 | * `walker` prop removed. 1023 | Code depending on this will have to be rewritten to use the `astPlugins` 1024 | prop, which functions differently. 1025 | * `allowNode` has new arguments (node, index, parent) 1026 | — node has different props, see renderer props 1027 | * `childBefore` and `childAfter` props removed. 1028 | Use `root` renderer instead. 1029 | * `parserOptions` removed (new parser, so the old options doesn’t make sense 1030 | anymore) 1031 | 1032 | ## 2.5.1 - 2017-11-11 1033 | 1034 | ### Changes 1035 | 1036 | * Fix `
` not having a node key (Alex Zaworski) 1037 | 1038 | ## 2.5.0 - 2017-04-10 1039 | 1040 | ### Changes 1041 | 1042 | * Fix deprecations for React v15.5 (Renée Kooi) 1043 | 1044 | ## 2.4.6 - 2017-03-14 1045 | 1046 | ### Changes 1047 | 1048 | * Fix too strict TypeScript definition (Rasmus Eneman) 1049 | * Update JSON-loader info in readme to match webpack 2 (Robin Wieruch) 1050 | 1051 | ### Added 1052 | 1053 | * Add ability to pass options to the CommonMark parser (Evan Hensleigh) 1054 | 1055 | ## 2.4.4 - 2017-01-16 1056 | 1057 | ### Changes 1058 | 1059 | * Fixed TypeScript definitions (Kohei Asai) 1060 | 1061 | ## 2.4.3 - 2017-01-12 1062 | 1063 | ### Added 1064 | 1065 | * Added TypeScript definitions (Ibragimov Ruslan) 1066 | 1067 | ## 2.4.2 - 2016-07-09 1068 | 1069 | ### Added 1070 | 1071 | * Added UMD-build (`umd/react-markdown.js`) (Espen Hovlandsdal) 1072 | 1073 | ## 2.4.1 - 2016-07-09 1074 | 1075 | ### Changes 1076 | 1077 | * Update `commonmark-react-renderer`, fixing a bug with missing nodes 1078 | (Espen Hovlandsdal) 1079 | 1080 | ## 2.4.0 - 2016-07-09 1081 | 1082 | ### Changes 1083 | 1084 | * Plain DOM-node renderers are now given only their respective props. 1085 | Fixes warnings when using React >= 15.2 (Espen Hovlandsdal) 1086 | 1087 | ### Added 1088 | 1089 | * New `transformImageUri` option allows you to transform URIs for images 1090 | (Petri Lehtinen) 1091 | 1092 | ## 2.3.0 - 2016-06-06 1093 | 1094 | ## Added 1095 | 1096 | * The `walker` instance is now passed to the `walker` callback function 1097 | (Riku Rouvila) 1098 | 1099 | ## 2.2.0 - 2016-04-20 1100 | 1101 | * Add `childBefore`/`childAfter` options (Thomas Lindstrøm) 1102 | 1103 | ## 2.1.1 - 2016-03-25 1104 | 1105 | * Add `containerProps` option (Thomas Lindstrøm) 1106 | 1107 | ## 2.1.0 - 2016-03-12 1108 | 1109 | ### Changes 1110 | 1111 | * Join sibling text nodes into one text node (Espen Hovlandsdal) 1112 | 1113 | ## 2.0.1 - 2016-02-21 1114 | 1115 | ### Changed 1116 | 1117 | * Update `commonmark-react-renderer` dependency to latest version to add keys 1118 | to all elements and simplify custom renderers 1119 | 1120 | ## 2.0.0 - 2016-02-21 1121 | 1122 | ### Changed 1123 | 1124 | * **Breaking change**: The renderer now requires Node 0.14 or higher. 1125 | This is because the renderer uses stateless components internally. 1126 | * **Breaking change**: `allowNode` now receives different properties in the 1127 | options argument. 1128 | See `README.md` for more details. 1129 | * **Breaking change**: CommonMark has changed some type names. 1130 | `Html` is now `HtmlInline`, `Header` is now `Heading` and `HorizontalRule` 1131 | is now `ThematicBreak`. 1132 | This affects the `allowedTypes` and `disallowedTypes` options. 1133 | * **Breaking change**: A bug in the `allowedTypes`/`disallowedTypes` and 1134 | `allowNode` options made them only applicable to certain types. 1135 | In this version, all types are filtered, as expected. 1136 | * **Breaking change**: Link URIs are now filtered through an XSS-filter by 1137 | default, prefixing “dangerous” protocols such as `javascript:` with `x-` 1138 | (eg: `javascript:alert('foo')` turns into `x-javascript:alert('foo')`). 1139 | This can be overridden with the `transformLinkUri`-option. 1140 | Pass `null` to disable the feature or a custom function to replace the 1141 | built-in behaviour. 1142 | 1143 | ### Added 1144 | 1145 | * New `renderers` option allows you to customize which React component should 1146 | be used for rendering given types. 1147 | See `README.md` for more details. 1148 | (Espen Hovlandsdal / Guillaume Plique) 1149 | * New `unwrapDisallowed` option allows you to select if the contents of a 1150 | disallowed node should be “unwrapped” (placed into the disallowed node 1151 | position). 1152 | For instance, setting this option to true and disallowing a link would still 1153 | render the text of the link, instead of the whole link node and all it’s 1154 | children disappearing. 1155 | (Espen Hovlandsdal) 1156 | * New `transformLinkUri` option allows you to transform URIs in links. 1157 | By default, an XSS-filter is used, but you could also use this for use cases 1158 | like transforming absolute to relative URLs, or similar. 1159 | (Espen Hovlandsdal) 1160 | 1161 | ## 1.2.4 - 2016-01-28 1162 | 1163 | ### Changed 1164 | 1165 | * Rolled back dependencies because of breaking changes 1166 | 1167 | ## 1.2.3 - 2016-01-24 1168 | 1169 | ### Changed 1170 | 1171 | * Updated dependencies for both `commonmark` and `commonmark-react-parser` to 1172 | work around an embarrassing oversight on my part. 1173 | 1174 | ## 1.2.2 - 2016-01-08 1175 | 1176 | ### Changed 1177 | 1178 | * Reverted change from 1.2.1 that uses the dist version. 1179 | Instead, documentation is added that specified the need for `json-loader` to 1180 | be enabled when using webpack. 1181 | 1182 | ## 1.2.1 - 2015-12-29 1183 | 1184 | ### Fixed 1185 | 1186 | * Use pre-built (dist version) of commonmark renderer in order to work around 1187 | JSON-loader dependency. 1188 | 1189 | ## 1.2.0 - 2015-12-16 1190 | 1191 | ### Added 1192 | 1193 | * Added new `allowNode`-property. 1194 | See README for details. 1195 | 1196 | ## 1.1.4 - 2015-12-14 1197 | 1198 | ### Fixed 1199 | 1200 | * Set correct `libraryTarget` to make UMD builds work as expected 1201 | 1202 | ## 1.1.3 - 2015-12-14 1203 | 1204 | ### Fixed 1205 | 1206 | * Update babel dependencies and run prepublish only as actual prepublish, not 1207 | install 1208 | 1209 | ## 1.1.1 - 2015-11-28 1210 | 1211 | ### Fixed 1212 | 1213 | * Fixed issue with React external name in global environment (`react` vs `React`) 1214 | 1215 | ## 1.1.0 - 2015-11-22 1216 | 1217 | ### Changed 1218 | 1219 | * Add ability to allow/disallow specific node types (`allowedTypes`/`disallowedTypes`) 1220 | 1221 | ## 1.0.5 - 2015-10-22 1222 | 1223 | ### Changed 1224 | 1225 | * Moved React from dependency to peer dependency. 1226 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('./lib/index.js').AllowElement} AllowElement 3 | * @typedef {import('./lib/index.js').Components} Components 4 | * @typedef {import('./lib/index.js').ExtraProps} ExtraProps 5 | * @typedef {import('./lib/index.js').HooksOptions} HooksOptions 6 | * @typedef {import('./lib/index.js').Options} Options 7 | * @typedef {import('./lib/index.js').UrlTransform} UrlTransform 8 | */ 9 | 10 | export { 11 | MarkdownAsync, 12 | MarkdownHooks, 13 | Markdown as default, 14 | defaultUrlTransform 15 | } from './lib/index.js' 16 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @import {Element, Nodes, Parents, Root} from 'hast' 3 | * @import {Root as MdastRoot} from 'mdast' 4 | * @import {ComponentType, JSX, ReactElement, ReactNode} from 'react' 5 | * @import {Options as RemarkRehypeOptions} from 'remark-rehype' 6 | * @import {BuildVisitor} from 'unist-util-visit' 7 | * @import {PluggableList, Processor} from 'unified' 8 | */ 9 | 10 | /** 11 | * @callback AllowElement 12 | * Filter elements. 13 | * @param {Readonly} element 14 | * Element to check. 15 | * @param {number} index 16 | * Index of `element` in `parent`. 17 | * @param {Readonly | undefined} parent 18 | * Parent of `element`. 19 | * @returns {boolean | null | undefined} 20 | * Whether to allow `element` (default: `false`). 21 | */ 22 | 23 | /** 24 | * @typedef ExtraProps 25 | * Extra fields we pass. 26 | * @property {Element | undefined} [node] 27 | * passed when `passNode` is on. 28 | */ 29 | 30 | /** 31 | * @typedef {{ 32 | * [Key in keyof JSX.IntrinsicElements]?: ComponentType | keyof JSX.IntrinsicElements 33 | * }} Components 34 | * Map tag names to components. 35 | */ 36 | 37 | /** 38 | * @typedef Deprecation 39 | * Deprecation. 40 | * @property {string} from 41 | * Old field. 42 | * @property {string} id 43 | * ID in readme. 44 | * @property {keyof Options} [to] 45 | * New field. 46 | */ 47 | 48 | /** 49 | * @typedef Options 50 | * Configuration. 51 | * @property {AllowElement | null | undefined} [allowElement] 52 | * Filter elements (optional); 53 | * `allowedElements` / `disallowedElements` is used first. 54 | * @property {ReadonlyArray | null | undefined} [allowedElements] 55 | * Tag names to allow (default: all tag names); 56 | * cannot combine w/ `disallowedElements`. 57 | * @property {string | null | undefined} [children] 58 | * Markdown. 59 | * @property {Components | null | undefined} [components] 60 | * Map tag names to components. 61 | * @property {ReadonlyArray | null | undefined} [disallowedElements] 62 | * Tag names to disallow (default: `[]`); 63 | * cannot combine w/ `allowedElements`. 64 | * @property {PluggableList | null | undefined} [rehypePlugins] 65 | * List of rehype plugins to use. 66 | * @property {PluggableList | null | undefined} [remarkPlugins] 67 | * List of remark plugins to use. 68 | * @property {Readonly | null | undefined} [remarkRehypeOptions] 69 | * Options to pass through to `remark-rehype`. 70 | * @property {boolean | null | undefined} [skipHtml=false] 71 | * Ignore HTML in markdown completely (default: `false`). 72 | * @property {boolean | null | undefined} [unwrapDisallowed=false] 73 | * Extract (unwrap) what’s in disallowed elements (default: `false`); 74 | * normally when say `strong` is not allowed, it and it’s children are dropped, 75 | * with `unwrapDisallowed` the element itself is replaced by its children. 76 | * @property {UrlTransform | null | undefined} [urlTransform] 77 | * Change URLs (default: `defaultUrlTransform`) 78 | */ 79 | 80 | /** 81 | * @typedef HooksOptionsOnly 82 | * Configuration specifically for {@linkcode MarkdownHooks}. 83 | * @property {ReactNode | null | undefined} [fallback] 84 | * Content to render while the processor processing the markdown (optional). 85 | */ 86 | 87 | /** 88 | * @typedef {Options & HooksOptionsOnly} HooksOptions 89 | * Configuration for {@linkcode MarkdownHooks}; 90 | * extends the regular {@linkcode Options} with a `fallback` prop. 91 | */ 92 | 93 | /** 94 | * @callback UrlTransform 95 | * Transform all URLs. 96 | * @param {string} url 97 | * URL. 98 | * @param {string} key 99 | * Property name (example: `'href'`). 100 | * @param {Readonly} node 101 | * Node. 102 | * @returns {string | null | undefined} 103 | * Transformed URL (optional). 104 | */ 105 | 106 | import {unreachable} from 'devlop' 107 | import {toJsxRuntime} from 'hast-util-to-jsx-runtime' 108 | import {urlAttributes} from 'html-url-attributes' 109 | import {Fragment, jsx, jsxs} from 'react/jsx-runtime' 110 | import {useEffect, useMemo, useState} from 'react' 111 | import remarkParse from 'remark-parse' 112 | import remarkRehype from 'remark-rehype' 113 | import {unified} from 'unified' 114 | import {visit} from 'unist-util-visit' 115 | import {VFile} from 'vfile' 116 | 117 | const changelog = 118 | 'https://github.com/remarkjs/react-markdown/blob/main/changelog.md' 119 | 120 | /** @type {PluggableList} */ 121 | const emptyPlugins = [] 122 | /** @type {Readonly} */ 123 | const emptyRemarkRehypeOptions = {allowDangerousHtml: true} 124 | const safeProtocol = /^(https?|ircs?|mailto|xmpp)$/i 125 | 126 | // Mutable because we `delete` any time it’s used and a message is sent. 127 | /** @type {ReadonlyArray>} */ 128 | const deprecations = [ 129 | {from: 'astPlugins', id: 'remove-buggy-html-in-markdown-parser'}, 130 | {from: 'allowDangerousHtml', id: 'remove-buggy-html-in-markdown-parser'}, 131 | { 132 | from: 'allowNode', 133 | id: 'replace-allownode-allowedtypes-and-disallowedtypes', 134 | to: 'allowElement' 135 | }, 136 | { 137 | from: 'allowedTypes', 138 | id: 'replace-allownode-allowedtypes-and-disallowedtypes', 139 | to: 'allowedElements' 140 | }, 141 | {from: 'className', id: 'remove-classname'}, 142 | { 143 | from: 'disallowedTypes', 144 | id: 'replace-allownode-allowedtypes-and-disallowedtypes', 145 | to: 'disallowedElements' 146 | }, 147 | {from: 'escapeHtml', id: 'remove-buggy-html-in-markdown-parser'}, 148 | {from: 'includeElementIndex', id: '#remove-includeelementindex'}, 149 | { 150 | from: 'includeNodeIndex', 151 | id: 'change-includenodeindex-to-includeelementindex' 152 | }, 153 | {from: 'linkTarget', id: 'remove-linktarget'}, 154 | {from: 'plugins', id: 'change-plugins-to-remarkplugins', to: 'remarkPlugins'}, 155 | {from: 'rawSourcePos', id: '#remove-rawsourcepos'}, 156 | {from: 'renderers', id: 'change-renderers-to-components', to: 'components'}, 157 | {from: 'source', id: 'change-source-to-children', to: 'children'}, 158 | {from: 'sourcePos', id: '#remove-sourcepos'}, 159 | {from: 'transformImageUri', id: '#add-urltransform', to: 'urlTransform'}, 160 | {from: 'transformLinkUri', id: '#add-urltransform', to: 'urlTransform'} 161 | ] 162 | 163 | /** 164 | * Component to render markdown. 165 | * 166 | * This is a synchronous component. 167 | * When using async plugins, 168 | * see {@linkcode MarkdownAsync} or {@linkcode MarkdownHooks}. 169 | * 170 | * @param {Readonly} options 171 | * Props. 172 | * @returns {ReactElement} 173 | * React element. 174 | */ 175 | export function Markdown(options) { 176 | const processor = createProcessor(options) 177 | const file = createFile(options) 178 | return post(processor.runSync(processor.parse(file), file), options) 179 | } 180 | 181 | /** 182 | * Component to render markdown with support for async plugins 183 | * through async/await. 184 | * 185 | * Components returning promises are supported on the server. 186 | * For async support on the client, 187 | * see {@linkcode MarkdownHooks}. 188 | * 189 | * @param {Readonly} options 190 | * Props. 191 | * @returns {Promise} 192 | * Promise to a React element. 193 | */ 194 | export async function MarkdownAsync(options) { 195 | const processor = createProcessor(options) 196 | const file = createFile(options) 197 | const tree = await processor.run(processor.parse(file), file) 198 | return post(tree, options) 199 | } 200 | 201 | /** 202 | * Component to render markdown with support for async plugins through hooks. 203 | * 204 | * This uses `useEffect` and `useState` hooks. 205 | * Hooks run on the client and do not immediately render something. 206 | * For async support on the server, 207 | * see {@linkcode MarkdownAsync}. 208 | * 209 | * @param {Readonly} options 210 | * Props. 211 | * @returns {ReactNode} 212 | * React node. 213 | */ 214 | export function MarkdownHooks(options) { 215 | const processor = useMemo( 216 | function () { 217 | return createProcessor(options) 218 | }, 219 | [options.rehypePlugins, options.remarkPlugins, options.remarkRehypeOptions] 220 | ) 221 | const [error, setError] = useState( 222 | /** @type {Error | undefined} */ (undefined) 223 | ) 224 | const [tree, setTree] = useState(/** @type {Root | undefined} */ (undefined)) 225 | 226 | useEffect( 227 | function () { 228 | let cancelled = false 229 | const file = createFile(options) 230 | 231 | processor.run(processor.parse(file), file, function (error, tree) { 232 | if (!cancelled) { 233 | setError(error) 234 | setTree(tree) 235 | } 236 | }) 237 | 238 | /** 239 | * @returns {undefined} 240 | * Nothing. 241 | */ 242 | return function () { 243 | cancelled = true 244 | } 245 | }, 246 | [options.children, processor] 247 | ) 248 | 249 | if (error) throw error 250 | 251 | return tree ? post(tree, options) : options.fallback 252 | } 253 | 254 | /** 255 | * Set up the `unified` processor. 256 | * 257 | * @param {Readonly} options 258 | * Props. 259 | * @returns {Processor} 260 | * Result. 261 | */ 262 | function createProcessor(options) { 263 | const rehypePlugins = options.rehypePlugins || emptyPlugins 264 | const remarkPlugins = options.remarkPlugins || emptyPlugins 265 | const remarkRehypeOptions = options.remarkRehypeOptions 266 | ? {...options.remarkRehypeOptions, ...emptyRemarkRehypeOptions} 267 | : emptyRemarkRehypeOptions 268 | 269 | const processor = unified() 270 | .use(remarkParse) 271 | .use(remarkPlugins) 272 | .use(remarkRehype, remarkRehypeOptions) 273 | .use(rehypePlugins) 274 | 275 | return processor 276 | } 277 | 278 | /** 279 | * Set up the virtual file. 280 | * 281 | * @param {Readonly} options 282 | * Props. 283 | * @returns {VFile} 284 | * Result. 285 | */ 286 | function createFile(options) { 287 | const children = options.children || '' 288 | const file = new VFile() 289 | 290 | if (typeof children === 'string') { 291 | file.value = children 292 | } else { 293 | unreachable( 294 | 'Unexpected value `' + 295 | children + 296 | '` for `children` prop, expected `string`' 297 | ) 298 | } 299 | 300 | return file 301 | } 302 | 303 | /** 304 | * Process the result from unified some more. 305 | * 306 | * @param {Nodes} tree 307 | * Tree. 308 | * @param {Readonly} options 309 | * Props. 310 | * @returns {ReactElement} 311 | * React element. 312 | */ 313 | function post(tree, options) { 314 | const allowedElements = options.allowedElements 315 | const allowElement = options.allowElement 316 | const components = options.components 317 | const disallowedElements = options.disallowedElements 318 | const skipHtml = options.skipHtml 319 | const unwrapDisallowed = options.unwrapDisallowed 320 | const urlTransform = options.urlTransform || defaultUrlTransform 321 | 322 | for (const deprecation of deprecations) { 323 | if (Object.hasOwn(options, deprecation.from)) { 324 | unreachable( 325 | 'Unexpected `' + 326 | deprecation.from + 327 | '` prop, ' + 328 | (deprecation.to 329 | ? 'use `' + deprecation.to + '` instead' 330 | : 'remove it') + 331 | ' (see <' + 332 | changelog + 333 | '#' + 334 | deprecation.id + 335 | '> for more info)' 336 | ) 337 | } 338 | } 339 | 340 | if (allowedElements && disallowedElements) { 341 | unreachable( 342 | 'Unexpected combined `allowedElements` and `disallowedElements`, expected one or the other' 343 | ) 344 | } 345 | 346 | visit(tree, transform) 347 | 348 | return toJsxRuntime(tree, { 349 | Fragment, 350 | components, 351 | ignoreInvalidStyle: true, 352 | jsx, 353 | jsxs, 354 | passKeys: true, 355 | passNode: true 356 | }) 357 | 358 | /** @type {BuildVisitor} */ 359 | function transform(node, index, parent) { 360 | if (node.type === 'raw' && parent && typeof index === 'number') { 361 | if (skipHtml) { 362 | parent.children.splice(index, 1) 363 | } else { 364 | parent.children[index] = {type: 'text', value: node.value} 365 | } 366 | 367 | return index 368 | } 369 | 370 | if (node.type === 'element') { 371 | /** @type {string} */ 372 | let key 373 | 374 | for (key in urlAttributes) { 375 | if ( 376 | Object.hasOwn(urlAttributes, key) && 377 | Object.hasOwn(node.properties, key) 378 | ) { 379 | const value = node.properties[key] 380 | const test = urlAttributes[key] 381 | if (test === null || test.includes(node.tagName)) { 382 | node.properties[key] = urlTransform(String(value || ''), key, node) 383 | } 384 | } 385 | } 386 | } 387 | 388 | if (node.type === 'element') { 389 | let remove = allowedElements 390 | ? !allowedElements.includes(node.tagName) 391 | : disallowedElements 392 | ? disallowedElements.includes(node.tagName) 393 | : false 394 | 395 | if (!remove && allowElement && typeof index === 'number') { 396 | remove = !allowElement(node, index, parent) 397 | } 398 | 399 | if (remove && parent && typeof index === 'number') { 400 | if (unwrapDisallowed && node.children) { 401 | parent.children.splice(index, 1, ...node.children) 402 | } else { 403 | parent.children.splice(index, 1) 404 | } 405 | 406 | return index 407 | } 408 | } 409 | } 410 | } 411 | 412 | /** 413 | * Make a URL safe. 414 | * 415 | * This follows how GitHub works. 416 | * It allows the protocols `http`, `https`, `irc`, `ircs`, `mailto`, and `xmpp`, 417 | * and URLs relative to the current protocol (such as `/something`). 418 | * 419 | * @satisfies {UrlTransform} 420 | * @param {string} value 421 | * URL. 422 | * @returns {string} 423 | * Safe URL. 424 | */ 425 | export function defaultUrlTransform(value) { 426 | // Same as: 427 | // 428 | // But without the `encode` part. 429 | const colon = value.indexOf(':') 430 | const questionMark = value.indexOf('?') 431 | const numberSign = value.indexOf('#') 432 | const slash = value.indexOf('/') 433 | 434 | if ( 435 | // If there is no protocol, it’s relative. 436 | colon === -1 || 437 | // If the first colon is after a `?`, `#`, or `/`, it’s not a protocol. 438 | (slash !== -1 && colon > slash) || 439 | (questionMark !== -1 && colon > questionMark) || 440 | (numberSign !== -1 && colon > numberSign) || 441 | // It is a protocol, it should be allowed. 442 | safeProtocol.test(value.slice(0, colon)) 443 | ) { 444 | return value 445 | } 446 | 447 | return '' 448 | } 449 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Espen Hovlandsdal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Espen Hovlandsdal ", 3 | "bugs": "https://github.com/remarkjs/react-markdown/issues", 4 | "contributors": [ 5 | "Alexander Wallin ", 6 | "Alexander Wong ", 7 | "André Staltz ", 8 | "Angus MacIsaac ", 9 | "Beau Roberts ", 10 | "Charlie Chen ", 11 | "Christian Murphy ", 12 | "Christoph Werner ", 13 | "Danny ", 14 | "Dennis S ", 15 | "Espen Hovlandsdal ", 16 | "Evan Hensleigh ", 17 | "Fabian Irsara ", 18 | "Florentin Luca Rieger ", 19 | "Frank ", 20 | "Igor Kamyshev ", 21 | "Jack Williams ", 22 | "Jakub Chrzanowski ", 23 | "Jeremy Moseley ", 24 | "Jesse Pinho ", 25 | "Kelvin Chan ", 26 | "Kohei Asai ", 27 | "Linus Unnebäck ", 28 | "Marshall Smith ", 29 | "Nathan Bierema ", 30 | "Nicolas Venegas ", 31 | "Peng Guanwen ", 32 | "Petr Gazarov ", 33 | "Phil Rajchgot ", 34 | "Rasmus Eneman ", 35 | "René Kooi ", 36 | "Riku Rouvila ", 37 | "Robin Wieruch ", 38 | "Rostyslav Melnychuk ", 39 | "Ted Piotrowski ", 40 | "Thibaud Courtoison ", 41 | "Thomas Lindstrøm ", 42 | "Tiago Roldão ", 43 | "Titus Wormer (https://wooorm.com)", 44 | "cerkiewny ", 45 | "evoye ", 46 | "gRoberts84 ", 47 | "mudrz ", 48 | "vanchagreen " 49 | ], 50 | "dependencies": { 51 | "@types/hast": "^3.0.0", 52 | "@types/mdast": "^4.0.0", 53 | "devlop": "^1.0.0", 54 | "hast-util-to-jsx-runtime": "^2.0.0", 55 | "html-url-attributes": "^3.0.0", 56 | "mdast-util-to-hast": "^13.0.0", 57 | "remark-parse": "^11.0.0", 58 | "remark-rehype": "^11.0.0", 59 | "unified": "^11.0.0", 60 | "unist-util-visit": "^5.0.0", 61 | "vfile": "^6.0.0" 62 | }, 63 | "description": "React component to render markdown", 64 | "devDependencies": { 65 | "@testing-library/react": "^16.0.0", 66 | "@types/node": "^22.0.0", 67 | "@types/react": "^19.0.0", 68 | "@types/react-dom": "^19.0.0", 69 | "c8": "^10.0.0", 70 | "concat-stream": "^2.0.0", 71 | "esbuild": "^0.25.0", 72 | "eslint-plugin-react": "^7.0.0", 73 | "global-jsdom": "^26.0.0", 74 | "prettier": "^3.0.0", 75 | "react": "^19.0.0", 76 | "react-dom": "^19.0.0", 77 | "rehype-raw": "^7.0.0", 78 | "rehype-starry-night": "^2.0.0", 79 | "remark-cli": "^12.0.0", 80 | "remark-gfm": "^4.0.0", 81 | "remark-preset-wooorm": "^11.0.0", 82 | "remark-toc": "^9.0.0", 83 | "type-coverage": "^2.0.0", 84 | "typescript": "^5.0.0", 85 | "xo": "^0.60.0" 86 | }, 87 | "exports": "./index.js", 88 | "files": [ 89 | "index.d.ts.map", 90 | "index.d.ts", 91 | "index.js", 92 | "lib/" 93 | ], 94 | "funding": { 95 | "type": "opencollective", 96 | "url": "https://opencollective.com/unified" 97 | }, 98 | "keywords": [ 99 | "ast", 100 | "commonmark", 101 | "component", 102 | "gfm", 103 | "markdown", 104 | "react", 105 | "react-component", 106 | "remark", 107 | "unified" 108 | ], 109 | "license": "MIT", 110 | "name": "react-markdown", 111 | "peerDependencies": { 112 | "@types/react": ">=18", 113 | "react": ">=18" 114 | }, 115 | "prettier": { 116 | "bracketSpacing": false, 117 | "singleQuote": true, 118 | "semi": false, 119 | "tabWidth": 2, 120 | "trailingComma": "none", 121 | "useTabs": false 122 | }, 123 | "remarkConfig": { 124 | "plugins": [ 125 | "remark-preset-wooorm", 126 | [ 127 | "remark-lint-no-html", 128 | false 129 | ] 130 | ] 131 | }, 132 | "repository": "remarkjs/react-markdown", 133 | "scripts": { 134 | "build": "tsc --build --clean && tsc --build && type-coverage", 135 | "format": "remark --frail --output --quiet -- . && prettier --log-level warn --write -- . && xo --fix", 136 | "test-api": "node --conditions development --experimental-loader=./script/load-jsx.js --no-warnings test.jsx", 137 | "test-coverage": "c8 --100 --exclude script/ --reporter lcov -- npm run test-api", 138 | "test": "npm run build && npm run format && npm run test-coverage" 139 | }, 140 | "sideEffects": false, 141 | "typeCoverage": { 142 | "atLeast": 100, 143 | "strict": true 144 | }, 145 | "type": "module", 146 | "version": "10.1.0", 147 | "xo": { 148 | "envs": [ 149 | "shared-node-browser" 150 | ], 151 | "extends": "plugin:react/jsx-runtime", 152 | "overrides": [ 153 | { 154 | "files": [ 155 | "**/*.jsx" 156 | ], 157 | "rules": { 158 | "no-unused-vars": "off" 159 | } 160 | } 161 | ], 162 | "prettier": true, 163 | "rules": { 164 | "complexity": "off", 165 | "n/file-extension-in-import": "off", 166 | "unicorn/prevent-abbreviations": "off" 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # react-markdown 8 | 9 | [![Build][badge-build-image]][badge-build-url] 10 | [![Coverage][badge-coverage-image]][badge-coverage-url] 11 | [![Downloads][badge-downloads-image]][badge-downloads-url] 12 | [![Size][badge-size-image]][badge-size-url] 13 | 14 | React component to render markdown. 15 | 16 | ## Feature highlights 17 | 18 | * [x] **[safe][section-security] by default** 19 | (no `dangerouslySetInnerHTML` or XSS attacks) 20 | * [x] **[components][section-components]** 21 | (pass your own component to use instead of `

` for `## hi`) 22 | * [x] **[plugins][section-plugins]** 23 | (many plugins you can pick and choose from) 24 | * [x] **[compliant][section-syntax]** 25 | (100% to CommonMark, 100% to GFM with a plugin) 26 | 27 | ## Contents 28 | 29 | * [What is this?](#what-is-this) 30 | * [When should I use this?](#when-should-i-use-this) 31 | * [Install](#install) 32 | * [Use](#use) 33 | * [API](#api) 34 | * [`Markdown`](#markdown) 35 | * [`MarkdownAsync`](#markdownasync) 36 | * [`MarkdownHooks`](#markdownhooks) 37 | * [`defaultUrlTransform(url)`](#defaulturltransformurl) 38 | * [`AllowElement`](#allowelement) 39 | * [`Components`](#components) 40 | * [`ExtraProps`](#extraprops) 41 | * [`HooksOptions`](#hooksoptions) 42 | * [`Options`](#options) 43 | * [`UrlTransform`](#urltransform) 44 | * [Examples](#examples) 45 | * [Use a plugin](#use-a-plugin) 46 | * [Use a plugin with options](#use-a-plugin-with-options) 47 | * [Use custom components (syntax highlight)](#use-custom-components-syntax-highlight) 48 | * [Use remark and rehype plugins (math)](#use-remark-and-rehype-plugins-math) 49 | * [Plugins](#plugins) 50 | * [Syntax](#syntax) 51 | * [Compatibility](#compatibility) 52 | * [Architecture](#architecture) 53 | * [Appendix A: HTML in markdown](#appendix-a-html-in-markdown) 54 | * [Appendix B: Components](#appendix-b-components) 55 | * [Appendix C: line endings in markdown (and JSX)](#appendix-c-line-endings-in-markdown-and-jsx) 56 | * [Security](#security) 57 | * [Related](#related) 58 | * [Contribute](#contribute) 59 | * [License](#license) 60 | 61 | ## What is this? 62 | 63 | This package is a [React][] component that can be given a string of markdown 64 | that it’ll safely render to React elements. 65 | You can pass plugins to change how markdown is transformed and pass components 66 | that will be used instead of normal HTML elements. 67 | 68 | * to learn markdown, see this [cheatsheet and tutorial][commonmark-help] 69 | * to try out `react-markdown`, see [our demo][github-io-react-markdown] 70 | 71 | ## When should I use this? 72 | 73 | There are other ways to use markdown in React out there so why use this one? 74 | The three main reasons are that they often rely on `dangerouslySetInnerHTML`, 75 | have bugs with how they handle markdown, or don’t let you swap elements for 76 | components. 77 | `react-markdown` builds a virtual DOM, so React only replaces what changed, 78 | from a syntax tree. 79 | That’s supported because we use [unified][github-unified], 80 | specifically [remark][github-remark] for markdown and [rehype][github-rehype] 81 | for HTML, 82 | which are popular tools to transform content with plugins. 83 | 84 | This package focusses on making it easy for beginners to safely use markdown in 85 | React. 86 | When you’re familiar with unified, you can use a modern hooks based alternative 87 | [`react-remark`][github-react-remark] or [`rehype-react`][github-rehype-react] 88 | manually. 89 | If you instead want to use JavaScript and JSX *inside* markdown files, use 90 | [MDX][github-mdx]. 91 | 92 | ## Install 93 | 94 | This package is [ESM only][esm]. 95 | In Node.js (version 16+), install with [npm][npm-install]: 96 | 97 | ```sh 98 | npm install react-markdown 99 | ``` 100 | 101 | In Deno with [`esm.sh`][esmsh]: 102 | 103 | ```js 104 | import Markdown from 'https://esm.sh/react-markdown@10' 105 | ``` 106 | 107 | In browsers with [`esm.sh`][esmsh]: 108 | 109 | ```html 110 | 113 | ``` 114 | 115 | ## Use 116 | 117 | A basic hello world: 118 | 119 | ```js 120 | import React from 'react' 121 | import {createRoot} from 'react-dom/client' 122 | import Markdown from 'react-markdown' 123 | 124 | const markdown = '# Hi, *Pluto*!' 125 | 126 | createRoot(document.body).render({markdown}) 127 | ``` 128 | 129 |
130 | Show equivalent JSX 131 | 132 | ```js 133 |

134 | Hi, Pluto! 135 |

136 | ``` 137 | 138 |
139 | 140 | Here is an example that shows how to use a plugin 141 | ([`remark-gfm`][github-remark-gfm], 142 | which adds support for footnotes, strikethrough, tables, tasklists and 143 | URLs directly): 144 | 145 | ```js 146 | import React from 'react' 147 | import {createRoot} from 'react-dom/client' 148 | import Markdown from 'react-markdown' 149 | import remarkGfm from 'remark-gfm' 150 | 151 | const markdown = `Just a link: www.nasa.gov.` 152 | 153 | createRoot(document.body).render( 154 | {markdown} 155 | ) 156 | ``` 157 | 158 |
159 | Show equivalent JSX 160 | 161 | ```js 162 |

163 | Just a link: www.nasa.gov. 164 |

165 | ``` 166 | 167 |
168 | 169 | ## API 170 | 171 | This package exports the identifiers 172 | [`MarkdownAsync`][api-markdown-async], 173 | [`MarkdownHooks`][api-markdown-hooks], 174 | and 175 | [`defaultUrlTransform`][api-default-url-transform]. 176 | The default export is [`Markdown`][api-markdown]. 177 | 178 | It also exports the additional [TypeScript][] types 179 | [`AllowElement`][api-allow-element], 180 | [`Components`][api-components], 181 | [`ExtraProps`][api-extra-props], 182 | [`HooksOptions`][api-hooks-options], 183 | [`Options`][api-options], 184 | and 185 | [`UrlTransform`][api-url-transform]. 186 | 187 | ### `Markdown` 188 | 189 | Component to render markdown. 190 | 191 | This is a synchronous component. 192 | When using async plugins, 193 | see [`MarkdownAsync`][api-markdown-async] or 194 | [`MarkdownHooks`][api-markdown-hooks]. 195 | 196 | ###### Parameters 197 | 198 | * `options` ([`Options`][api-options]) 199 | — props 200 | 201 | ###### Returns 202 | 203 | React element (`ReactElement`). 204 | 205 | ### `MarkdownAsync` 206 | 207 | Component to render markdown with support for async plugins 208 | through async/await. 209 | 210 | Components returning promises are supported on the server. 211 | For async support on the client, 212 | see [`MarkdownHooks`][api-markdown-hooks]. 213 | 214 | ###### Parameters 215 | 216 | * `options` ([`Options`][api-options]) 217 | — props 218 | 219 | ###### Returns 220 | 221 | Promise to a React element (`Promise`). 222 | 223 | ### `MarkdownHooks` 224 | 225 | Component to render markdown with support for async plugins through hooks. 226 | 227 | This uses `useEffect` and `useState` hooks. 228 | Hooks run on the client and do not immediately render something. 229 | For async support on the server, 230 | see [`MarkdownAsync`][api-markdown-async]. 231 | 232 | ###### Parameters 233 | 234 | * `options` ([`Options`][api-options]) 235 | — props 236 | 237 | ###### Returns 238 | 239 | React node (`ReactNode`). 240 | 241 | ### `defaultUrlTransform(url)` 242 | 243 | Make a URL safe. 244 | 245 | This follows how GitHub works. 246 | It allows the protocols `http`, `https`, `irc`, `ircs`, `mailto`, and `xmpp`, 247 | and URLs relative to the current protocol (such as `/something`). 248 | 249 | ###### Parameters 250 | 251 | * `url` (`string`) 252 | — URL 253 | 254 | ###### Returns 255 | 256 | Safe URL (`string`). 257 | 258 | ### `AllowElement` 259 | 260 | Filter elements (TypeScript type). 261 | 262 | ###### Parameters 263 | 264 | * `node` ([`Element` from `hast`][github-hast-element]) 265 | — element to check 266 | * `index` (`number | undefined`) 267 | — index of `element` in `parent` 268 | * `parent` ([`Node` from `hast`][github-hast-nodes]) 269 | — parent of `element` 270 | 271 | ###### Returns 272 | 273 | Whether to allow `element` (`boolean`, optional). 274 | 275 | ### `Components` 276 | 277 | Map tag names to components (TypeScript type). 278 | 279 | ###### Type 280 | 281 | ```ts 282 | import type {ExtraProps} from 'react-markdown' 283 | import type {ComponentProps, ElementType} from 'react' 284 | 285 | type Components = { 286 | [Key in Extract]?: ElementType & ExtraProps> 287 | } 288 | ``` 289 | 290 | ### `ExtraProps` 291 | 292 | Extra fields we pass to components (TypeScript type). 293 | 294 | ###### Fields 295 | 296 | * `node` ([`Element` from `hast`][github-hast-element], optional) 297 | — original node 298 | 299 | ### `HooksOptions` 300 | 301 | Configuration for [`MarkdownHooks`][api-markdown-hooks] (TypeScript type); 302 | extends the regular [`Options`][api-options] with a `fallback` prop. 303 | 304 | ###### Extends 305 | 306 | [`Options`][api-options]. 307 | 308 | ###### Fields 309 | 310 | * `fallback` (`ReactNode`, optional) 311 | — content to render while the processor processing the markdown 312 | 313 | ### `Options` 314 | 315 | Configuration (TypeScript type). 316 | 317 | ###### Fields 318 | 319 | * `allowElement` ([`AllowElement`][api-allow-element], optional) 320 | — filter elements; 321 | `allowedElements` / `disallowedElements` is used first 322 | * `allowedElements` (`Array`, default: all tag names) 323 | — tag names to allow; 324 | cannot combine w/ `disallowedElements` 325 | * `children` (`string`, optional) 326 | — markdown 327 | * `components` ([`Components`][api-components], optional) 328 | — map tag names to components 329 | * `disallowedElements` (`Array`, default: `[]`) 330 | — tag names to disallow; 331 | cannot combine w/ `allowedElements` 332 | * `rehypePlugins` (`Array`, optional) 333 | — list of [rehype plugins][github-rehype-plugins] to use 334 | * `remarkPlugins` (`Array`, optional) 335 | — list of [remark plugins][github-remark-plugins] to use 336 | * `remarkRehypeOptions` 337 | ([`Options` from `remark-rehype`][github-remark-rehype-options], 338 | optional) 339 | — options to pass through to `remark-rehype` 340 | * `skipHtml` (`boolean`, default: `false`) 341 | — ignore HTML in markdown completely 342 | * `unwrapDisallowed` (`boolean`, default: `false`) 343 | — extract (unwrap) what’s in disallowed elements; 344 | normally when say `strong` is not allowed, it and it’s children are dropped, 345 | with `unwrapDisallowed` the element itself is replaced by its children 346 | * `urlTransform` ([`UrlTransform`][api-url-transform], default: 347 | [`defaultUrlTransform`][api-default-url-transform]) 348 | — change URLs 349 | 350 | ### `UrlTransform` 351 | 352 | Transform URLs (TypeScript type). 353 | 354 | ###### Parameters 355 | 356 | * `url` (`string`) 357 | — URL 358 | * `key` (`string`, example: `'href'`) 359 | — property name 360 | * `node` ([`Element` from `hast`][github-hast-element]) 361 | — element to check 362 | 363 | ###### Returns 364 | 365 | Transformed URL (`string`, optional). 366 | 367 | ## Examples 368 | 369 | ### Use a plugin 370 | 371 | This example shows how to use a remark plugin. 372 | In this case, [`remark-gfm`][github-remark-gfm], 373 | which adds support for strikethrough, tables, tasklists and URLs directly: 374 | 375 | ```js 376 | import React from 'react' 377 | import {createRoot} from 'react-dom/client' 378 | import Markdown from 'react-markdown' 379 | import remarkGfm from 'remark-gfm' 380 | 381 | const markdown = `A paragraph with *emphasis* and **strong importance**. 382 | 383 | > A block quote with ~strikethrough~ and a URL: https://reactjs.org. 384 | 385 | * Lists 386 | * [ ] todo 387 | * [x] done 388 | 389 | A table: 390 | 391 | | a | b | 392 | | - | - | 393 | ` 394 | 395 | createRoot(document.body).render( 396 | {markdown} 397 | ) 398 | ``` 399 | 400 |
401 | Show equivalent JSX 402 | 403 | ```js 404 | <> 405 |

406 | A paragraph with emphasis and strong importance. 407 |

408 |
409 |

410 | A block quote with strikethrough and a URL:{' '} 411 | https://reactjs.org. 412 |

413 |
414 |
    415 |
  • Lists
  • 416 |
  • 417 | todo 418 |
  • 419 |
  • 420 | done 421 |
  • 422 |
423 |

A table:

424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 |
ab
432 | 433 | ``` 434 | 435 |
436 | 437 | ### Use a plugin with options 438 | 439 | This example shows how to use a plugin and give it options. 440 | To do that, use an array with the plugin at the first place, and the options 441 | second. 442 | [`remark-gfm`][github-remark-gfm] has an option to allow only double tildes for 443 | strikethrough: 444 | 445 | ```js 446 | import React from 'react' 447 | import {createRoot} from 'react-dom/client' 448 | import Markdown from 'react-markdown' 449 | import remarkGfm from 'remark-gfm' 450 | 451 | const markdown = 'This ~is not~ strikethrough, but ~~this is~~!' 452 | 453 | createRoot(document.body).render( 454 | 455 | {markdown} 456 | 457 | ) 458 | ``` 459 | 460 |
461 | Show equivalent JSX 462 | 463 | ```js 464 |

465 | This ~is not~ strikethrough, but this is! 466 |

467 | ``` 468 | 469 |
470 | 471 | ### Use custom components (syntax highlight) 472 | 473 | This example shows how you can overwrite the normal handling of an element by 474 | passing a component. 475 | In this case, we apply syntax highlighting with the seriously super amazing 476 | [`react-syntax-highlighter`][github-react-syntax-highlighter] by 477 | [**@conorhastings**][github-conorhastings]: 478 | 479 | 480 | 481 | ```js 482 | import React from 'react' 483 | import {createRoot} from 'react-dom/client' 484 | import Markdown from 'react-markdown' 485 | import {Prism as SyntaxHighlighter} from 'react-syntax-highlighter' 486 | import {dark} from 'react-syntax-highlighter/dist/esm/styles/prism' 487 | 488 | // Did you know you can use tildes instead of backticks for code in markdown? ✨ 489 | const markdown = `Here is some JavaScript code: 490 | 491 | ~~~js 492 | console.log('It works!') 493 | ~~~ 494 | ` 495 | 496 | createRoot(document.body).render( 497 | 511 | ) : ( 512 | 513 | {children} 514 | 515 | ) 516 | } 517 | }} 518 | /> 519 | ) 520 | ``` 521 | 522 |
523 | Show equivalent JSX 524 | 525 | ```js 526 | <> 527 |

Here is some JavaScript code:

528 |
529 |     
530 |   
531 | 532 | ``` 533 | 534 |
535 | 536 | ### Use remark and rehype plugins (math) 537 | 538 | This example shows how a syntax extension 539 | (through [`remark-math`][github-remark-math]) 540 | is used to support math in markdown, and a transform plugin 541 | ([`rehype-katex`][github-rehype-katex]) to render that math. 542 | 543 | ```js 544 | import React from 'react' 545 | import {createRoot} from 'react-dom/client' 546 | import Markdown from 'react-markdown' 547 | import rehypeKatex from 'rehype-katex' 548 | import remarkMath from 'remark-math' 549 | import 'katex/dist/katex.min.css' // `rehype-katex` does not import the CSS for you 550 | 551 | const markdown = `The lift coefficient ($C_L$) is a dimensionless coefficient.` 552 | 553 | createRoot(document.body).render( 554 | 555 | {markdown} 556 | 557 | ) 558 | ``` 559 | 560 |
561 | Show equivalent JSX 562 | 563 | ```js 564 |

565 | The lift coefficient ( 566 | 567 | 568 | {/* … */} 569 | 570 | 573 | 574 | ) is a dimensionless coefficient. 575 |

576 | ``` 577 | 578 |
579 | 580 | ## Plugins 581 | 582 | We use [unified][github-unified], 583 | specifically [remark][github-remark] for markdown and 584 | [rehype][github-rehype] for HTML, 585 | which are tools to transform content with plugins. 586 | Here are three good ways to find plugins: 587 | 588 | * [`awesome-remark`][github-awesome-remark] and 589 | [`awesome-rehype`][github-awesome-rehype] 590 | — selection of the most awesome projects 591 | * [List of remark plugins][github-remark-plugins] and 592 | [list of rehype plugins][github-rehype-plugins] 593 | — list of all plugins 594 | * [`remark-plugin`][github-topic-remark-plugin] and 595 | [`rehype-plugin`][github-topic-rehype-plugin] topics 596 | — any tagged repo on GitHub 597 | 598 | ## Syntax 599 | 600 | `react-markdown` follows CommonMark, which standardizes the differences between 601 | markdown implementations, by default. 602 | Some syntax extensions are supported through plugins. 603 | 604 | We use [`micromark`][github-micromark] under the hood for our parsing. 605 | See its documentation for more information on markdown, CommonMark, and 606 | extensions. 607 | 608 | ## Compatibility 609 | 610 | Projects maintained by the unified collective are compatible with maintained 611 | versions of Node.js. 612 | 613 | When we cut a new major release, we drop support for unmaintained versions of 614 | Node. 615 | This means we try to keep the current release line, `react-markdown@10`, 616 | compatible with Node.js 16. 617 | 618 | They work in all modern browsers (essentially: everything not IE 11). 619 | You can use a bundler (such as esbuild, webpack, or Rollup) to use this package 620 | in your project, and use its options (or plugins) to add support for legacy 621 | browsers. 622 | 623 | ## Architecture 624 | 625 |
                                                           react-markdown
626 |          +----------------------------------------------------------------------------------------------------------------+
627 |          |                                                                                                                |
628 |          |  +----------+        +----------------+        +---------------+       +----------------+       +------------+ |
629 |          |  |          |        |                |        |               |       |                |       |            | |
630 | markdown-+->+  remark  +-mdast->+ remark plugins +-mdast->+ remark-rehype +-hast->+ rehype plugins +-hast->+ components +-+->react elements
631 |          |  |          |        |                |        |               |       |                |       |            | |
632 |          |  +----------+        +----------------+        +---------------+       +----------------+       +------------+ |
633 |          |                                                                                                                |
634 |          +----------------------------------------------------------------------------------------------------------------+
635 | 
636 | 637 | To understand what this project does, it’s important to first understand what 638 | unified does: please read through the [`unifiedjs/unified`][github-unified] 639 | readme 640 | (the part until you hit the API section is required reading). 641 | 642 | `react-markdown` is a unified pipeline — wrapped so that most folks don’t need 643 | to directly interact with unified. 644 | The processor goes through these steps: 645 | 646 | * parse markdown to mdast (markdown syntax tree) 647 | * transform through remark (markdown ecosystem) 648 | * transform mdast to hast (HTML syntax tree) 649 | * transform through rehype (HTML ecosystem) 650 | * render hast to React with components 651 | 652 | ## Appendix A: HTML in markdown 653 | 654 | `react-markdown` typically escapes HTML (or ignores it, with `skipHtml`) 655 | because it is dangerous and defeats the purpose of this library. 656 | 657 | However, if you are in a trusted environment (you trust the markdown), and 658 | can spare the bundle size (±60kb minzipped), then you can use 659 | [`rehype-raw`][github-rehype-raw]: 660 | 661 | ```js 662 | import React from 'react' 663 | import {createRoot} from 'react-dom/client' 664 | import Markdown from 'react-markdown' 665 | import rehypeRaw from 'rehype-raw' 666 | 667 | const markdown = `
668 | 669 | Some *emphasis* and strong! 670 | 671 |
` 672 | 673 | createRoot(document.body).render( 674 | {markdown} 675 | ) 676 | ``` 677 | 678 |
679 | Show equivalent JSX 680 | 681 | ```js 682 |
683 |

684 | Some emphasis and strong! 685 |

686 |
687 | ``` 688 | 689 |
690 | 691 | **Note**: HTML in markdown is still bound by how [HTML works in 692 | CommonMark][commonmark-html]. 693 | Make sure to use blank lines around block-level HTML that again contains 694 | markdown! 695 | 696 | ## Appendix B: Components 697 | 698 | You can also change the things that come from markdown: 699 | 700 | ```js 701 | 709 | } 710 | }} 711 | /> 712 | ``` 713 | 714 | The keys in components are HTML equivalents for the things you write with 715 | markdown (such as `h1` for `# heading`). 716 | Normally, in markdown, those are: `a`, `blockquote`, `br`, `code`, `em`, `h1`, 717 | `h2`, `h3`, `h4`, `h5`, `h6`, `hr`, `img`, `li`, `ol`, `p`, `pre`, `strong`, and 718 | `ul`. 719 | With [`remark-gfm`][github-remark-gfm], 720 | you can also use `del`, `input`, `table`, `tbody`, `td`, `th`, `thead`, and `tr`. 721 | Other remark or rehype plugins that add support for new constructs will also 722 | work with `react-markdown`. 723 | 724 | The props that are passed are what you probably would expect: an `a` (link) will 725 | get `href` (and `title`) props, and `img` (image) an `src`, `alt` and `title`, 726 | etc. 727 | 728 | Every component will receive a `node`. 729 | This is the original [`Element` from `hast`][github-hast-element] element being 730 | turned into a React element. 731 | 732 | ## Appendix C: line endings in markdown (and JSX) 733 | 734 | You might have trouble with how line endings work in markdown and JSX. 735 | We recommend the following, which solves all line ending problems: 736 | 737 | ```js 738 | // If you write actual markdown in your code, put your markdown in a variable; 739 | // **do not indent markdown**: 740 | const markdown = ` 741 | # This is perfect! 742 | ` 743 | 744 | // Pass the value as an expression as an only child: 745 | const result = {markdown} 746 | ``` 747 | 748 | 👆 That works. 749 | Read on for what doesn’t and why that is. 750 | 751 | You might try to write markdown directly in your JSX and find that it **does 752 | not** work: 753 | 754 | ```js 755 | 756 | # Hi 757 | 758 | This is **not** a paragraph. 759 | 760 | ``` 761 | 762 | The is because in JSX the whitespace (including line endings) is collapsed to 763 | a single space. 764 | So the above example is equivalent to: 765 | 766 | ```js 767 | # Hi This is **not** a paragraph. 768 | ``` 769 | 770 | Instead, to pass markdown to `Markdown`, you can use an expression: 771 | with a template literal: 772 | 773 | ```js 774 | {` 775 | # Hi 776 | 777 | This is a paragraph. 778 | `} 779 | ``` 780 | 781 | Template literals have another potential problem, because they keep whitespace 782 | (including indentation) inside them. 783 | That means that the following **does not** turn into a heading: 784 | 785 | ```js 786 | {` 787 | # This is **not** a heading, it’s an indented code block 788 | `} 789 | ``` 790 | 791 | ## Security 792 | 793 | Use of `react-markdown` is secure by default. 794 | Overwriting `urlTransform` to something insecure will open you up to XSS 795 | vectors. 796 | Furthermore, the `remarkPlugins`, `rehypePlugins`, and `components` you use may 797 | be insecure. 798 | 799 | To make sure the content is completely safe, even after what plugins do, 800 | use [`rehype-sanitize`][github-rehype-sanitize]. 801 | It lets you define your own schema of what is and isn’t allowed. 802 | 803 | ## Related 804 | 805 | * [`MDX`][github-mdx] 806 | — JSX *in* markdown 807 | * [`remark-gfm`][github-remark-gfm] 808 | — add support for GitHub flavored markdown support 809 | * [`react-remark`][github-react-remark] 810 | — hook based alternative 811 | * [`rehype-react`][github-rehype-react] 812 | — turn HTML into React elements 813 | 814 | ## Contribute 815 | 816 | See [`contributing.md`][health-contributing] in [`remarkjs/.github`][health] 817 | for ways to get started. 818 | See [`support.md`][health-support] for ways to get help. 819 | 820 | This project has a [code of conduct][health-coc]. 821 | By interacting with this repository, organization, or community you agree to 822 | abide by its terms. 823 | 824 | ## License 825 | 826 | [MIT][file-license] © [Espen Hovlandsdal][author] 827 | 828 | [api-allow-element]: #allowelement 829 | 830 | [api-components]: #components 831 | 832 | [api-default-url-transform]: #defaulturltransformurl 833 | 834 | [api-extra-props]: #extraprops 835 | 836 | [api-hooks-options]: #hooksoptions 837 | 838 | [api-markdown]: #markdown 839 | 840 | [api-markdown-async]: #markdownasync 841 | 842 | [api-markdown-hooks]: #markdownhooks 843 | 844 | [api-options]: #options 845 | 846 | [api-url-transform]: #urltransform 847 | 848 | [author]: https://espen.codes/ 849 | 850 | [badge-build-image]: https://github.com/remarkjs/react-markdown/workflows/main/badge.svg 851 | 852 | [badge-build-url]: https://github.com/remarkjs/react-markdown/actions 853 | 854 | [badge-coverage-image]: https://img.shields.io/codecov/c/github/remarkjs/react-markdown.svg 855 | 856 | [badge-coverage-url]: https://codecov.io/github/remarkjs/react-markdown 857 | 858 | [badge-downloads-image]: https://img.shields.io/npm/dm/react-markdown.svg 859 | 860 | [badge-downloads-url]: https://www.npmjs.com/package/react-markdown 861 | 862 | [badge-size-image]: https://img.shields.io/bundlejs/size/react-markdown 863 | 864 | [badge-size-url]: https://bundlejs.com/?q=react-markdown 865 | 866 | [commonmark-help]: https://commonmark.org/help/ 867 | 868 | [commonmark-html]: https://spec.commonmark.org/0.31.2/#html-blocks 869 | 870 | [esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c 871 | 872 | [esmsh]: https://esm.sh 873 | 874 | [file-license]: license 875 | 876 | [github-awesome-rehype]: https://github.com/rehypejs/awesome-rehype 877 | 878 | [github-awesome-remark]: https://github.com/remarkjs/awesome-remark 879 | 880 | [github-conorhastings]: https://github.com/conorhastings 881 | 882 | [github-hast-element]: https://github.com/syntax-tree/hast#element 883 | 884 | [github-hast-nodes]: https://github.com/syntax-tree/hast#nodes 885 | 886 | [github-io-react-markdown]: https://remarkjs.github.io/react-markdown/ 887 | 888 | [github-mdx]: https://github.com/mdx-js/mdx/ 889 | 890 | [github-micromark]: https://github.com/micromark/micromark 891 | 892 | [github-react-remark]: https://github.com/remarkjs/react-remark 893 | 894 | [github-react-syntax-highlighter]: https://github.com/react-syntax-highlighter/react-syntax-highlighter 895 | 896 | [github-rehype]: https://github.com/rehypejs/rehype 897 | 898 | [github-rehype-katex]: https://github.com/remarkjs/remark-math/tree/main/packages/rehype-katex 899 | 900 | [github-rehype-plugins]: https://github.com/rehypejs/rehype/blob/main/doc/plugins.md#list-of-plugins 901 | 902 | [github-rehype-raw]: https://github.com/rehypejs/rehype-raw 903 | 904 | [github-rehype-react]: https://github.com/rehypejs/rehype-react 905 | 906 | [github-rehype-sanitize]: https://github.com/rehypejs/rehype-sanitize 907 | 908 | [github-remark]: https://github.com/remarkjs/remark 909 | 910 | [github-remark-gfm]: https://github.com/remarkjs/remark-gfm 911 | 912 | [github-remark-math]: https://github.com/remarkjs/remark-math 913 | 914 | [github-remark-plugins]: https://github.com/remarkjs/remark/blob/main/doc/plugins.md#list-of-plugins 915 | 916 | [github-remark-rehype-options]: https://github.com/remarkjs/remark-rehype#options 917 | 918 | [github-topic-rehype-plugin]: https://github.com/topics/rehype-plugin 919 | 920 | [github-topic-remark-plugin]: https://github.com/topics/remark-plugin 921 | 922 | [github-unified]: https://github.com/unifiedjs/unified 923 | 924 | [health]: https://github.com/remarkjs/.github 925 | 926 | [health-coc]: https://github.com/remarkjs/.github/blob/main/code-of-conduct.md 927 | 928 | [health-contributing]: https://github.com/remarkjs/.github/blob/main/contributing.md 929 | 930 | [health-support]: https://github.com/remarkjs/.github/blob/main/support.md 931 | 932 | [npm-install]: https://docs.npmjs.com/cli/install 933 | 934 | [react]: http://reactjs.org 935 | 936 | [section-components]: #appendix-b-components 937 | 938 | [section-plugins]: #plugins 939 | 940 | [section-security]: #security 941 | 942 | [section-syntax]: #syntax 943 | 944 | [typescript]: https://www.typescriptlang.org 945 | -------------------------------------------------------------------------------- /script/load-jsx.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises' 2 | import {fileURLToPath} from 'node:url' 3 | import {transform} from 'esbuild' 4 | 5 | const {getFormat, load, transformSource} = createLoader() 6 | 7 | export {getFormat, load, transformSource} 8 | 9 | /** 10 | * A tiny JSX loader. 11 | */ 12 | export function createLoader() { 13 | return {load, getFormat, transformSource} 14 | 15 | // Node version 17. 16 | /** 17 | * @param {string} href 18 | * @param {unknown} context 19 | * @param {Function} defaultLoad 20 | */ 21 | async function load(href, context, defaultLoad) { 22 | const url = new URL(href) 23 | 24 | if (!url.pathname.endsWith('.jsx')) { 25 | return defaultLoad(href, context, defaultLoad) 26 | } 27 | 28 | const {code, warnings} = await transform(String(await fs.readFile(url)), { 29 | format: 'esm', 30 | loader: 'jsx', 31 | sourcefile: fileURLToPath(url), 32 | sourcemap: 'both', 33 | target: 'esnext' 34 | }) 35 | 36 | if (warnings) { 37 | for (const warning of warnings) { 38 | console.log(warning.location) 39 | console.log(warning.text) 40 | } 41 | } 42 | 43 | return {format: 'module', shortCircuit: true, source: code} 44 | } 45 | 46 | // Pre version 17. 47 | /** 48 | * @param {string} href 49 | * @param {unknown} context 50 | * @param {Function} defaultGetFormat 51 | */ 52 | function getFormat(href, context, defaultGetFormat) { 53 | const url = new URL(href) 54 | 55 | return url.pathname.endsWith('.jsx') 56 | ? {format: 'module'} 57 | : defaultGetFormat(href, context, defaultGetFormat) 58 | } 59 | 60 | /** 61 | * @param {Buffer} value 62 | * @param {{url: string, [x: string]: unknown}} context 63 | * @param {Function} defaultTransformSource 64 | */ 65 | async function transformSource(value, context, defaultTransformSource) { 66 | const url = new URL(context.url) 67 | 68 | if (!url.pathname.endsWith('.jsx')) { 69 | return defaultTransformSource(value, context, defaultTransformSource) 70 | } 71 | 72 | const {code, warnings} = await transform(String(value), { 73 | format: context.format === 'module' ? 'esm' : 'cjs', 74 | loader: 'jsx', 75 | sourcefile: fileURLToPath(url), 76 | sourcemap: 'both', 77 | target: 'esnext' 78 | }) 79 | 80 | if (warnings) { 81 | for (const warning of warnings) { 82 | console.log(warning.location) 83 | console.log(warning.text) 84 | } 85 | } 86 | 87 | return {source: code} 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /test.jsx: -------------------------------------------------------------------------------- 1 | /* @jsxRuntime automatic @jsxImportSource react */ 2 | /** 3 | * @import {Root} from 'hast' 4 | * @import {ComponentProps, ReactNode} from 'react' 5 | * @import {ExtraProps} from 'react-markdown' 6 | * @import {Plugin} from 'unified' 7 | */ 8 | 9 | /** 10 | * @typedef DeferredPlugin 11 | * Deferred plugin. 12 | * @property {Plugin<[]>} plugin 13 | * Plugin. 14 | * @property {(error: Error) => undefined} reject 15 | * Reject the plugin. 16 | * @property {() => undefined} resolve 17 | * Resolve the plugin. 18 | */ 19 | 20 | import assert from 'node:assert/strict' 21 | import test from 'node:test' 22 | import 'global-jsdom/register' 23 | import {render, waitFor} from '@testing-library/react' 24 | import concatStream from 'concat-stream' 25 | import {Component} from 'react' 26 | import {renderToPipeableStream, renderToStaticMarkup} from 'react-dom/server' 27 | import Markdown, {MarkdownAsync, MarkdownHooks} from 'react-markdown' 28 | import rehypeRaw from 'rehype-raw' 29 | import rehypeStarryNight from 'rehype-starry-night' 30 | import remarkGfm from 'remark-gfm' 31 | import remarkToc from 'remark-toc' 32 | import {visit} from 'unist-util-visit' 33 | 34 | const decoder = new TextDecoder() 35 | 36 | test('react-markdown (core)', async function (t) { 37 | await t.test('should expose the public api', async function () { 38 | assert.deepEqual(Object.keys(await import('react-markdown')).sort(), [ 39 | 'MarkdownAsync', 40 | 'MarkdownHooks', 41 | 'default', 42 | 'defaultUrlTransform' 43 | ]) 44 | }) 45 | }) 46 | 47 | test('Markdown', async function (t) { 48 | await t.test('should work', function () { 49 | assert.equal(renderToStaticMarkup(), '

a

') 50 | }) 51 | 52 | await t.test('should throw w/ `source`', function () { 53 | assert.throws(function () { 54 | // @ts-expect-error: check how the runtime handles untyped `source`. 55 | renderToStaticMarkup() 56 | }, /Unexpected `source` prop, use `children` instead/) 57 | }) 58 | 59 | await t.test('should throw w/ non-string children (number)', function () { 60 | assert.throws(function () { 61 | // @ts-expect-error: check how the runtime handles invalid `children`. 62 | renderToStaticMarkup() 63 | }, /Unexpected value `1` for `children` prop, expected `string`/) 64 | }) 65 | 66 | await t.test('should throw w/ non-string children (boolean)', function () { 67 | assert.throws(function () { 68 | // @ts-expect-error: check how the runtime handles invalid `children`. 69 | renderToStaticMarkup() 70 | }, /Unexpected value `true` for `children` prop, expected `string`/) 71 | }) 72 | 73 | await t.test('should support `null` as children', function () { 74 | assert.equal(renderToStaticMarkup(), '') 75 | }) 76 | 77 | await t.test('should support `undefined` as children', function () { 78 | assert.equal(renderToStaticMarkup(), '') 79 | }) 80 | 81 | await t.test('should warn w/ `allowDangerousHtml`', function () { 82 | assert.throws(function () { 83 | // @ts-expect-error: check how the runtime handles deprecated `allowDangerousHtml`. 84 | renderToStaticMarkup() 85 | }, /Unexpected `allowDangerousHtml` prop, remove it/) 86 | }) 87 | 88 | await t.test('should support a block quote', function () { 89 | assert.equal( 90 | renderToStaticMarkup(), 91 | '
\n

a

\n
' 92 | ) 93 | }) 94 | 95 | await t.test('should support a break', function () { 96 | assert.equal( 97 | renderToStaticMarkup(), 98 | '

a
\nb

' 99 | ) 100 | }) 101 | 102 | await t.test('should support a code (block, flow; indented)', function () { 103 | assert.equal( 104 | renderToStaticMarkup(), 105 | '
a\n
' 106 | ) 107 | }) 108 | 109 | await t.test('should support a code (block, flow; fenced)', function () { 110 | assert.equal( 111 | renderToStaticMarkup(), 112 | '
a\n
' 113 | ) 114 | }) 115 | 116 | await t.test('should support a delete (GFM)', function () { 117 | assert.equal( 118 | renderToStaticMarkup( 119 | 120 | ), 121 | '

a

' 122 | ) 123 | }) 124 | 125 | await t.test('should support an emphasis', function () { 126 | assert.equal( 127 | renderToStaticMarkup(), 128 | '

a

' 129 | ) 130 | }) 131 | 132 | await t.test('should support a footnote (GFM)', function () { 133 | assert.equal( 134 | renderToStaticMarkup( 135 | 136 | ), 137 | '

a1

\n

Footnotes

\n
    \n
  1. \n

    y

    \n
  2. \n
\n
' 138 | ) 139 | }) 140 | 141 | await t.test('should support a heading', function () { 142 | assert.equal( 143 | renderToStaticMarkup(), 144 | '

a

' 145 | ) 146 | }) 147 | 148 | await t.test('should support an html (default)', function () { 149 | assert.equal( 150 | renderToStaticMarkup(), 151 | '

<i>a</i>

' 152 | ) 153 | }) 154 | 155 | await t.test('should support an html (w/ `rehype-raw`)', function () { 156 | assert.equal( 157 | renderToStaticMarkup( 158 | 159 | ), 160 | '

a

' 161 | ) 162 | }) 163 | 164 | await t.test('should support an image', function () { 165 | assert.equal( 166 | renderToStaticMarkup(), 167 | // Note: React weirdly adds `rel="preload"`. 168 | '

a

' 169 | ) 170 | }) 171 | 172 | await t.test('should support an image w/ a title', function () { 173 | assert.equal( 174 | renderToStaticMarkup(), 175 | // Note: React weirdly adds `rel="preload"`. 176 | '

a

' 177 | ) 178 | }) 179 | 180 | await t.test('should support an image reference / definition', function () { 181 | assert.equal( 182 | renderToStaticMarkup(), 183 | // Note: React weirdly adds `rel="preload"`. 184 | '

a

' 185 | ) 186 | }) 187 | 188 | await t.test('should support code (text, inline)', function () { 189 | assert.equal( 190 | renderToStaticMarkup(), 191 | '

a

' 192 | ) 193 | }) 194 | 195 | await t.test('should support a link', function () { 196 | assert.equal( 197 | renderToStaticMarkup(), 198 | '

a

' 199 | ) 200 | }) 201 | 202 | await t.test('should support a link w/ a title', function () { 203 | assert.equal( 204 | renderToStaticMarkup(), 205 | '

a

' 206 | ) 207 | }) 208 | 209 | await t.test('should support a link reference / definition', function () { 210 | assert.equal( 211 | renderToStaticMarkup(), 212 | '

a

' 213 | ) 214 | }) 215 | 216 | await t.test('should support prototype poluting identifiers', function () { 217 | assert.equal( 218 | renderToStaticMarkup( 219 | 224 | ), 225 | '

' 226 | ) 227 | }) 228 | 229 | await t.test('should support duplicate definitions', function () { 230 | assert.equal( 231 | renderToStaticMarkup(), 232 | '

a

' 233 | ) 234 | }) 235 | 236 | await t.test('should support a list (unordered) / list item', function () { 237 | assert.equal( 238 | renderToStaticMarkup(), 239 | '
    \n
  • a
  • \n
' 240 | ) 241 | }) 242 | 243 | await t.test('should support a list (ordered) / list item', function () { 244 | assert.equal( 245 | renderToStaticMarkup(), 246 | '
    \n
  1. a
  2. \n
' 247 | ) 248 | }) 249 | 250 | await t.test('should support a paragraph', function () { 251 | assert.equal(renderToStaticMarkup(), '

a

') 252 | }) 253 | 254 | await t.test('should support a strong', function () { 255 | assert.equal( 256 | renderToStaticMarkup(), 257 | '

a

' 258 | ) 259 | }) 260 | 261 | await t.test('should support a table (GFM)', function () { 262 | assert.equal( 263 | renderToStaticMarkup( 264 | 268 | ), 269 | '
a
b
' 270 | ) 271 | }) 272 | 273 | await t.test('should support a table (GFM; w/ align)', function () { 274 | assert.equal( 275 | renderToStaticMarkup( 276 | 280 | ), 281 | '
abcd
' 282 | ) 283 | }) 284 | 285 | await t.test('should support a thematic break', function () { 286 | assert.equal(renderToStaticMarkup(), '
') 287 | }) 288 | 289 | await t.test('should support ab absolute path', function () { 290 | assert.equal( 291 | renderToStaticMarkup(), 292 | '

' 293 | ) 294 | }) 295 | 296 | await t.test('should support an absolute URL', function () { 297 | assert.equal( 298 | renderToStaticMarkup(), 299 | '

' 300 | ) 301 | }) 302 | 303 | await t.test('should support a URL w/ uppercase protocol', function () { 304 | assert.equal( 305 | renderToStaticMarkup(), 306 | '

' 307 | ) 308 | }) 309 | 310 | await t.test('should make a `javascript:` URL safe', function () { 311 | assert.equal( 312 | renderToStaticMarkup(), 313 | '

' 314 | ) 315 | }) 316 | 317 | await t.test('should make a `vbscript:` URL safe', function () { 318 | assert.equal( 319 | renderToStaticMarkup(), 320 | '

' 321 | ) 322 | }) 323 | 324 | await t.test('should make a `VBSCRIPT:` URL safe', function () { 325 | assert.equal( 326 | renderToStaticMarkup(), 327 | '

' 328 | ) 329 | }) 330 | 331 | await t.test('should make a `file:` URL safe', function () { 332 | assert.equal( 333 | renderToStaticMarkup(), 334 | '

' 335 | ) 336 | }) 337 | 338 | await t.test('should allow an empty URL', function () { 339 | assert.equal( 340 | renderToStaticMarkup(), 341 | '

' 342 | ) 343 | }) 344 | 345 | await t.test('should support search (`?`) in a URL', function () { 346 | assert.equal( 347 | renderToStaticMarkup(), 348 | '

' 349 | ) 350 | }) 351 | 352 | await t.test('should support hash (`&`) in a URL', function () { 353 | assert.equal( 354 | renderToStaticMarkup(), 355 | '

' 356 | ) 357 | }) 358 | 359 | await t.test('should support hash (`#`) in a URL', function () { 360 | assert.equal( 361 | renderToStaticMarkup(), 362 | '

' 363 | ) 364 | }) 365 | 366 | await t.test('should support `urlTransform` (`href` on `a`)', function () { 367 | assert.equal( 368 | renderToStaticMarkup( 369 | 378 | ), 379 | '

a

' 380 | ) 381 | }) 382 | 383 | await t.test('should support `urlTransform` w/ empty URLs', function () { 384 | assert.equal( 385 | renderToStaticMarkup( 386 | 395 | ), 396 | '

' 397 | ) 398 | }) 399 | 400 | await t.test('should support `urlTransform` (`src` on `img`)', function () { 401 | assert.equal( 402 | renderToStaticMarkup( 403 | 412 | ), 413 | '

a

' 414 | ) 415 | }) 416 | 417 | await t.test('should support `skipHtml`', function () { 418 | const actual = renderToStaticMarkup( 419 | 420 | ) 421 | assert.equal(actual, '

abc

') 422 | }) 423 | 424 | await t.test( 425 | 'should support `allowedElements` (drop unlisted nodes)', 426 | function () { 427 | assert.equal( 428 | renderToStaticMarkup( 429 | 433 | ), 434 | '

\n
    \n
  • b
  • \n
' 435 | ) 436 | } 437 | ) 438 | 439 | await t.test('should support `allowedElements` as a function', function () { 440 | assert.equal( 441 | renderToStaticMarkup( 442 | 448 | ), 449 | '

b

' 450 | ) 451 | }) 452 | await t.test('should support `disallowedElements`', function () { 453 | assert.equal( 454 | renderToStaticMarkup( 455 | 456 | ), 457 | '

\n
    \n
  • b
  • \n
' 458 | ) 459 | }) 460 | 461 | await t.test( 462 | 'should fail for both `allowedElements` and `disallowedElements`', 463 | function () { 464 | assert.throws(function () { 465 | renderToStaticMarkup( 466 | 471 | ) 472 | }, /Unexpected combined `allowedElements` and `disallowedElements`, expected one or the other/) 473 | } 474 | ) 475 | 476 | await t.test( 477 | 'should support `unwrapDisallowed` w/ `allowedElements`', 478 | function () { 479 | assert.equal( 480 | renderToStaticMarkup( 481 | 486 | ), 487 | '

a

' 488 | ) 489 | } 490 | ) 491 | 492 | await t.test( 493 | 'should support `unwrapDisallowed` w/ `disallowedElements`', 494 | function () { 495 | assert.equal( 496 | renderToStaticMarkup( 497 | 502 | ), 503 | '

a

' 504 | ) 505 | } 506 | ) 507 | 508 | await t.test('should support `remarkRehypeOptions`', function () { 509 | assert.equal( 510 | renderToStaticMarkup( 511 | 516 | ), 517 | '

1

\n

Footnotes

\n
    \n
  1. \n

    a

    \n
  2. \n
\n
' 518 | ) 519 | }) 520 | 521 | await t.test('should support `components`', function () { 522 | assert.equal( 523 | renderToStaticMarkup(), 524 | '

a

' 525 | ) 526 | }) 527 | 528 | await t.test('should support `components` as functions', function () { 529 | assert.equal( 530 | renderToStaticMarkup( 531 | 538 | } 539 | }} 540 | /> 541 | ), 542 | '
a
' 543 | ) 544 | }) 545 | 546 | await t.test('should fail on an invalid component', function () { 547 | assert.throws(function () { 548 | renderToStaticMarkup( 549 | 556 | ) 557 | }, /Element type is invalid/) 558 | }) 559 | 560 | await t.test('should support `components` (headings)', function () { 561 | let calls = 0 562 | 563 | assert.equal( 564 | renderToStaticMarkup( 565 | 569 | ), 570 | '

a

\n

b

' 571 | ) 572 | 573 | assert.equal(calls, 2) 574 | 575 | /** 576 | * @param {ComponentProps<'h1'> & ExtraProps} props 577 | */ 578 | function heading(props) { 579 | const {node, ...rest} = props 580 | assert(node) 581 | assert(node.tagName === 'h1' || node.tagName === 'h2') 582 | calls++ 583 | return 584 | } 585 | }) 586 | 587 | await t.test('should support `components` (code)', function () { 588 | let calls = 0 589 | assert.equal( 590 | renderToStaticMarkup( 591 | 600 | } 601 | }} 602 | /> 603 | ), 604 | '
a\n
\n
b\n
\n

c

' 605 | ) 606 | 607 | assert.equal(calls, 3) 608 | }) 609 | 610 | await t.test('should support `components` (li)', function () { 611 | let calls = 0 612 | 613 | assert.equal( 614 | renderToStaticMarkup( 615 | 624 | } 625 | }} 626 | remarkPlugins={[remarkGfm]} 627 | /> 628 | ), 629 | '
    \n
  • a
  • \n
\n
    \n
  1. b
  2. \n
' 630 | ) 631 | 632 | assert.equal(calls, 2) 633 | }) 634 | 635 | await t.test('should support `components` (ol)', function () { 636 | let calls = 0 637 | 638 | assert.equal( 639 | renderToStaticMarkup( 640 | 649 | } 650 | }} 651 | /> 652 | ), 653 | '
    \n
  1. a
  2. \n
' 654 | ) 655 | 656 | assert.equal(calls, 1) 657 | }) 658 | 659 | await t.test('should support `components` (ul)', function () { 660 | let calls = 0 661 | 662 | assert.equal( 663 | renderToStaticMarkup( 664 | 673 | } 674 | }} 675 | /> 676 | ), 677 | '
    \n
  • a
  • \n
' 678 | ) 679 | 680 | assert.equal(calls, 1) 681 | }) 682 | 683 | await t.test('should support `components` (tr)', function () { 684 | let calls = 0 685 | 686 | assert.equal( 687 | renderToStaticMarkup( 688 | 697 | } 698 | }} 699 | remarkPlugins={[remarkGfm]} 700 | /> 701 | ), 702 | '
a
b
' 703 | ) 704 | 705 | assert.equal(calls, 2) 706 | }) 707 | 708 | await t.test('should support `components` (td, th)', function () { 709 | let tdCalls = 0 710 | let thCalls = 0 711 | 712 | assert.equal( 713 | renderToStaticMarkup( 714 | 723 | }, 724 | th(props) { 725 | const {node, ...rest} = props 726 | assert(node) 727 | assert(node.tagName === 'th') 728 | thCalls++ 729 | return 730 | } 731 | }} 732 | remarkPlugins={[remarkGfm]} 733 | /> 734 | ), 735 | '
a
b
' 736 | ) 737 | 738 | assert.equal(tdCalls, 1) 739 | assert.equal(thCalls, 1) 740 | }) 741 | 742 | await t.test('should pass `node` to components', function () { 743 | let calls = 0 744 | assert.equal( 745 | renderToStaticMarkup( 746 | 772 | } 773 | }} 774 | /> 775 | ), 776 | '

a

' 777 | ) 778 | 779 | assert.equal(calls, 1) 780 | }) 781 | 782 | await t.test('should support plugins (`remark-gfm`)', function () { 783 | assert.equal( 784 | renderToStaticMarkup( 785 | 786 | ), 787 | '

a b c

' 788 | ) 789 | }) 790 | 791 | await t.test('should support plugins (`remark-toc`)', function () { 792 | assert.equal( 793 | renderToStaticMarkup( 794 | 798 | ), 799 | `

a

800 |

Contents

801 |
    802 |
  • b 803 |
      804 |
    • c
    • 805 |
    806 |
  • 807 |
  • d
  • 808 |
809 |

b

810 |

c

811 |

d

` 812 | ) 813 | }) 814 | 815 | await t.test('should support aria properties', function () { 816 | assert.equal( 817 | renderToStaticMarkup(), 818 | '

c

' 819 | ) 820 | 821 | function plugin() { 822 | /** 823 | * @param {Root} tree 824 | * @returns {undefined} 825 | */ 826 | return function (tree) { 827 | tree.children.unshift({ 828 | type: 'element', 829 | tagName: 'input', 830 | properties: {id: 'a', ariaDescribedBy: 'b', required: true}, 831 | children: [] 832 | }) 833 | } 834 | } 835 | }) 836 | 837 | await t.test('should support data properties', function () { 838 | assert.equal( 839 | renderToStaticMarkup(), 840 | '

b

' 841 | ) 842 | 843 | function plugin() { 844 | /** 845 | * @param {Root} tree 846 | * @returns {undefined} 847 | */ 848 | return function (tree) { 849 | tree.children.unshift({ 850 | type: 'element', 851 | tagName: 'i', 852 | properties: {dataWhatever: 'a', dataIgnoreThis: undefined}, 853 | children: [] 854 | }) 855 | } 856 | } 857 | }) 858 | 859 | await t.test('should support comma separated properties', function () { 860 | assert.equal( 861 | renderToStaticMarkup(), 862 | '

c

' 863 | ) 864 | 865 | function plugin() { 866 | /** 867 | * @param {Root} tree 868 | * @returns {undefined} 869 | */ 870 | return function (tree) { 871 | tree.children.unshift({ 872 | type: 'element', 873 | tagName: 'i', 874 | properties: {accept: ['a', 'b']}, 875 | children: [] 876 | }) 877 | } 878 | } 879 | }) 880 | 881 | await t.test('should support `style` properties', function () { 882 | assert.equal( 883 | renderToStaticMarkup(), 884 | '

a

' 885 | ) 886 | 887 | function plugin() { 888 | /** 889 | * @param {Root} tree 890 | * @returns {undefined} 891 | */ 892 | return function (tree) { 893 | tree.children.unshift({ 894 | type: 'element', 895 | tagName: 'i', 896 | properties: {style: 'color: red; font-weight: bold'}, 897 | children: [] 898 | }) 899 | } 900 | } 901 | }) 902 | 903 | await t.test( 904 | 'should support `style` properties w/ vendor prefixes', 905 | function () { 906 | assert.equal( 907 | renderToStaticMarkup( 908 | 909 | ), 910 | '

a

' 911 | ) 912 | 913 | function plugin() { 914 | /** 915 | * @param {Root} tree 916 | * @returns {undefined} 917 | */ 918 | return function (tree) { 919 | tree.children.unshift({ 920 | type: 'element', 921 | tagName: 'i', 922 | properties: {style: '-ms-b: 1; -webkit-c: 2'}, 923 | children: [] 924 | }) 925 | } 926 | } 927 | } 928 | ) 929 | 930 | await t.test('should support broken `style` properties', function () { 931 | assert.equal( 932 | renderToStaticMarkup(), 933 | '

a

' 934 | ) 935 | 936 | function plugin() { 937 | /** 938 | * @param {Root} tree 939 | * @returns {undefined} 940 | */ 941 | return function (tree) { 942 | tree.children.unshift({ 943 | type: 'element', 944 | tagName: 'i', 945 | properties: {style: 'broken'}, 946 | children: [] 947 | }) 948 | } 949 | } 950 | }) 951 | 952 | await t.test('should support SVG elements', function () { 953 | assert.equal( 954 | renderToStaticMarkup(), 955 | 'SVG `<circle>` element

a

' 956 | ) 957 | 958 | function plugin() { 959 | /** 960 | * @param {Root} tree 961 | * @returns {undefined} 962 | */ 963 | return function (tree) { 964 | tree.children.unshift({ 965 | type: 'element', 966 | tagName: 'svg', 967 | properties: { 968 | viewBox: '0 0 500 500', 969 | xmlns: 'http://www.w3.org/2000/svg' 970 | }, 971 | children: [ 972 | { 973 | type: 'element', 974 | tagName: 'title', 975 | properties: {}, 976 | children: [{type: 'text', value: 'SVG `` element'}] 977 | }, 978 | { 979 | type: 'element', 980 | tagName: 'circle', 981 | properties: {cx: 120, cy: 120, r: 100}, 982 | children: [] 983 | }, 984 | // `strokeMiterLimit` in hast, `strokeMiterlimit` in React. 985 | { 986 | type: 'element', 987 | tagName: 'path', 988 | properties: {strokeMiterLimit: -1}, 989 | children: [] 990 | } 991 | ] 992 | }) 993 | } 994 | } 995 | }) 996 | 997 | await t.test('should support comments (ignore them)', function () { 998 | const input = 'a' 999 | const actual = renderToStaticMarkup( 1000 | 1001 | ) 1002 | const expected = '

a

' 1003 | assert.equal(actual, expected) 1004 | 1005 | function plugin() { 1006 | /** 1007 | * @param {Root} tree 1008 | * @returns {undefined} 1009 | */ 1010 | return function (tree) { 1011 | tree.children.unshift({type: 'comment', value: 'things!'}) 1012 | } 1013 | } 1014 | }) 1015 | 1016 | await t.test('should support table cells w/ style', function () { 1017 | assert.equal( 1018 | renderToStaticMarkup( 1019 | 1024 | ), 1025 | '
a
' 1026 | ) 1027 | 1028 | function plugin() { 1029 | /** 1030 | * @param {Root} tree 1031 | * @returns {undefined} 1032 | */ 1033 | return function (tree) { 1034 | visit(tree, 'element', function (node) { 1035 | if (node.tagName === 'th') { 1036 | node.properties = {...node.properties, style: 'color: red'} 1037 | } 1038 | }) 1039 | } 1040 | } 1041 | }) 1042 | 1043 | await t.test('should not fail on a plugin replacing `root`', function () { 1044 | assert.equal( 1045 | renderToStaticMarkup(), 1046 | '' 1047 | ) 1048 | 1049 | function plugin() { 1050 | /** 1051 | * @returns {Root} 1052 | */ 1053 | return function () { 1054 | // @ts-expect-error: check how non-roots are handled. 1055 | return {type: 'comment', value: 'things!'} 1056 | } 1057 | } 1058 | }) 1059 | }) 1060 | 1061 | test('MarkdownAsync', async function (t) { 1062 | await t.test('should support `MarkdownAsync` (1)', async function () { 1063 | assert.throws(function () { 1064 | renderToStaticMarkup() 1065 | }, /A component suspended while responding to synchronous input/) 1066 | }) 1067 | 1068 | await t.test('should support `MarkdownAsync` (2)', async function () { 1069 | return new Promise(function (resolve, reject) { 1070 | renderToPipeableStream() 1071 | .pipe( 1072 | concatStream({encoding: 'u8'}, function (data) { 1073 | assert.equal(decoder.decode(data), '

a

') 1074 | resolve() 1075 | }) 1076 | ) 1077 | .on('error', reject) 1078 | }) 1079 | }) 1080 | 1081 | await t.test( 1082 | 'should support async plugins w/ `MarkdownAsync` (`rehype-starry-night`)', 1083 | async function () { 1084 | return new Promise(function (resolve) { 1085 | renderToPipeableStream( 1086 | 1090 | ).pipe( 1091 | concatStream({encoding: 'u8'}, function (data) { 1092 | assert.equal( 1093 | decoder.decode(data), 1094 | '
console.log(3.14)\n
' 1095 | ) 1096 | resolve() 1097 | }) 1098 | ) 1099 | }) 1100 | } 1101 | ) 1102 | }) 1103 | 1104 | // Note: hooks are not supported on the “server”. 1105 | test('MarkdownHooks', async function (t) { 1106 | await t.test('should support `MarkdownHooks`', async function () { 1107 | const plugin = deferPlugin() 1108 | const result = render( 1109 | 1110 | ) 1111 | 1112 | assert.equal(result.container.innerHTML, '') 1113 | 1114 | plugin.resolve() 1115 | 1116 | await waitFor(function () { 1117 | assert.notEqual(result.container.innerHTML, '') 1118 | }) 1119 | 1120 | assert.equal(result.container.innerHTML, '

a

') 1121 | }) 1122 | 1123 | await t.test( 1124 | 'should support async plugins w/ `MarkdownHooks` (`rehype-starry-night`)', 1125 | async function () { 1126 | const plugin = deferPlugin() 1127 | const result = render( 1128 | 1132 | ) 1133 | 1134 | assert.equal(result.container.innerHTML, '') 1135 | 1136 | plugin.resolve() 1137 | 1138 | await waitFor(function () { 1139 | assert.notEqual(result.container.innerHTML, '') 1140 | }) 1141 | 1142 | assert.equal( 1143 | result.container.innerHTML, 1144 | '
console.log(3.14)\n
' 1145 | ) 1146 | } 1147 | ) 1148 | 1149 | await t.test('should support `fallback`', async function () { 1150 | const plugin = deferPlugin() 1151 | const result = render( 1152 | 1157 | ) 1158 | 1159 | assert.equal(result.container.innerHTML, 'Loading') 1160 | 1161 | plugin.resolve() 1162 | 1163 | await waitFor(function () { 1164 | assert.notEqual(result.container.innerHTML, 'Loading') 1165 | }) 1166 | 1167 | assert.equal(result.container.innerHTML, '

a

') 1168 | }) 1169 | 1170 | await t.test('should support plugins that error', async function () { 1171 | const plugin = deferPlugin() 1172 | const result = render( 1173 | 1174 | 1175 | 1176 | ) 1177 | 1178 | assert.equal(result.container.innerHTML, '') 1179 | 1180 | console.info('\nNote: the below error (`Error: rejected`) is expected.\n') 1181 | 1182 | plugin.reject(new Error('rejected')) 1183 | 1184 | await waitFor(function () { 1185 | assert.notEqual(result.container.innerHTML, '') 1186 | }) 1187 | 1188 | console.info('Note: the above error (`Error: rejected`) was expected.') 1189 | 1190 | assert.equal(result.container.innerHTML, 'Error: rejected') 1191 | }) 1192 | 1193 | await t.test('should support rerenders', async function () { 1194 | const pluginA = deferPlugin() 1195 | const pluginB = deferPlugin() 1196 | 1197 | const result = render( 1198 | 1199 | ) 1200 | 1201 | assert.equal(result.container.innerHTML, '') 1202 | 1203 | result.rerender( 1204 | 1205 | ) 1206 | 1207 | assert.equal(result.container.innerHTML, '') 1208 | 1209 | pluginA.resolve() 1210 | pluginB.resolve() 1211 | 1212 | await waitFor(function () { 1213 | assert.notEqual(result.container.innerHTML, '') 1214 | }) 1215 | 1216 | assert.equal(result.container.innerHTML, '

b

') 1217 | }) 1218 | }) 1219 | 1220 | /** 1221 | * Create an async unified plugin that waits until a promise is resolved or 1222 | * rejected from the outside. 1223 | * 1224 | * @returns {DeferredPlugin} 1225 | * Deferred plugin object. 1226 | */ 1227 | function deferPlugin() { 1228 | /** @type {(error: Error) => void} */ 1229 | let hoistedReject 1230 | /** @type {() => void} */ 1231 | let hoistedResolve 1232 | /** @type {Promise} */ 1233 | const promise = new Promise(function (resolve, reject) { 1234 | hoistedResolve = resolve 1235 | hoistedReject = reject 1236 | }) 1237 | 1238 | return { 1239 | plugin() { 1240 | return function () { 1241 | return promise 1242 | } 1243 | }, 1244 | reject(error) { 1245 | hoistedReject(error) 1246 | }, 1247 | resolve() { 1248 | hoistedResolve() 1249 | } 1250 | } 1251 | } 1252 | 1253 | /** 1254 | * Basic error boundary. 1255 | */ 1256 | class ErrorBoundary extends Component { 1257 | /** 1258 | * @param {Error} error 1259 | * Error. 1260 | * @returns {undefined} 1261 | * Nothing. 1262 | */ 1263 | componentDidCatch(error) { 1264 | this.setState({error}) 1265 | } 1266 | 1267 | render() { 1268 | const props = /** @type {{children: ReactNode}} */ (this.props) 1269 | 1270 | return this.state.error ? String(this.state.error) : props.children 1271 | } 1272 | 1273 | state = { 1274 | /** 1275 | * @type {Error | undefined} 1276 | * Error. 1277 | */ 1278 | error: undefined 1279 | } 1280 | } 1281 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "checkJs": true, 4 | "customConditions": ["development"], 5 | "declarationMap": true, 6 | "declaration": true, 7 | "emitDeclarationOnly": true, 8 | "exactOptionalPropertyTypes": true, 9 | "jsx": "preserve", 10 | "lib": ["dom", "es2022"], 11 | "module": "node16", 12 | "strict": true, 13 | "target": "es2022" 14 | }, 15 | "exclude": ["coverage/", "node_modules/"], 16 | "include": ["**/*.js", "**/*.jsx"] 17 | } 18 | --------------------------------------------------------------------------------