diff --git a/.editorconfig b/.editorconfig index c6c8b362..0f178672 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,9 +1,9 @@ root = true [*] -indent_style = space -indent_size = 2 -end_of_line = lf charset = utf-8 -trim_trailing_whitespace = true +end_of_line = lf +indent_size = 2 +indent_style = space insert_final_newline = true +trim_trailing_whitespace = true diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 8b5bc234..00000000 --- a/.eslintignore +++ /dev/null @@ -1,2 +0,0 @@ -coverage/ -react-markdown.min.js diff --git a/.github/workflows/bb.yml b/.github/workflows/bb.yml index 0198fc3f..3dbfce5b 100644 --- a/.github/workflows/bb.yml +++ b/.github/workflows/bb.yml @@ -1,9 +1,3 @@ -name: bb -on: - issues: - types: [opened, reopened, edited, closed, labeled, unlabeled] - pull_request_target: - types: [opened, reopened, edited, closed, labeled, unlabeled] jobs: main: runs-on: ubuntu-latest @@ -11,3 +5,9 @@ jobs: - uses: unifiedjs/beep-boop-beta@main with: repo-token: ${{secrets.GITHUB_TOKEN}} +name: bb +on: + issues: + types: [closed, edited, labeled, opened, reopened, unlabeled] + pull_request_target: + types: [closed, edited, labeled, opened, reopened, unlabeled] diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fe284ad1..ade39213 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,21 +1,21 @@ -name: main -on: - - pull_request - - push jobs: main: name: ${{matrix.node}} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: dcodeIO/setup-node-nvm@master + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: ${{matrix.node}} - run: npm install - run: npm test - - uses: codecov/codecov-action@v1 + - uses: codecov/codecov-action@v5 strategy: matrix: node: - - lts/erbium + - lts/hydrogen - node +name: main +on: + - pull_request + - push diff --git a/.gitignore b/.gitignore index fdbc06a8..fceff11a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,9 @@ -coverage/ -node_modules/ *.d.ts *.log +*.map +*.tsbuildinfo .DS_Store +coverage/ +node_modules/ react-markdown.min.js yarn.lock diff --git a/.npmrc b/.npmrc index 43c97e71..3757b304 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,2 @@ +ignore-scripts=true package-lock=false diff --git a/.prettierignore b/.prettierignore index 95769e01..cebe81f8 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,4 +1,2 @@ coverage/ -*.html *.md -react-markdown.min.js diff --git a/changelog.md b/changelog.md index 06335805..54cd5698 100644 --- a/changelog.md +++ b/changelog.md @@ -1,13 +1,314 @@ -# Change Log + + +# Changelog All notable changes will be documented in this file. +## 10.0.0 - 2025-02-20 + +* [`aaaa40b`](https://github.com/remarkjs/react-markdown/commit/aaaa40b) + Remove support for `className` prop + **migrate**: see “Remove `className`” below + +### Remove `className` + +The `className` prop was removed. +If you want to add classes to some element that wraps the markdown +you can explicitly write that element and add the class to it. +You can then choose yourself which tag name to use and whether to add other +props. + +Before: + +```js +{markdown} +``` + +After: + +```js +
+ {markdown} +
+``` + +## 9.1.0 - 2025-02-20 + +* [`6ce120e`](https://github.com/remarkjs/react-markdown/commit/6ce120e) + Add support for async plugins + +## 9.0.3 - 2025-01-06 + +(same as 9.0.2 but now with d.ts files) + +## 9.0.2 - 2025-01-06 + +* [`b151a90`](https://github.com/remarkjs/react-markdown/commit/b151a90) + Fix types for React 19 +* [`6962af7`](https://github.com/remarkjs/react-markdown/commit/6962af7) + Add declaration maps +* [`aa5933b`](https://github.com/remarkjs/react-markdown/commit/aa5933b) + Refactor to use `@import` to import types + +## 9.0.1 - 2023-11-13 + +* [`d8e3787`](https://github.com/remarkjs/react-markdown/commit/d8e3787) + Fix double encoding in new url transform + +## 9.0.0 - 2023-09-27 + +* [`b67d714`](https://github.com/remarkjs/react-markdown/commit/b67d714) + Change to require Node.js 16\ + **migrate**: update too +* [`ec2b134`](https://github.com/remarkjs/react-markdown/commit/ec2b134) + Change to require React 18\ + **migrate**: update too +* [`bf5824f`](https://github.com/remarkjs/react-markdown/commit/bf5824f) + Change to use `exports`\ + **migrate**: don’t use private APIs +* [`c383a45`](https://github.com/remarkjs/react-markdown/commit/c383a45) + Update `@types/hast`, utilities, plugins, etc\ + **migrate**: update too +* [`eca5e6b`](https://github.com/remarkjs/react-markdown/commit/eca5e6b) + [`08ead9e`](https://github.com/remarkjs/react-markdown/commit/08ead9e) + Replace `transformImageUri`, `transformLinkUri` w/ `urlTransform`\ + **migrate**: see “Add `urlTransform`” below +* [`de29396`](https://github.com/remarkjs/react-markdown/commit/de29396) + Remove `linkTarget` option\ + **migrate**: see “Remove `linkTarget`” below +* [`4346276`](https://github.com/remarkjs/react-markdown/commit/4346276) + Remove support for passing custom props to components\ + **migrate**: see “Remove `includeElementIndex`”, “Remove `rawSourcePos`”, + “Remove `sourcePos`”, “Remove extra props passed to certain components” + below +* [`c0dfbd6`](https://github.com/remarkjs/react-markdown/commit/c0dfbd6) + Remove UMD bundle from package\ + **migrate**: use `esm.sh` or a CDN or so +* [`e12b5e9`](https://github.com/remarkjs/react-markdown/commit/e12b5e9) + Remove `prop-types`\ + **migrate**: use TypeScript +* [`4eb7aa0`](https://github.com/remarkjs/react-markdown/commit/4eb7aa0) + Change to throw errors for removed props\ + **migrate**: don’t pass options that don’t do things +* [`8aabf74`](https://github.com/remarkjs/react-markdown/commit/8aabf74) + Change to improve error messages\ + **migrate**: expect better messages + +### Add `urlTransform` + +The `transformImageUri` and `transformLinkUri` were removed. +Having two functions is a bit much, particularly because there are more URLs +you might want to change (or which might be unsafe so *we* make them safe). +And their name and APIs were a bit weird. +You can use the new `urlTransform` prop instead to change all your URLs. + +### Remove `linkTarget` + +The `linkTarget` option was removed; you should likely not set targets. +If you want to, use +[`rehype-external-links`](https://github.com/rehypejs/rehype-external-links). + +### Remove `includeElementIndex` + +The `includeElementIndex` option was removed, so `index` is never passed to +components. +Write a plugin to pass `index`: + +
+Show example of plugin + +```js +import {visit} from 'unist-util-visit' + +function rehypePluginAddingIndex() { + /** + * @param {import('hast').Root} tree + * @returns {undefined} + */ + return function (tree) { + visit(tree, function (node, index) { + if (node.type === 'element' && typeof index === 'number') { + node.properties.index = index + } + }) + } +} +``` + +
+ +### Remove `rawSourcePos` + +The `rawSourcePos` option was removed, so `sourcePos` is never passed to +components. +All components are passed `node`, so you can get `node.position` from them. + +### Remove `sourcePos` + +The `sourcePos` option was removed, so `data-sourcepos` is never passed to +elements. +Write a plugin to pass `index`: + +
+Show example of plugin + +```js +import {stringifyPosition} from 'unist-util-stringify-position' +import {visit} from 'unist-util-visit' + +function rehypePluginAddingIndex() { + /** + * @param {import('hast').Root} tree + * @returns {undefined} + */ + return function (tree) { + visit(tree, function (node) { + if (node.type === 'element') { + node.properties.dataSourcepos = stringifyPosition(node.position) + } + }) + } +} +``` + +
+ +### Remove extra props passed to certain components + +When overwriting components, these props are no longer passed: + +* `inline` on `code` + — create a plugin or use `pre` for the block +* `level` on `h1`, `h2`, `h3`, `h4`, `h5`, `h6` + — check `node.tagName` instead +* `checked` on `li` + — check `task-list-item` class or check `props.children` +* `index` on `li` + — create a plugin +* `ordered` on `li` + — create a plugin or check the parent +* `depth` on `ol`, `ul` + — create a plugin +* `ordered` on `ol`, `ul` + — check `node.tagName` instead +* `isHeader` on `td`, `th` + — check `node.tagName` instead +* `isHeader` on `tr` + — create a plugin or check children + +## 8.0.7 - 2023-04-12 + +* [`c289176`](https://github.com/remarkjs/react-markdown/commit/c289176) + Fix performance for keys + by [**@wooorm**](https://github.com/wooorm) + in [#738](https://github.com/remarkjs/react-markdown/pull/738) +* [`9034dbd`](https://github.com/remarkjs/react-markdown/commit/9034dbd) + Fix types in syntax highlight example + by [**@dlqqq**](https://github.com/dlqqq) + in [#736](https://github.com/remarkjs/react-markdown/pull/736) + +**Full Changelog**: + +## 8.0.6 - 2023-03-20 + +* [`33ab015`](https://github.com/remarkjs/react-markdown/commit/33ab015) + Update to TS 5\ + by [**@Methuselah96**](https://github.com/Methuselah96) + in [#734](https://github.com/remarkjs/react-markdown/issues/734) + +## 8.0.5 - 2023-01-17 + +* [`d640d40`](https://github.com/remarkjs/react-markdown/commit/d640d40) + Update to use `node16` module resolution in `tsconfig.json`\ + by [**@ChristianMurphy**](https://github.com/ChristianMurphy) + in [#723](https://github.com/remarkjs/react-markdown/pull/723) +* [`402fea3`](https://github.com/remarkjs/react-markdown/commit/402fea3) + Fix typo in `plugins` deprecation message\ + by [**@marc2332**](https://github.com/marc2332) + in [#719](https://github.com/remarkjs/react-markdown/pull/719) +* [`4f98f73`](https://github.com/remarkjs/react-markdown/commit/4f98f73) + Remove deprecated and unneeded `defaultProps`\ + by [**@Lepozepo**](https://github.com/Lepozepo) + in [#718](https://github.com/remarkjs/react-markdown/pull/718) + +## 8.0.4 - 2022-12-01 + +* [`9b20440`](https://github.com/remarkjs/react-markdown/commit/9b20440) + Fix type of `td`, `th` props\ + by [**@lucasassisrosa**](https://github.com/lucasassisrosa) + in [#714](https://github.com/remarkjs/react-markdown/pull/714) +* [`cfe075b`](https://github.com/remarkjs/react-markdown/commit/cfe075b) + Add clarification of `alt` on `img` in docs\ + by [**@cballenar**](https://github.com/cballenar) + in [#692](https://github.com/remarkjs/react-markdown/pull/692) + +## 8.0.3 - 2022-04-20 + +* [`a2fb833`](https://github.com/remarkjs/react-markdown/commit/a2fb833) + Fix prop types of plugins\ + by [**@starpit**](https://github.com/starpit) + in [#683](https://github.com/remarkjs/react-markdown/pull/683) + +## 8.0.2 - 2022-03-31 + +* [`2712227`](https://github.com/remarkjs/react-markdown/commit/2712227) + Update `react-is` +* [`704c3c6`](https://github.com/remarkjs/react-markdown/commit/704c3c6) + Fix TypeScript bug by adding workaround\ + by [**@Methuselah96**](https://github.com/Methuselah96) + in [#676](https://github.com/remarkjs/react-markdown/pull/676) + +## 8.0.1 - 2022-03-14 + +* [`c23ecf6`](https://github.com/remarkjs/react-markdown/commit/c23ecf6) + Add missing dependency for types\ + by [**@Methuselah96**](https://github.com/Methuselah96) + in [#675](https://github.com/remarkjs/react-markdown/pull/675) + +## 8.0.0 - 2022-01-17 + + + +* [`cd845c9`](https://github.com/remarkjs/react-markdown/commit/cd845c9) + Remove deprecated `plugins` option\ + (**migrate by renaming it to `remarkPlugins`**) +* [`36e4916`](https://github.com/remarkjs/react-markdown/commit/36e4916) + Update [`remark-rehype`](https://github.com/remarkjs/remark-rehype), + add support for passing it options\ + by [**@peolic**](https://github.com/peolic) + in [#669](https://github.com/remarkjs/react-markdown/pull/669)\ + (**migrate by removing `remark-footnotes` and updating `remark-gfm` if you + were using them, otherwise you shouldn’t notice this**) + +## 7.1.2 - 2022-01-02 + +* [`656a4fa`](https://github.com/remarkjs/react-markdown/commit/656a4fa) + Fix `ref` in types\ + by [**@ChristianMurphy**](https://github.com/ChristianMurphy) + in [#668](https://github.com/remarkjs/react-markdown/pull/668) + +## 7.1.1 - 2021-11-29 + +* [`4185f06`](https://github.com/remarkjs/react-markdown/commit/4185f06) + Add improved docs\ + by [**@wooorm**](https://github.com/wooorm) + in [#657](https://github.com/remarkjs/react-markdown/pull/657) + +## 7.1.0 - 2021-10-21 + +* [`7b8a829`](https://github.com/remarkjs/react-markdown/commit/7b8a829) + Add support for `SpecialComponents` to be any `ComponentType`\ + by [**@Methuselah96**](https://github.com/Methuselah96) + in [#640](https://github.com/remarkjs/react-markdown/pull/640) +* [`a7c26fc`](https://github.com/remarkjs/react-markdown/commit/a7c26fc) + Remove warning on whitespace in tables + ## 7.0.1 - 2021-08-26 -* [`ec387c2`](https://github.com/remarkjs/react-markdown/commit/ec387c2) - Add improved type for `linkTarget` as string -* [`5af6bc7`](https://github.com/remarkjs/react-markdown/commit/5af6bc7) - Fix to correctly compile intrinsic types +* [`ec387c2`](https://github.com/remarkjs/react-markdown/commit/ec387c2) + Add improved type for `linkTarget` as string +* [`5af6bc7`](https://github.com/remarkjs/react-markdown/commit/5af6bc7) + Fix to correctly compile intrinsic types ## 7.0.0 - 2021-08-13 @@ -16,51 +317,51 @@ This a major release and therefore contains breaking changes. ### Breaking changes -* [`01b11fe`](https://github.com/remarkjs/react-markdown/commit/01b11fe) - [`c613efd`](https://github.com/remarkjs/react-markdown/commit/c613efd) - [`a1e1c3f`](https://github.com/remarkjs/react-markdown/commit/a1e1c3f) - [`aeee9ac`](https://github.com/remarkjs/react-markdown/commit/aeee9ac) - Use ESM - (please [read this](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c)) -* [`3dffd6a`](https://github.com/remarkjs/react-markdown/commit/3dffd6a) - Update dependencies - (upgrade all your plugins and this should go fine) +* [`01b11fe`](https://github.com/remarkjs/react-markdown/commit/01b11fe) + [`c613efd`](https://github.com/remarkjs/react-markdown/commit/c613efd) + [`a1e1c3f`](https://github.com/remarkjs/react-markdown/commit/a1e1c3f) + [`aeee9ac`](https://github.com/remarkjs/react-markdown/commit/aeee9ac) + Use ESM + (please [read this](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c)) +* [`3dffd6a`](https://github.com/remarkjs/react-markdown/commit/3dffd6a) + Update dependencies + (upgrade all your plugins and this should go fine) ### Internals -* [`8b5481c`](https://github.com/remarkjs/react-markdown/commit/8b5481c) - [`fb1b512`](https://github.com/remarkjs/react-markdown/commit/fb1b512) - [`144af79`](https://github.com/remarkjs/react-markdown/commit/144af79) - Replace `jest` with `uvu` -* [`8c572df`](https://github.com/remarkjs/react-markdown/commit/8c572df) - Replace `rollup` with `esbuild` -* [`8737eac`](https://github.com/remarkjs/react-markdown/commit/8737eac) - [`28d4c75`](https://github.com/remarkjs/react-markdown/commit/28d4c75) - [`b2dd046`](https://github.com/remarkjs/react-markdown/commit/b2dd046) - Refactor code-style +* [`8b5481c`](https://github.com/remarkjs/react-markdown/commit/8b5481c) + [`fb1b512`](https://github.com/remarkjs/react-markdown/commit/fb1b512) + [`144af79`](https://github.com/remarkjs/react-markdown/commit/144af79) + Replace `jest` with `uvu` +* [`8c572df`](https://github.com/remarkjs/react-markdown/commit/8c572df) + Replace `rollup` with `esbuild` +* [`8737eac`](https://github.com/remarkjs/react-markdown/commit/8737eac) + [`28d4c75`](https://github.com/remarkjs/react-markdown/commit/28d4c75) + [`b2dd046`](https://github.com/remarkjs/react-markdown/commit/b2dd046) + Refactor code-style ## 6.0.3 - 2021-07-30 -* [`13367ed`](https://github.com/remarkjs/react-markdown/commit/13367ed) - Fix types to include each element w/ its properties -* [`0a1931a`](https://github.com/remarkjs/react-markdown/commit/0a1931a) - Fix to add min version of `property-information` +* [`13367ed`](https://github.com/remarkjs/react-markdown/commit/13367ed) + Fix types to include each element w/ its properties +* [`0a1931a`](https://github.com/remarkjs/react-markdown/commit/0a1931a) + Fix to add min version of `property-information` ## 6.0.2 - 2021-05-06 -* [`cefc02d`](https://github.com/remarkjs/react-markdown/commit/cefc02d) - Add string type for `className`s -* [`6355e45`](https://github.com/remarkjs/react-markdown/commit/6355e45) - Fix to pass `vfile` to plugins -* [`5cf6e1b`](https://github.com/remarkjs/react-markdown/commit/5cf6e1b) - Fix to add warning when non-strings are given as `children` +* [`cefc02d`](https://github.com/remarkjs/react-markdown/commit/cefc02d) + Add string type for `className`s +* [`6355e45`](https://github.com/remarkjs/react-markdown/commit/6355e45) + Fix to pass `vfile` to plugins +* [`5cf6e1b`](https://github.com/remarkjs/react-markdown/commit/5cf6e1b) + Fix to add warning when non-strings are given as `children` ## 6.0.1 - 2021-04-23 -* [`2e956be`](https://github.com/remarkjs/react-markdown/commit/2e956be) - Fix whitespace in table elements -* [`d36048a`](https://github.com/remarkjs/react-markdown/commit/d36048a) - Add architecture section to readme +* [`2e956be`](https://github.com/remarkjs/react-markdown/commit/2e956be) + Fix whitespace in table elements +* [`d36048a`](https://github.com/remarkjs/react-markdown/commit/d36048a) + Add architecture section to readme ## 6.0.0 - 2021-04-15 @@ -85,7 +386,7 @@ for more on components. Before (**broken**): -```jsx +```js Show conversion table -| Type (`renderers`) | Tag names (`components`) | -| - | - | -| `blockquote` | `blockquote` | -| `break` | `br` | -| `code`, `inlineCode` | `code`, `pre`**​\*​** | -| `definition` | **†** | -| `delete` | `del`**‡** | -| `emphasis` | `em` | -| `heading` | `h1`, `h2`, `h3`, `h4`, `h5`, `h6`**§** | -| `html`, `parsedHtml`, `virtualHtml` | **‖** | -| `image`, `imageReference` | `img`**†** | -| `link`, `linkReference` | `a`**†** | -| `list` | `ol`, `ul`**¶** | -| `listItem` | `li` | -| `paragraph` | `p` | -| `root` | **​\*\*​** | -| `strong` | `strong` | -| `table` | `table`**‡** | -| `tableHead` | `thead`**‡** | -| `tableBody` | `tbody`**‡** | -| `tableRow` | `tr`**‡** | -| `tableCell` | `td`, `th`**‡** | -| `text` | | -| `thematicBreak` | `hr` | - -* **​\*​** It’s possible to differentiate between code based on the `inline` - prop. - Block code is also wrapped in a `pre` -* **†** Resource (`[text](url)`) and reference (`[text][id]`) style links and - images (and their definitions) are now resolved and treated the same -* **‡** Available when using - [`remark-gfm`](https://github.com/remarkjs/remark-gfm) -* **§** It’s possible to differentiate between heading based on the `level` - prop -* **‖** When using `rehype-raw` (see below), components for those elements - can also be used (for example, `abbr` for - `HTML`) -* **¶** It’s possible to differentiate between lists based on the `ordered` - prop -* **​\*\*​** Wrap `ReactMarkdown` in a component instead +| Type (`renderers`) | Tag names (`components`) | +| ----------------------------------- | --------------------------------------- | +| `blockquote` | `blockquote` | +| `break` | `br` | +| `code`, `inlineCode` | `code`, `pre`**​\*​** | +| `definition` | **†** | +| `delete` | `del`**‡** | +| `emphasis` | `em` | +| `heading` | `h1`, `h2`, `h3`, `h4`, `h5`, `h6`**§** | +| `html`, `parsedHtml`, `virtualHtml` | **‖** | +| `image`, `imageReference` | `img`**†** | +| `link`, `linkReference` | `a`**†** | +| `list` | `ol`, `ul`**¶** | +| `listItem` | `li` | +| `paragraph` | `p` | +| `root` | **​\*\*​** | +| `strong` | `strong` | +| `table` | `table`**‡** | +| `tableHead` | `thead`**‡** | +| `tableBody` | `tbody`**‡** | +| `tableRow` | `tr`**‡** | +| `tableCell` | `td`, `th`**‡** | +| `text` | | +| `thematicBreak` | `hr` | + +* **​\*​** It’s possible to differentiate between code based on the `inline` + prop. + Block code is also wrapped in a `pre` +* **†** Resource (`[text](url)`) and reference (`[text][id]`) style links and + images (and their definitions) are now resolved and treated the same +* **‡** Available when using + [`remark-gfm`](https://github.com/remarkjs/remark-gfm) +* **§** It’s possible to differentiate between heading based on the `level` + prop +* **‖** When using `rehype-raw` (see below), components for those elements + can also be used (for example, `abbr` for + `HTML`) +* **¶** It’s possible to differentiate between lists based on the `ordered` + prop +* **​\*\*​** Wrap `ReactMarkdown` in a component instead @@ -174,7 +475,7 @@ for more plugins.
Show example of feature -```jsx +```js import rehypeHighlight from 'rehype-highlight' {`~~~js @@ -206,7 +507,7 @@ too. Before (**broken**): -```jsx +```js import MarkdownWithHtml from 'react-markdown/with-html' {`# Hello, world!`} @@ -214,7 +515,7 @@ import MarkdownWithHtml from 'react-markdown/with-html' Now (**fixed**): -```jsx +```js import Markdown from 'react-markdown' import rehypeRaw from 'rehype-raw' import rehypeSanitize from 'rehype-sanitize' @@ -233,20 +534,20 @@ Instead of passing a `source` pass `children` instead: Before (**broken**): -```jsx +```js ``` Now (**fixed**): -```jsx +```js {`some markdown`} ``` Or (**also fixed**): -```jsx +```js ``` @@ -265,7 +566,7 @@ names: `allowNode` to `allowElement`, `allowedTypes` to `allowedElements`, and Before (**broken**): -```jsx +```js node.type !== 'heading' || node.depth !== 1} @@ -294,7 +595,7 @@ Before (**broken**): Now (**fixed**): -```jsx +```js element.tagName !== 'h1'} @@ -313,7 +614,7 @@ to components also changed from being based on markdown to being based on HTML. Before (**broken**): -```jsx +```js }} … /> ``` Should now be written as: -```jsx +```js }} … /> ``` @@ -428,497 +729,497 @@ slightly differently. ### Fixes -* (Typings) Fix incorrect typescript definitions (Peng Guanwen) +* (Typings) Fix incorrect typescript definitions (Peng Guanwen) ## 4.3.0 - 2020-01-02 ### Fixes -* (Typings) Add typings for `react-markdown/html-parser` (Peng Guanwen) +* (Typings) Add typings for `react-markdown/html-parser` (Peng Guanwen) ## 4.2.2 - 2019-09-03 ### Fixes -* (Typings) Inline `RemarkParseOptions` for now (Espen Hovlandsdal) +* (Typings) Inline `RemarkParseOptions` for now (Espen Hovlandsdal) ## 4.2.1 - 2019-09-01 ### Fixes -* (Typings) Fix incorrect import - `RemarkParseOptions` (Jakub Chrzanowski) +* (Typings) Fix incorrect import - `RemarkParseOptions` (Jakub Chrzanowski) ## 4.2.0 - 2019-09-01 ### Added -* Add support for plugins that use AST transformations (Frankie Ali) +* Add support for plugins that use AST transformations (Frankie Ali) ### Fixes -* (Typings) Add `parserOptions` to type defintions (Ted Piotrowski) -* Allow renderer to be any React element type (Nathan Bierema) +* (Typings) Add `parserOptions` to type definitions (Ted Piotrowski) +* Allow renderer to be any React element type (Nathan Bierema) ## 4.1.0 - 2019-06-24 ### Added -* Add prop `parserOptions` to specify options for remark-parse (Kelvin Chan) +* Add prop `parserOptions` to specify options for remark-parse (Kelvin Chan) ## 4.0.9 - 2019-06-22 ### Fixes -* (Typings) Make transformLinkUri & transformImageUri actually nullable - (Florentin Luca Rieger) +* (Typings) Make transformLinkUri & transformImageUri actually nullable + (Florentin Luca Rieger) ## 4.0.8 - 2019-04-14 ### Fixes -* Fix HTML parsing of elements with a single child vs. multiple children - (Nicolas Venegas) +* Fix HTML parsing of elements with a single child vs. multiple children + (Nicolas Venegas) ## 4.0.7 - 2019-04-14 ### Fixes -* Fix matching of replaced non-void elements in HTML parser plugin (Nicolas - Venegas) -* Fix HTML parsing of multiple void elements (Nicolas Venegas) -* Fix void element children invariant violation (Nicolas Venegas) +* Fix matching of replaced non-void elements in HTML parser plugin (Nicolas + Venegas) +* Fix HTML parsing of multiple void elements (Nicolas Venegas) +* Fix void element children invariant violation (Nicolas Venegas) ## 4.0.6 - 2019-01-04 ### Fixes -* Mitigate regex ddos by upgrading html-to-react (Christoph Werner) -* Update typings to allow arbitrary node types (Jesse Pinho) -* Readme: Add note about only parsing plugins working (Vincent Tunru) +* Mitigate regex ddos by upgrading html-to-react (Christoph Werner) +* Update typings to allow arbitrary node types (Jesse Pinho) +* Readme: Add note about only parsing plugins working (Vincent Tunru) ## 4.0.4 - 2018-11-30 ### Changed -* Upgrade dependencies (Espen Hovlandsdal) +* Upgrade dependencies (Espen Hovlandsdal) ## 4.0.3 - 2018-10-11 ### Fixes -* Output paragraph element for last item in loose list (Jeremy Moseley) +* Output paragraph element for last item in loose list (Jeremy Moseley) ## 4.0.2 - 2018-10-05 ### Fixes -* Fix text rendering in React versions lower than or equal to 15 (Espen - Hovlandsdal) +* Fix text rendering in React versions lower than or equal to 15 (Espen + Hovlandsdal) ## 4.0.1 - 2018-10-03 ### Fixes -* \[TypeScript] Fix TypeScript index signature for renderers (Linus Unnebäck) +* \[TypeScript] Fix TypeScript index signature for renderers (Linus Unnebäck) ## 4.0.0 - 2018-10-03 ### BREAKING -* `text` is now a first-class node + renderer - — if you are using `allowedNodes`, it needs to be included in this list. - Since it is now a React component, it will be passed an object of props - instead of the old approach where a string was passed. - `children` will contain the actual text string. -* On React >= 16.2, if no `className` prop is provided, a fragment will be - used instead of a div. - To always render a div, pass `'div'` as the `root` renderer. -* On React >= 16.2, escaped HTML will no longer be rendered with div/span - containers -* The UMD bundle now exports the component as `window.ReactMarkdown` instead - of `window.reactMarkdown` +* `text` is now a first-class node + renderer + — if you are using `allowedNodes`, it needs to be included in this list. + Since it is now a React component, it will be passed an object of props + instead of the old approach where a string was passed. + `children` will contain the actual text string. +* On React >= 16.2, if no `className` prop is provided, a fragment will be + used instead of a div. + To always render a div, pass `'div'` as the `root` renderer. +* On React >= 16.2, escaped HTML will no longer be rendered with div/span + containers +* The UMD bundle now exports the component as `window.ReactMarkdown` instead + of `window.reactMarkdown` ### Added -* HTML parser plugin for full HTML compatibility (Espen Hovlandsdal) +* HTML parser plugin for full HTML compatibility (Espen Hovlandsdal) ### Fixes -* URI transformer allows uppercase http/https URLs (Liam Kennedy) -* \[TypeScript] Strongly type the keys of `renderers` (Linus Unnebäck) +* URI transformer allows uppercase http/https URLs (Liam Kennedy) +* \[TypeScript] Strongly type the keys of `renderers` (Linus Unnebäck) ## 3.6.0 - 2018-09-05 ### Added -* Add support for passing index info to renderers (Beau Roberts) +* Add support for passing index info to renderers (Beau Roberts) ## 3.5.0 - 2018-09-03 ### Added -* Allow specifying `target` attribute for links (Marshall Smith) +* Allow specifying `target` attribute for links (Marshall Smith) ## 3.4.1 - 2018-07-25 ### Fixes -* Bump dependency for mdast-add-list-metadata as it was using ES6 features - (Espen Hovlandsdal) +* Bump dependency for mdast-add-list-metadata as it was using ES6 features + (Espen Hovlandsdal) ## 3.4.0 - 2018-07-25 ### Added -* Add more metadata props to list and listItem (André Staltz) - * list: `depth` - * listItem: `ordered`, `index` +* Add more metadata props to list and listItem (André Staltz) + * list: `depth` + * listItem: `ordered`, `index` ### Fixes -* Make `source` property optional in typescript definition (gRoberts84) +* Make `source` property optional in typescript definition (gRoberts84) ## 3.3.4 - 2018-06-19 ### Fixes -* Fix bug where rendering empty link references (`[][]`) would fail (Dennis S) +* Fix bug where rendering empty link references (`[][]`) would fail (Dennis S) ## 3.3.3 - 2018-06-14 ### Fixes -* Fix bug where unwrapping certain disallowed nodes would fail (Petr Gazarov) +* Fix bug where unwrapping certain disallowed nodes would fail (Petr Gazarov) ## 3.3.2 - 2018-05-07 ### Changes -* Add `rawSourcePos` property for passing structured source position info to - renderers (Espen Hovlandsdal) +* Add `rawSourcePos` property for passing structured source position info to + renderers (Espen Hovlandsdal) ## 3.3.1 - 2018-05-07 ### Changes -* Pass properties of unknown nodes directly to renderer (Jesse Pinho) -* Update TypeScript definition and prop types (ClassicDarkChocolate) +* Pass properties of unknown nodes directly to renderer (Jesse Pinho) +* Update TypeScript definition and prop types (ClassicDarkChocolate) ## 3.3.0 - 2018-03-06 ### Added -* Add support for fragment renderers (Benjamim Sonntag) +* Add support for fragment renderers (Benjamim Sonntag) ## 3.2.2 - 2018-02-26 ### Fixes -* Fix language escaping in code blocks (Espen Hovlandsdal) +* Fix language escaping in code blocks (Espen Hovlandsdal) ## 3.2.1 - 2018-02-21 ### Fixes -* Pass the React key into an overridden text renderer (vanchagreen) +* Pass the React key into an overridden text renderer (vanchagreen) ## 3.2.0 - 2018-02-12 ### Added -* Allow overriding text renderer (Thibaud Courtoison) +* Allow overriding text renderer (Thibaud Courtoison) ## 3.1.5 - 2018-02-03 ### Fixes -* Only use first language from code block (Espen Hovlandsdal) +* Only use first language from code block (Espen Hovlandsdal) ## 3.1.4 - 2017-12-30 ### Fixes -* Enable transformImageUri for image references (evoye) +* Enable transformImageUri for image references (evoye) ## 3.1.3 - 2017-12-16 ### Fixes -* Exclude babel config from npm package (Espen Hovlandsdal) +* Exclude babel config from npm package (Espen Hovlandsdal) ## 3.1.2 - 2017-12-16 ### Fixes -* Fixed partial table exception (Alexander Wong) +* Fixed partial table exception (Alexander Wong) ## 3.1.1 - 2017-12-11 ### Fixes -* Add readOnly property to checkboxes (Phil Rajchgot) +* Add readOnly property to checkboxes (Phil Rajchgot) ## 3.1.0 - 2017-11-30 ### Added -* Support for checkbox lists (Espen Hovlandsdal) +* Support for checkbox lists (Espen Hovlandsdal) ### Fixes -* Better typings (Igor Kamyshev) +* Better typings (Igor Kamyshev) ## 3.0.1 - 2017-11-21 ### Added -* *Experimental* support for plugins (Espen Hovlandsdal) +* *Experimental* support for plugins (Espen Hovlandsdal) ### Changes -* Provide more arguments to `transformLinkUri`/`transformImageUri` (children, - title, alt) (mudrz) +* Provide more arguments to `transformLinkUri`/`transformImageUri` (children, + title, alt) (mudrz) ## 3.0.0 - 2017-11-20 ### Notes -* **FULL REWRITE**. - Changed parser from CommonMark to Markdown. - Big, breaking changes. - See *BREAKING* below. +* **FULL REWRITE**. + Changed parser from CommonMark to Markdown. + Big, breaking changes. + See *BREAKING* below. ### Added -* Table support! - * New types: `table`, `tableHead`, `tableBody`, `tableRow`, `tableCell` -* New type: `delete` (`~~foo~~`) -* New type: `imageReference` -* New type: `linkReference` -* New type: `definition` -* Hacky, but basic support for React-native rendering of attributeless HTML - nodes (``, ``, etc) +* Table support! + * New types: `table`, `tableHead`, `tableBody`, `tableRow`, `tableCell` +* New type: `delete` (`~~foo~~`) +* New type: `imageReference` +* New type: `linkReference` +* New type: `definition` +* Hacky, but basic support for React-native rendering of attributeless HTML + nodes (``, ``, etc) ### BREAKING -* Container props removed (`containerTagName`, `containerProps`), override - `root` renderer instead -* `softBreak` option removed. - New solution will be added at some point in the future. -* `escapeHtml` is now TRUE by default -* `HtmlInline`/`HtmlBlock` are now named `html` (use `isBlock` prop to check\ - if inline or block) -* Renderer names are camelcased and in certain cases, renamed. - For instance: - * `Emph` => `emphasis` - * `Item` => `listItem` - * `Code` => `inlineCode` - * `CodeBlock` => `code` - * `linebreak`/`hardbreak` => `break` -* All renderers: `literal` prop is now called `value`\* List renderer: `type` - prop is now a boolean named `ordered` (`Bullet` => `false`, `Ordered` => - `true`) -* `walker` prop removed. - Code depending on this will have to be rewritten to use the `astPlugins` - prop, which functions differently. -* `allowNode` has new arguments (node, index, parent) - — node has different props, see renderer props -* `childBefore` and `childAfter` props removed. - Use `root` renderer instead. -* `parserOptions` removed (new parser, so the old options doesn’t make sense - anymore) +* Container props removed (`containerTagName`, `containerProps`), override + `root` renderer instead +* `softBreak` option removed. + New solution will be added at some point in the future. +* `escapeHtml` is now TRUE by default +* `HtmlInline`/`HtmlBlock` are now named `html` (use `isBlock` prop to check\ + if inline or block) +* Renderer names are camelcased and in certain cases, renamed. + For instance: + * `Emph` => `emphasis` + * `Item` => `listItem` + * `Code` => `inlineCode` + * `CodeBlock` => `code` + * `linebreak`/`hardbreak` => `break` +* All renderers: `literal` prop is now called `value`\* List renderer: `type` + prop is now a boolean named `ordered` (`Bullet` => `false`, `Ordered` => + `true`) +* `walker` prop removed. + Code depending on this will have to be rewritten to use the `astPlugins` + prop, which functions differently. +* `allowNode` has new arguments (node, index, parent) + — node has different props, see renderer props +* `childBefore` and `childAfter` props removed. + Use `root` renderer instead. +* `parserOptions` removed (new parser, so the old options doesn’t make sense + anymore) ## 2.5.1 - 2017-11-11 ### Changes -* Fix `
` not having a node key (Alex Zaworski) +* Fix `
` not having a node key (Alex Zaworski) ## 2.5.0 - 2017-04-10 ### Changes -* Fix deprecations for React v15.5 (Renée Kooi) +* Fix deprecations for React v15.5 (Renée Kooi) ## 2.4.6 - 2017-03-14 ### Changes -* Fix too strict TypeScript definition (Rasmus Eneman) -* Update JSON-loader info in readme to match webpack 2 (Robin Wieruch) +* Fix too strict TypeScript definition (Rasmus Eneman) +* Update JSON-loader info in readme to match webpack 2 (Robin Wieruch) ### Added -* Add ability to pass options to the CommonMark parser (Evan Hensleigh) +* Add ability to pass options to the CommonMark parser (Evan Hensleigh) ## 2.4.4 - 2017-01-16 ### Changes -* Fixed TypeScript definitions (Kohei Asai) +* Fixed TypeScript definitions (Kohei Asai) ## 2.4.3 - 2017-01-12 ### Added -* Added TypeScript definitions (Ibragimov Ruslan) +* Added TypeScript definitions (Ibragimov Ruslan) ## 2.4.2 - 2016-07-09 ### Added -* Added UMD-build (`umd/react-markdown.js`) (Espen Hovlandsdal) +* Added UMD-build (`umd/react-markdown.js`) (Espen Hovlandsdal) ## 2.4.1 - 2016-07-09 ### Changes -* Update `commonmark-react-renderer`, fixing a bug with missing nodes - (Espen Hovlandsdal) +* Update `commonmark-react-renderer`, fixing a bug with missing nodes + (Espen Hovlandsdal) ## 2.4.0 - 2016-07-09 ### Changes -* Plain DOM-node renderers are now given only their respective props. - Fixes warnings when using React >= 15.2 (Espen Hovlandsdal) +* Plain DOM-node renderers are now given only their respective props. + Fixes warnings when using React >= 15.2 (Espen Hovlandsdal) ### Added -* New `transformImageUri` option allows you to transform URIs for images - (Petri Lehtinen) +* New `transformImageUri` option allows you to transform URIs for images + (Petri Lehtinen) ## 2.3.0 - 2016-06-06 ## Added -* The `walker` instance is now passed to the `walker` callback function - (Riku Rouvila) +* The `walker` instance is now passed to the `walker` callback function + (Riku Rouvila) ## 2.2.0 - 2016-04-20 -* Add `childBefore`/`childAfter` options (Thomas Lindstrøm) +* Add `childBefore`/`childAfter` options (Thomas Lindstrøm) ## 2.1.1 - 2016-03-25 -* Add `containerProps` option (Thomas Lindstrøm) +* Add `containerProps` option (Thomas Lindstrøm) ## 2.1.0 - 2016-03-12 ### Changes -* Join sibling text nodes into one text node (Espen Hovlandsdal) +* Join sibling text nodes into one text node (Espen Hovlandsdal) ## 2.0.1 - 2016-02-21 ### Changed -* Update `commonmark-react-renderer` dependency to latest version to add keys - to all elements and simplify custom renderers +* Update `commonmark-react-renderer` dependency to latest version to add keys + to all elements and simplify custom renderers ## 2.0.0 - 2016-02-21 ### Changed -* **Breaking change**: The renderer now requires Node 0.14 or higher. - This is because the renderer uses stateless components internally. -* **Breaking change**: `allowNode` now receives different properties in the - options argument. - See `README.md` for more details. -* **Breaking change**: CommonMark has changed some type names. - `Html` is now `HtmlInline`, `Header` is now `Heading` and `HorizontalRule` - is now `ThematicBreak`. - This affects the `allowedTypes` and `disallowedTypes` options. -* **Breaking change**: A bug in the `allowedTypes`/`disallowedTypes` and - `allowNode` options made them only applicable to certain types. - In this version, all types are filtered, as expected. -* **Breaking change**: Link URIs are now filtered through an XSS-filter by - default, prefixing “dangerous” protocols such as `javascript:` with `x-` - (eg: `javascript:alert('foo')` turns into `x-javascript:alert('foo')`). - This can be overridden with the `transformLinkUri`-option. - Pass `null` to disable the feature or a custom function to replace the - built-in behaviour. +* **Breaking change**: The renderer now requires Node 0.14 or higher. + This is because the renderer uses stateless components internally. +* **Breaking change**: `allowNode` now receives different properties in the + options argument. + See `README.md` for more details. +* **Breaking change**: CommonMark has changed some type names. + `Html` is now `HtmlInline`, `Header` is now `Heading` and `HorizontalRule` + is now `ThematicBreak`. + This affects the `allowedTypes` and `disallowedTypes` options. +* **Breaking change**: A bug in the `allowedTypes`/`disallowedTypes` and + `allowNode` options made them only applicable to certain types. + In this version, all types are filtered, as expected. +* **Breaking change**: Link URIs are now filtered through an XSS-filter by + default, prefixing “dangerous” protocols such as `javascript:` with `x-` + (eg: `javascript:alert('foo')` turns into `x-javascript:alert('foo')`). + This can be overridden with the `transformLinkUri`-option. + Pass `null` to disable the feature or a custom function to replace the + built-in behaviour. ### Added -* New `renderers` option allows you to customize which React component should - be used for rendering given types. - See `README.md` for more details. - (Espen Hovlandsdal / Guillaume Plique) -* New `unwrapDisallowed` option allows you to select if the contents of a - disallowed node should be “unwrapped” (placed into the disallowed node - position). - For instance, setting this option to true and disallowing a link would still - render the text of the link, instead of the whole link node and all it’s - children disappearing. - (Espen Hovlandsdal) -* New `transformLinkUri` option allows you to transform URIs in links. - By default, an XSS-filter is used, but you could also use this for use cases - like transforming absolute to relative URLs, or similar. - (Espen Hovlandsdal) +* New `renderers` option allows you to customize which React component should + be used for rendering given types. + See `README.md` for more details. + (Espen Hovlandsdal / Guillaume Plique) +* New `unwrapDisallowed` option allows you to select if the contents of a + disallowed node should be “unwrapped” (placed into the disallowed node + position). + For instance, setting this option to true and disallowing a link would still + render the text of the link, instead of the whole link node and all it’s + children disappearing. + (Espen Hovlandsdal) +* New `transformLinkUri` option allows you to transform URIs in links. + By default, an XSS-filter is used, but you could also use this for use cases + like transforming absolute to relative URLs, or similar. + (Espen Hovlandsdal) ## 1.2.4 - 2016-01-28 ### Changed -* Rolled back dependencies because of breaking changes +* Rolled back dependencies because of breaking changes ## 1.2.3 - 2016-01-24 ### Changed -* Updated dependencies for both `commonmark` and `commonmark-react-parser` to - work around an embarrassing oversight on my part. +* Updated dependencies for both `commonmark` and `commonmark-react-parser` to + work around an embarrassing oversight on my part. ## 1.2.2 - 2016-01-08 ### Changed -* Reverted change from 1.2.1 that uses the dist version. - Instead, documentation is added that specified the need for `json-loader` to - be enabled when using webpack. +* Reverted change from 1.2.1 that uses the dist version. + Instead, documentation is added that specified the need for `json-loader` to + be enabled when using webpack. ## 1.2.1 - 2015-12-29 ### Fixed -* Use pre-built (dist version) of commonmark renderer in order to work around - JSON-loader dependency. +* Use pre-built (dist version) of commonmark renderer in order to work around + JSON-loader dependency. ## 1.2.0 - 2015-12-16 ### Added -* Added new `allowNode`-property. - See README for details. +* Added new `allowNode`-property. + See README for details. ## 1.1.4 - 2015-12-14 ### Fixed -* Set correct `libraryTarget` to make UMD builds work as expected +* Set correct `libraryTarget` to make UMD builds work as expected ## 1.1.3 - 2015-12-14 ### Fixed -* Update babel dependencies and run prepublish only as actual prepublish, not - install +* Update babel dependencies and run prepublish only as actual prepublish, not + install ## 1.1.1 - 2015-11-28 ### Fixed -* Fixed issue with React external name in global environment (`react` vs `React`) +* Fixed issue with React external name in global environment (`react` vs `React`) ## 1.1.0 - 2015-11-22 ### Changed -* Add ability to allow/disallow specific node types (`allowedTypes`/`disallowedTypes`) +* Add ability to allow/disallow specific node types (`allowedTypes`/`disallowedTypes`) ## 1.0.5 - 2015-10-22 ### Changed -* Moved React from dependency to peer dependency. +* Moved React from dependency to peer dependency. diff --git a/index.js b/index.js index 918d3ff5..d0fc80e0 100644 --- a/index.js +++ b/index.js @@ -1,8 +1,15 @@ /** - * @typedef {import('./lib/react-markdown.js').ReactMarkdownOptions} Options - * @typedef {import('./lib/ast-to-react.js').Components} Components + * @typedef {import('./lib/index.js').AllowElement} AllowElement + * @typedef {import('./lib/index.js').Components} Components + * @typedef {import('./lib/index.js').ExtraProps} ExtraProps + * @typedef {import('./lib/index.js').HooksOptions} HooksOptions + * @typedef {import('./lib/index.js').Options} Options + * @typedef {import('./lib/index.js').UrlTransform} UrlTransform */ -export {uriTransformer} from './lib/uri-transformer.js' - -export {ReactMarkdown as default} from './lib/react-markdown.js' +export { + MarkdownAsync, + MarkdownHooks, + Markdown as default, + defaultUrlTransform +} from './lib/index.js' diff --git a/lib/ast-to-react.js b/lib/ast-to-react.js deleted file mode 100644 index 31a06a19..00000000 --- a/lib/ast-to-react.js +++ /dev/null @@ -1,449 +0,0 @@ -/** - * @template T - * @typedef {import('react').ComponentType} ComponentType - */ - -/** - * @template T - * @typedef {import('react').ComponentPropsWithoutRef} ComponentPropsWithoutRef - */ - -/** - * @typedef {import('react').ReactNode} ReactNode - * @typedef {import('unist').Position} Position - * @typedef {import('hast').Element} Element - * @typedef {import('hast').ElementContent} ElementContent - * @typedef {import('hast').Root} Root - * @typedef {import('hast').Text} Text - * @typedef {import('hast').Comment} Comment - * @typedef {import('hast').DocType} Doctype - * @typedef {import('property-information').Info} Info - * @typedef {import('property-information').Schema} Schema - * @typedef {import('./complex-types').ReactMarkdownProps} ReactMarkdownProps - * - * @typedef Raw - * @property {'raw'} type - * @property {string} value - * - * @typedef Context - * @property {Options} options - * @property {Schema} schema - * @property {number} listDepth - * - * @callback TransformLink - * @param {string} href - * @param {Array} children - * @param {string?} title - * @returns {string} - * - * @callback TransformImage - * @param {string} src - * @param {string} alt - * @param {string?} title - * @returns {string} - * - * @typedef {import('react').HTMLAttributeAnchorTarget} TransformLinkTargetType - * - * @callback TransformLinkTarget - * @param {string} href - * @param {Array} children - * @param {string?} title - * @returns {TransformLinkTargetType|undefined} - * - * @typedef {keyof JSX.IntrinsicElements} ReactMarkdownNames - * - * To do: is `data-sourcepos` typeable? - * - * @typedef {ComponentPropsWithoutRef<'code'> & ReactMarkdownProps & {inline?: boolean}} CodeProps - * @typedef {ComponentPropsWithoutRef<'h1'> & ReactMarkdownProps & {level: number}} HeadingProps - * @typedef {ComponentPropsWithoutRef<'li'> & ReactMarkdownProps & {checked: boolean|null, index: number, ordered: boolean}} LiProps - * @typedef {ComponentPropsWithoutRef<'ol'> & ReactMarkdownProps & {depth: number, ordered: true}} OrderedListProps - * @typedef {ComponentPropsWithoutRef<'table'> & ReactMarkdownProps & {style?: Record, isHeader: boolean}} TableCellProps - * @typedef {ComponentPropsWithoutRef<'tr'> & ReactMarkdownProps & {isHeader: boolean}} TableRowProps - * @typedef {ComponentPropsWithoutRef<'ul'> & ReactMarkdownProps & {depth: number, ordered: false}} UnorderedListProps - * - * @typedef {ComponentType} CodeComponent - * @typedef {ComponentType} HeadingComponent - * @typedef {ComponentType} LiComponent - * @typedef {ComponentType} OrderedListComponent - * @typedef {ComponentType} TableCellComponent - * @typedef {ComponentType} TableRowComponent - * @typedef {ComponentType} UnorderedListComponent - * - * @typedef SpecialComponents - * @property {CodeComponent|ReactMarkdownNames} code - * @property {HeadingComponent|ReactMarkdownNames} h1 - * @property {HeadingComponent|ReactMarkdownNames} h2 - * @property {HeadingComponent|ReactMarkdownNames} h3 - * @property {HeadingComponent|ReactMarkdownNames} h4 - * @property {HeadingComponent|ReactMarkdownNames} h5 - * @property {HeadingComponent|ReactMarkdownNames} h6 - * @property {LiComponent|ReactMarkdownNames} li - * @property {OrderedListComponent|ReactMarkdownNames} ol - * @property {TableCellComponent|ReactMarkdownNames} td - * @property {TableCellComponent|ReactMarkdownNames} th - * @property {TableRowComponent|ReactMarkdownNames} tr - * @property {UnorderedListComponent|ReactMarkdownNames} ul - * - * @typedef {Partial & SpecialComponents>} Components - * - * @typedef Options - * @property {boolean} [sourcePos=false] - * @property {boolean} [rawSourcePos=false] - * @property {boolean} [skipHtml=false] - * @property {boolean} [includeElementIndex=false] - * @property {null|false|TransformLink} [transformLinkUri] - * @property {TransformImage} [transformImageUri] - * @property {TransformLinkTargetType|TransformLinkTarget} [linkTarget] - * @property {Components} [components] - */ - -import React from 'react' -import ReactIs from 'react-is' -import {whitespace} from 'hast-util-whitespace' -import {svg, find, hastToReact} from 'property-information' -import {stringify as spaces} from 'space-separated-tokens' -import {stringify as commas} from 'comma-separated-tokens' -import style from 'style-to-object' - -const own = {}.hasOwnProperty - -// The table-related elements that must not contain whitespace text according -// to React. -const tableElements = new Set(['table', 'thead', 'tbody', 'tfoot', 'tr']) - -/** - * @param {Context} context - * @param {Element|Root} node - */ -export function childrenToReact(context, node) { - /** @type {Array} */ - const children = [] - let childIndex = -1 - /** @type {Comment|Doctype|Element|Raw|Text} */ - let child - - while (++childIndex < node.children.length) { - child = node.children[childIndex] - - if (child.type === 'element') { - children.push(toReact(context, child, childIndex, node)) - } else if (child.type === 'text') { - // Currently, a warning is triggered by react for *any* white space in - // tables. - // So we drop it. - // See: . - // See: . - // See: . - // See: . - if ( - node.type !== 'element' || - !tableElements.has(node.tagName) || - !whitespace(child) - ) { - children.push(child.value) - } - } else if (child.type === 'raw' && !context.options.skipHtml) { - // Default behavior is to show (encoded) HTML. - children.push(child.value) - } - } - - return children -} - -/** - * @param {Context} context - * @param {Element} node - * @param {number} index - * @param {Element|Root} parent - */ -function toReact(context, node, index, parent) { - const options = context.options - const parentSchema = context.schema - /** @type {ReactMarkdownNames} */ - // @ts-expect-error assume a known HTML/SVG element. - const name = node.tagName - /** @type {Record} */ - const properties = {} - let schema = parentSchema - /** @type {string} */ - let property - - if (parentSchema.space === 'html' && name === 'svg') { - schema = svg - context.schema = schema - } - - if (node.properties) { - for (property in node.properties) { - if (own.call(node.properties, property)) { - addProperty(properties, property, node.properties[property], context) - } - } - } - - if (name === 'ol' || name === 'ul') { - context.listDepth++ - } - - const children = childrenToReact(context, node) - - if (name === 'ol' || name === 'ul') { - context.listDepth-- - } - - // Restore parent schema. - context.schema = parentSchema - - // Nodes created by plugins do not have positional info, in which case we use - // an object that matches the position interface. - const position = node.position || { - start: {line: null, column: null, offset: null}, - end: {line: null, column: null, offset: null} - } - const component = - options.components && own.call(options.components, name) - ? options.components[name] - : name - const basic = typeof component === 'string' || component === React.Fragment - - if (!ReactIs.isValidElementType(component)) { - throw new TypeError( - `Component for name \`${name}\` not defined or is not renderable` - ) - } - - properties.key = [ - name, - position.start.line, - position.start.column, - index - ].join('-') - - if (name === 'a' && options.linkTarget) { - properties.target = - typeof options.linkTarget === 'function' - ? options.linkTarget( - String(properties.href || ''), - node.children, - typeof properties.title === 'string' ? properties.title : null - ) - : options.linkTarget - } - - if (name === 'a' && options.transformLinkUri) { - properties.href = options.transformLinkUri( - String(properties.href || ''), - node.children, - typeof properties.title === 'string' ? properties.title : null - ) - } - - if ( - !basic && - name === 'code' && - parent.type === 'element' && - parent.tagName !== 'pre' - ) { - properties.inline = true - } - - if ( - !basic && - (name === 'h1' || - name === 'h2' || - name === 'h3' || - name === 'h4' || - name === 'h5' || - name === 'h6') - ) { - properties.level = Number.parseInt(name.charAt(1), 10) - } - - if (name === 'img' && options.transformImageUri) { - properties.src = options.transformImageUri( - String(properties.src || ''), - String(properties.alt || ''), - typeof properties.title === 'string' ? properties.title : null - ) - } - - if (!basic && name === 'li' && parent.type === 'element') { - const input = getInputElement(node) - properties.checked = - input && input.properties ? Boolean(input.properties.checked) : null - properties.index = getElementsBeforeCount(parent, node) - properties.ordered = parent.tagName === 'ol' - } - - if (!basic && (name === 'ol' || name === 'ul')) { - properties.ordered = name === 'ol' - properties.depth = context.listDepth - } - - if (name === 'td' || name === 'th') { - if (properties.align) { - if (!properties.style) properties.style = {} - // @ts-expect-error assume `style` is an object - properties.style.textAlign = properties.align - delete properties.align - } - - if (!basic) { - properties.isHeader = name === 'th' - } - } - - if (!basic && name === 'tr' && parent.type === 'element') { - properties.isHeader = Boolean(parent.tagName === 'thead') - } - - // If `sourcePos` is given, pass source information (line/column info from markdown source). - if (options.sourcePos) { - properties['data-sourcepos'] = flattenPosition(position) - } - - if (!basic && options.rawSourcePos) { - properties.sourcePosition = node.position - } - - // If `includeElementIndex` is given, pass node index info to components. - if (!basic && options.includeElementIndex) { - properties.index = getElementsBeforeCount(parent, node) - properties.siblingCount = getElementsBeforeCount(parent) - } - - if (!basic) { - properties.node = node - } - - // Ensure no React warnings are emitted for void elements w/ children. - return children.length > 0 - ? React.createElement(component, properties, children) - : React.createElement(component, properties) -} - -/** - * @param {Element|Root} node - * @returns {Element?} - */ -function getInputElement(node) { - let index = -1 - - while (++index < node.children.length) { - const child = node.children[index] - - if (child.type === 'element' && child.tagName === 'input') { - return child - } - } - - return null -} - -/** - * @param {Element|Root} parent - * @param {Element} [node] - * @returns {number} - */ -function getElementsBeforeCount(parent, node) { - let index = -1 - let count = 0 - - while (++index < parent.children.length) { - if (parent.children[index] === node) break - if (parent.children[index].type === 'element') count++ - } - - return count -} - -/** - * @param {Record} props - * @param {string} prop - * @param {unknown} value - * @param {Context} ctx - */ -function addProperty(props, prop, value, ctx) { - const info = find(ctx.schema, prop) - let result = value - - // Ignore nullish and `NaN` values. - // eslint-disable-next-line no-self-compare - if (result === null || result === undefined || result !== result) { - return - } - - // Accept `array`. - // Most props are space-separated. - if (Array.isArray(result)) { - result = info.commaSeparated ? commas(result) : spaces(result) - } - - if (info.property === 'style' && typeof result === 'string') { - result = parseStyle(result) - } - - if (info.space && info.property) { - props[ - own.call(hastToReact, info.property) - ? hastToReact[info.property] - : info.property - ] = result - } else if (info.attribute) { - props[info.attribute] = result - } -} - -/** - * @param {string} value - * @returns {Record} - */ -function parseStyle(value) { - /** @type {Record} */ - const result = {} - - try { - style(value, iterator) - } catch { - // Silent. - } - - return result - - /** - * @param {string} name - * @param {string} v - */ - function iterator(name, v) { - const k = name.slice(0, 4) === '-ms-' ? `ms-${name.slice(4)}` : name - result[k.replace(/-([a-z])/g, styleReplacer)] = v - } -} - -/** - * @param {unknown} _ - * @param {string} $1 - */ -function styleReplacer(_, $1) { - return $1.toUpperCase() -} - -/** - * @param {Position|{start: {line: null, column: null, offset: null}, end: {line: null, column: null, offset: null}}} pos - * @returns {string} - */ -function flattenPosition(pos) { - return [ - pos.start.line, - ':', - pos.start.column, - '-', - pos.end.line, - ':', - pos.end.column - ] - .map((d) => String(d)) - .join('') -} diff --git a/lib/complex-types.ts b/lib/complex-types.ts deleted file mode 100644 index c639e7ae..00000000 --- a/lib/complex-types.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type {ReactNode, ComponentType, ComponentPropsWithoutRef} from 'react' -import type {Position} from 'unist' -import type {Element} from 'hast' - -/* File for types which are not handled correctly in JSDoc mode */ - -export interface ReactMarkdownProps { - node: Element - children: ReactNode[] - /** - * Passed when `options.rawSourcePos` is given - */ - sourcePosition?: Position - /** - * Passed when `options.includeElementIndex` is given - */ - index?: number - /** - * Passed when `options.includeElementIndex` is given - */ - siblingCount?: number -} - -export type NormalComponents = { - [TagName in keyof JSX.IntrinsicElements]: - | keyof JSX.IntrinsicElements - | ComponentType & ReactMarkdownProps> -} diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 00000000..06f07e12 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,448 @@ +/** + * @import {Element, Nodes, Parents, Root} from 'hast' + * @import {Root as MdastRoot} from 'mdast' + * @import {ComponentType, JSX, ReactElement, ReactNode} from 'react' + * @import {Options as RemarkRehypeOptions} from 'remark-rehype' + * @import {BuildVisitor} from 'unist-util-visit' + * @import {PluggableList, Processor} from 'unified' + */ + +/** + * @callback AllowElement + * Filter elements. + * @param {Readonly} element + * Element to check. + * @param {number} index + * Index of `element` in `parent`. + * @param {Readonly | undefined} parent + * Parent of `element`. + * @returns {boolean | null | undefined} + * Whether to allow `element` (default: `false`). + */ + +/** + * @typedef ExtraProps + * Extra fields we pass. + * @property {Element | undefined} [node] + * passed when `passNode` is on. + */ + +/** + * @typedef {{ + * [Key in keyof JSX.IntrinsicElements]?: ComponentType | keyof JSX.IntrinsicElements + * }} Components + * Map tag names to components. + */ + +/** + * @typedef Deprecation + * Deprecation. + * @property {string} from + * Old field. + * @property {string} id + * ID in readme. + * @property {keyof Options} [to] + * New field. + */ + +/** + * @typedef Options + * Configuration. + * @property {AllowElement | null | undefined} [allowElement] + * Filter elements (optional); + * `allowedElements` / `disallowedElements` is used first. + * @property {ReadonlyArray | null | undefined} [allowedElements] + * Tag names to allow (default: all tag names); + * cannot combine w/ `disallowedElements`. + * @property {string | null | undefined} [children] + * Markdown. + * @property {Components | null | undefined} [components] + * Map tag names to components. + * @property {ReadonlyArray | null | undefined} [disallowedElements] + * Tag names to disallow (default: `[]`); + * cannot combine w/ `allowedElements`. + * @property {PluggableList | null | undefined} [rehypePlugins] + * List of rehype plugins to use. + * @property {PluggableList | null | undefined} [remarkPlugins] + * List of remark plugins to use. + * @property {Readonly | null | undefined} [remarkRehypeOptions] + * Options to pass through to `remark-rehype`. + * @property {boolean | null | undefined} [skipHtml=false] + * Ignore HTML in markdown completely (default: `false`). + * @property {boolean | null | undefined} [unwrapDisallowed=false] + * Extract (unwrap) what’s in disallowed elements (default: `false`); + * normally when say `strong` is not allowed, it and it’s children are dropped, + * with `unwrapDisallowed` the element itself is replaced by its children. + * @property {UrlTransform | null | undefined} [urlTransform] + * Change URLs (default: `defaultUrlTransform`) + */ + +/** + * @typedef HooksOptionsOnly + * Configuration specifically for {@linkcode MarkdownHooks}. + * @property {ReactNode | null | undefined} [fallback] + * Content to render while the processor processing the markdown (optional). + */ + +/** + * @typedef {Options & HooksOptionsOnly} HooksOptions + * Configuration for {@linkcode MarkdownHooks}; + * extends the regular {@linkcode Options} with a `fallback` prop. + */ + +/** + * @callback UrlTransform + * Transform all URLs. + * @param {string} url + * URL. + * @param {string} key + * Property name (example: `'href'`). + * @param {Readonly} node + * Node. + * @returns {string | null | undefined} + * Transformed URL (optional). + */ + +import {unreachable} from 'devlop' +import {toJsxRuntime} from 'hast-util-to-jsx-runtime' +import {urlAttributes} from 'html-url-attributes' +import {Fragment, jsx, jsxs} from 'react/jsx-runtime' +import {useEffect, useMemo, useState} from 'react' +import remarkParse from 'remark-parse' +import remarkRehype from 'remark-rehype' +import {unified} from 'unified' +import {visit} from 'unist-util-visit' +import {VFile} from 'vfile' + +const changelog = + 'https://github.com/remarkjs/react-markdown/blob/main/changelog.md' + +/** @type {PluggableList} */ +const emptyPlugins = [] +/** @type {Readonly} */ +const emptyRemarkRehypeOptions = {allowDangerousHtml: true} +const safeProtocol = /^(https?|ircs?|mailto|xmpp)$/i + +// Mutable because we `delete` any time it’s used and a message is sent. +/** @type {ReadonlyArray>} */ +const deprecations = [ + {from: 'astPlugins', id: 'remove-buggy-html-in-markdown-parser'}, + {from: 'allowDangerousHtml', id: 'remove-buggy-html-in-markdown-parser'}, + { + from: 'allowNode', + id: 'replace-allownode-allowedtypes-and-disallowedtypes', + to: 'allowElement' + }, + { + from: 'allowedTypes', + id: 'replace-allownode-allowedtypes-and-disallowedtypes', + to: 'allowedElements' + }, + {from: 'className', id: 'remove-classname'}, + { + from: 'disallowedTypes', + id: 'replace-allownode-allowedtypes-and-disallowedtypes', + to: 'disallowedElements' + }, + {from: 'escapeHtml', id: 'remove-buggy-html-in-markdown-parser'}, + {from: 'includeElementIndex', id: '#remove-includeelementindex'}, + { + from: 'includeNodeIndex', + id: 'change-includenodeindex-to-includeelementindex' + }, + {from: 'linkTarget', id: 'remove-linktarget'}, + {from: 'plugins', id: 'change-plugins-to-remarkplugins', to: 'remarkPlugins'}, + {from: 'rawSourcePos', id: '#remove-rawsourcepos'}, + {from: 'renderers', id: 'change-renderers-to-components', to: 'components'}, + {from: 'source', id: 'change-source-to-children', to: 'children'}, + {from: 'sourcePos', id: '#remove-sourcepos'}, + {from: 'transformImageUri', id: '#add-urltransform', to: 'urlTransform'}, + {from: 'transformLinkUri', id: '#add-urltransform', to: 'urlTransform'} +] + +/** + * Component to render markdown. + * + * This is a synchronous component. + * When using async plugins, + * see {@linkcode MarkdownAsync} or {@linkcode MarkdownHooks}. + * + * @param {Readonly} options + * Props. + * @returns {ReactElement} + * React element. + */ +export function Markdown(options) { + const processor = createProcessor(options) + const file = createFile(options) + return post(processor.runSync(processor.parse(file), file), options) +} + +/** + * Component to render markdown with support for async plugins + * through async/await. + * + * Components returning promises are supported on the server. + * For async support on the client, + * see {@linkcode MarkdownHooks}. + * + * @param {Readonly} options + * Props. + * @returns {Promise} + * Promise to a React element. + */ +export async function MarkdownAsync(options) { + const processor = createProcessor(options) + const file = createFile(options) + const tree = await processor.run(processor.parse(file), file) + return post(tree, options) +} + +/** + * Component to render markdown with support for async plugins through hooks. + * + * This uses `useEffect` and `useState` hooks. + * Hooks run on the client and do not immediately render something. + * For async support on the server, + * see {@linkcode MarkdownAsync}. + * + * @param {Readonly} options + * Props. + * @returns {ReactNode} + * React node. + */ +export function MarkdownHooks(options) { + const processor = useMemo( + function () { + return createProcessor(options) + }, + [options.rehypePlugins, options.remarkPlugins, options.remarkRehypeOptions] + ) + const [error, setError] = useState( + /** @type {Error | undefined} */ (undefined) + ) + const [tree, setTree] = useState(/** @type {Root | undefined} */ (undefined)) + + useEffect( + function () { + let cancelled = false + const file = createFile(options) + + processor.run(processor.parse(file), file, function (error, tree) { + if (!cancelled) { + setError(error) + setTree(tree) + } + }) + + /** + * @returns {undefined} + * Nothing. + */ + return function () { + cancelled = true + } + }, + [options.children, processor] + ) + + if (error) throw error + + return tree ? post(tree, options) : options.fallback +} + +/** + * Set up the `unified` processor. + * + * @param {Readonly} options + * Props. + * @returns {Processor} + * Result. + */ +function createProcessor(options) { + const rehypePlugins = options.rehypePlugins || emptyPlugins + const remarkPlugins = options.remarkPlugins || emptyPlugins + const remarkRehypeOptions = options.remarkRehypeOptions + ? {...options.remarkRehypeOptions, ...emptyRemarkRehypeOptions} + : emptyRemarkRehypeOptions + + const processor = unified() + .use(remarkParse) + .use(remarkPlugins) + .use(remarkRehype, remarkRehypeOptions) + .use(rehypePlugins) + + return processor +} + +/** + * Set up the virtual file. + * + * @param {Readonly} options + * Props. + * @returns {VFile} + * Result. + */ +function createFile(options) { + const children = options.children || '' + const file = new VFile() + + if (typeof children === 'string') { + file.value = children + } else { + unreachable( + 'Unexpected value `' + + children + + '` for `children` prop, expected `string`' + ) + } + + return file +} + +/** + * Process the result from unified some more. + * + * @param {Nodes} tree + * Tree. + * @param {Readonly} options + * Props. + * @returns {ReactElement} + * React element. + */ +function post(tree, options) { + const allowedElements = options.allowedElements + const allowElement = options.allowElement + const components = options.components + const disallowedElements = options.disallowedElements + const skipHtml = options.skipHtml + const unwrapDisallowed = options.unwrapDisallowed + const urlTransform = options.urlTransform || defaultUrlTransform + + for (const deprecation of deprecations) { + if (Object.hasOwn(options, deprecation.from)) { + unreachable( + 'Unexpected `' + + deprecation.from + + '` prop, ' + + (deprecation.to + ? 'use `' + deprecation.to + '` instead' + : 'remove it') + + ' (see <' + + changelog + + '#' + + deprecation.id + + '> for more info)' + ) + } + } + + if (allowedElements && disallowedElements) { + unreachable( + 'Unexpected combined `allowedElements` and `disallowedElements`, expected one or the other' + ) + } + + visit(tree, transform) + + return toJsxRuntime(tree, { + Fragment, + components, + ignoreInvalidStyle: true, + jsx, + jsxs, + passKeys: true, + passNode: true + }) + + /** @type {BuildVisitor} */ + function transform(node, index, parent) { + if (node.type === 'raw' && parent && typeof index === 'number') { + if (skipHtml) { + parent.children.splice(index, 1) + } else { + parent.children[index] = {type: 'text', value: node.value} + } + + return index + } + + if (node.type === 'element') { + /** @type {string} */ + let key + + for (key in urlAttributes) { + if ( + Object.hasOwn(urlAttributes, key) && + Object.hasOwn(node.properties, key) + ) { + const value = node.properties[key] + const test = urlAttributes[key] + if (test === null || test.includes(node.tagName)) { + node.properties[key] = urlTransform(String(value || ''), key, node) + } + } + } + } + + if (node.type === 'element') { + let remove = allowedElements + ? !allowedElements.includes(node.tagName) + : disallowedElements + ? disallowedElements.includes(node.tagName) + : false + + if (!remove && allowElement && typeof index === 'number') { + remove = !allowElement(node, index, parent) + } + + if (remove && parent && typeof index === 'number') { + if (unwrapDisallowed && node.children) { + parent.children.splice(index, 1, ...node.children) + } else { + parent.children.splice(index, 1) + } + + return index + } + } + } +} + +/** + * Make a URL safe. + * + * This follows how GitHub works. + * It allows the protocols `http`, `https`, `irc`, `ircs`, `mailto`, and `xmpp`, + * and URLs relative to the current protocol (such as `/something`). + * + * @satisfies {UrlTransform} + * @param {string} value + * URL. + * @returns {string} + * Safe URL. + */ +export function defaultUrlTransform(value) { + // Same as: + // + // But without the `encode` part. + const colon = value.indexOf(':') + const questionMark = value.indexOf('?') + const numberSign = value.indexOf('#') + const slash = value.indexOf('/') + + if ( + // If there is no protocol, it’s relative. + colon === -1 || + // If the first colon is after a `?`, `#`, or `/`, it’s not a protocol. + (slash !== -1 && colon > slash) || + (questionMark !== -1 && colon > questionMark) || + (numberSign !== -1 && colon > numberSign) || + // It is a protocol, it should be allowed. + safeProtocol.test(value.slice(0, colon)) + ) { + return value + } + + return '' +} diff --git a/lib/react-markdown.js b/lib/react-markdown.js deleted file mode 100644 index 48ba44e3..00000000 --- a/lib/react-markdown.js +++ /dev/null @@ -1,160 +0,0 @@ -/** - * @typedef {import('react').ReactNode} ReactNode - * @typedef {import('react').ReactElement<{}>} ReactElement - * @typedef {import('unified').PluggableList} PluggableList - * @typedef {import('hast').Root} Root - * @typedef {import('./rehype-filter.js').Options} FilterOptions - * @typedef {import('./ast-to-react.js').Options} TransformOptions - * - * @typedef CoreOptions - * @property {string} children - * - * @typedef PluginOptions - * @property {PluggableList} [plugins=[]] **deprecated**: use `remarkPlugins` instead - * @property {PluggableList} [remarkPlugins=[]] - * @property {PluggableList} [rehypePlugins=[]] - * - * @typedef LayoutOptions - * @property {string} [className] - * - * @typedef {CoreOptions & PluginOptions & LayoutOptions & FilterOptions & TransformOptions} ReactMarkdownOptions - * - * @typedef Deprecation - * @property {string} id - * @property {string} [to] - */ - -import React from 'react' -import {VFile} from 'vfile' -import {unified} from 'unified' -import remarkParse from 'remark-parse' -import remarkRehype from 'remark-rehype' -import PropTypes from 'prop-types' -import {html} from 'property-information' -import rehypeFilter from './rehype-filter.js' -import {uriTransformer} from './uri-transformer.js' -import {childrenToReact} from './ast-to-react.js' - -const own = {}.hasOwnProperty -const changelog = - 'https://github.com/remarkjs/react-markdown/blob/main/changelog.md' - -/** @type {Record} */ -const deprecated = { - renderers: {to: 'components', id: 'change-renderers-to-components'}, - astPlugins: {id: 'remove-buggy-html-in-markdown-parser'}, - allowDangerousHtml: {id: 'remove-buggy-html-in-markdown-parser'}, - escapeHtml: {id: 'remove-buggy-html-in-markdown-parser'}, - source: {to: 'children', id: 'change-source-to-children'}, - allowNode: { - to: 'allowElement', - id: 'replace-allownode-allowedtypes-and-disallowedtypes' - }, - allowedTypes: { - to: 'allowedElements', - id: 'replace-allownode-allowedtypes-and-disallowedtypes' - }, - disallowedTypes: { - to: 'disallowedElements', - id: 'replace-allownode-allowedtypes-and-disallowedtypes' - }, - includeNodeIndex: { - to: 'includeElementIndex', - id: 'change-includenodeindex-to-includeelementindex' - } -} - -/** - * React component to render markdown. - * - * @param {ReactMarkdownOptions} options - * @returns {ReactElement} - */ -export function ReactMarkdown(options) { - for (const key in deprecated) { - if (own.call(deprecated, key) && own.call(options, key)) { - const deprecation = deprecated[key] - console.warn( - `[react-markdown] Warning: please ${ - deprecation.to ? `use \`${deprecation.to}\` instead of` : 'remove' - } \`${key}\` (see <${changelog}#${deprecation.id}> for more info)` - ) - delete deprecated[key] - } - } - - const processor = unified() - .use(remarkParse) - // TODO: deprecate `plugins` in v8.0.0. - .use(options.remarkPlugins || options.plugins || []) - .use(remarkRehype, {allowDangerousHtml: true}) - .use(options.rehypePlugins || []) - .use(rehypeFilter, options) - - const file = new VFile() - - if (typeof options.children === 'string') { - file.value = options.children - } else if (options.children !== undefined && options.children !== null) { - console.warn( - `[react-markdown] Warning: please pass a string as \`children\` (not: \`${options.children}\`)` - ) - } - - const hastNode = processor.runSync(processor.parse(file), file) - - if (hastNode.type !== 'root') { - throw new TypeError('Expected a `root` node') - } - - /** @type {ReactElement} */ - let result = React.createElement( - React.Fragment, - {}, - childrenToReact({options, schema: html, listDepth: 0}, hastNode) - ) - - if (options.className) { - result = React.createElement('div', {className: options.className}, result) - } - - return result -} - -ReactMarkdown.defaultProps = {transformLinkUri: uriTransformer} - -ReactMarkdown.propTypes = { - // Core options: - children: PropTypes.string, - // Layout options: - className: PropTypes.string, - // Filter options: - allowElement: PropTypes.func, - allowedElements: PropTypes.arrayOf(PropTypes.string), - disallowedElements: PropTypes.arrayOf(PropTypes.string), - unwrapDisallowed: PropTypes.bool, - // Plugin options: - remarkPlugins: PropTypes.arrayOf( - PropTypes.oneOfType([ - PropTypes.object, - PropTypes.func, - PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.object, PropTypes.func])) - ]) - ), - rehypePlugins: PropTypes.arrayOf( - PropTypes.oneOfType([ - PropTypes.object, - PropTypes.func, - PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.object, PropTypes.func])) - ]) - ), - // Transform options: - sourcePos: PropTypes.bool, - rawSourcePos: PropTypes.bool, - skipHtml: PropTypes.bool, - includeElementIndex: PropTypes.bool, - transformLinkUri: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]), - linkTarget: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), - transformImageUri: PropTypes.func, - components: PropTypes.object -} diff --git a/lib/rehype-filter.js b/lib/rehype-filter.js deleted file mode 100644 index 3d6a00d8..00000000 --- a/lib/rehype-filter.js +++ /dev/null @@ -1,66 +0,0 @@ -import {visit} from 'unist-util-visit' - -/** - * @typedef {import('unist').Node} Node - * @typedef {import('hast').Root} Root - * @typedef {import('hast').Element} Element - * - * @callback AllowElement - * @param {Element} element - * @param {number} index - * @param {Element|Root} parent - * @returns {boolean|undefined} - * - * @typedef Options - * @property {Array} [allowedElements] - * @property {Array} [disallowedElements=[]] - * @property {AllowElement} [allowElement] - * @property {boolean} [unwrapDisallowed=false] - */ - -/** - * @type {import('unified').Plugin<[Options], Root>} - */ -export default function rehypeFilter(options) { - if (options.allowedElements && options.disallowedElements) { - throw new TypeError( - 'Only one of `allowedElements` and `disallowedElements` should be defined' - ) - } - - if ( - options.allowedElements || - options.disallowedElements || - options.allowElement - ) { - return (tree) => { - visit(tree, 'element', (node, index, parent_) => { - const parent = /** @type {Element|Root} */ (parent_) - /** @type {boolean|undefined} */ - let remove - - if (options.allowedElements) { - remove = !options.allowedElements.includes(node.tagName) - } else if (options.disallowedElements) { - remove = options.disallowedElements.includes(node.tagName) - } - - if (!remove && options.allowElement && typeof index === 'number') { - remove = !options.allowElement(node, index, parent) - } - - if (remove && typeof index === 'number') { - if (options.unwrapDisallowed && node.children) { - parent.children.splice(index, 1, ...node.children) - } else { - parent.children.splice(index, 1) - } - - return index - } - - return undefined - }) - } - } -} diff --git a/lib/uri-transformer.js b/lib/uri-transformer.js deleted file mode 100644 index 0bcfa5bb..00000000 --- a/lib/uri-transformer.js +++ /dev/null @@ -1,45 +0,0 @@ -const protocols = ['http', 'https', 'mailto', 'tel'] - -/** - * @param {string} uri - * @returns {string} - */ -export function uriTransformer(uri) { - const url = (uri || '').trim() - const first = url.charAt(0) - - if (first === '#' || first === '/') { - return url - } - - const colon = url.indexOf(':') - if (colon === -1) { - return url - } - - let index = -1 - - while (++index < protocols.length) { - const protocol = protocols[index] - - if ( - colon === protocol.length && - url.slice(0, protocol.length).toLowerCase() === protocol - ) { - return url - } - } - - index = url.indexOf('?') - if (index !== -1 && colon > index) { - return url - } - - index = url.indexOf('#') - if (index !== -1 && colon > index) { - return url - } - - // eslint-disable-next-line no-script-url - return 'javascript:void(0)' -} diff --git a/license b/license index cd5deaaa..f552fc95 100644 --- a/license +++ b/license @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015 Espen Hovlandsdal +Copyright (c) Espen Hovlandsdal Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/package.json b/package.json index 67bce621..35fe1fa9 100644 --- a/package.json +++ b/package.json @@ -1,204 +1,169 @@ { - "name": "react-markdown", - "version": "7.1.2", - "description": "React component to render markdown", - "license": "MIT", - "keywords": [ - "remark", - "unified", - "markdown", - "commonmark", - "gfm", - "ast", - "react", - "react-component", - "component" - ], - "repository": "remarkjs/react-markdown", - "bugs": "https://github.com/remarkjs/react-markdown/issues", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - }, "author": "Espen Hovlandsdal ", + "bugs": "https://github.com/remarkjs/react-markdown/issues", "contributors": [ + "Alexander Wallin ", + "Alexander Wong ", + "André Staltz ", + "Angus MacIsaac ", + "Beau Roberts ", + "Charlie Chen ", + "Christian Murphy ", + "Christoph Werner ", + "Danny ", + "Dennis S ", "Espen Hovlandsdal ", - "Titus Wormer (https://wooorm.com)", - "Thomas Lindstrøm ", + "Evan Hensleigh ", "Fabian Irsara ", - "René Kooi ", - "Nicolas Venegas ", - "Christian Murphy ", - "Linus Unnebäck ", - "Peng Guanwen ", - "mudrz ", - "Jesse Pinho ", "Florentin Luca Rieger ", "Frank ", "Igor Kamyshev ", "Jack Williams ", "Jakub Chrzanowski ", "Jeremy Moseley ", + "Jesse Pinho ", "Kelvin Chan ", "Kohei Asai ", + "Linus Unnebäck ", "Marshall Smith ", "Nathan Bierema ", + "Nicolas Venegas ", + "Peng Guanwen ", "Petr Gazarov ", "Phil Rajchgot ", "Rasmus Eneman ", + "René Kooi ", "Riku Rouvila ", "Robin Wieruch ", "Rostyslav Melnychuk ", "Ted Piotrowski ", "Thibaud Courtoison ", + "Thomas Lindstrøm ", "Tiago Roldão ", + "Titus Wormer (https://wooorm.com)", "cerkiewny ", "evoye ", "gRoberts84 ", - "Alexander Wallin ", - "vanchagreen ", - "Alexander Wong ", - "André Staltz ", - "Angus MacIsaac ", - "Beau Roberts ", - "Charlie Chen ", - "Christoph Werner ", - "Danny ", - "Dennis S ", - "Evan Hensleigh " + "mudrz ", + "vanchagreen " ], - "sideEffects": false, - "type": "module", - "main": "index.js", - "types": "index.d.ts", - "unpkg": "react-markdown.min.js", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "description": "React component to render markdown", + "devDependencies": { + "@testing-library/react": "^16.0.0", + "@types/node": "^22.0.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "c8": "^10.0.0", + "concat-stream": "^2.0.0", + "esbuild": "^0.25.0", + "eslint-plugin-react": "^7.0.0", + "global-jsdom": "^26.0.0", + "prettier": "^3.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "rehype-raw": "^7.0.0", + "rehype-starry-night": "^2.0.0", + "remark-cli": "^12.0.0", + "remark-gfm": "^4.0.0", + "remark-preset-wooorm": "^11.0.0", + "remark-toc": "^9.0.0", + "type-coverage": "^2.0.0", + "typescript": "^5.0.0", + "xo": "^0.60.0" + }, + "exports": "./index.js", "files": [ - "lib/", + "index.d.ts.map", "index.d.ts", "index.js", - "react-markdown.min.js" + "lib/" ], - "dependencies": { - "@types/hast": "^2.0.0", - "@types/unist": "^2.0.0", - "comma-separated-tokens": "^2.0.0", - "hast-util-whitespace": "^2.0.0", - "prop-types": "^15.0.0", - "property-information": "^6.0.0", - "react-is": "^17.0.0", - "remark-parse": "^10.0.0", - "remark-rehype": "^9.0.0", - "space-separated-tokens": "^2.0.0", - "style-to-object": "^0.3.0", - "unified": "^10.0.0", - "unist-util-visit": "^4.0.0", - "vfile": "^5.0.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" }, + "keywords": [ + "ast", + "commonmark", + "component", + "gfm", + "markdown", + "react", + "react-component", + "remark", + "unified" + ], + "license": "MIT", + "name": "react-markdown", "peerDependencies": { - "@types/react": ">=16", - "react": ">=16" - }, - "devDependencies": { - "@types/react": "^17.0.0", - "@types/react-dom": "^17.0.0", - "@types/react-is": "^17.0.0", - "c8": "^7.0.0", - "esbuild": "^0.14.0", - "eslint-config-xo-react": "^0.25.0", - "eslint-plugin-es": "^4.0.0", - "eslint-plugin-react": "^7.0.0", - "eslint-plugin-react-hooks": "^4.0.0", - "eslint-plugin-security": "^1.0.0", - "prettier": "^2.0.0", - "react": "^17.0.0", - "react-dom": "^17.0.0", - "rehype-raw": "^6.0.0", - "remark-cli": "^10.0.0", - "remark-gfm": "^3.0.0", - "remark-preset-wooorm": "^9.0.0", - "remark-toc": "^8.0.0", - "rimraf": "^3.0.0", - "type-coverage": "^2.0.0", - "typescript": "~4.4.0", - "uvu": "^0.5.0", - "xo": "^0.47.0" + "@types/react": ">=18", + "react": ">=18" }, - "scripts": { - "prepack": "npm run build && npm run format", - "build": "rimraf \"{lib/**/**,test/**,script/**,}*.d.ts\" && tsc && type-coverage && esbuild index.js --bundle --minify --target=es2015 --outfile=react-markdown.min.js --global-name=ReactMarkdown --banner:js=\"(function (g, f) {typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = f() : typeof define === 'function' && define.amd ? define([], f) : (g = typeof globalThis !== 'undefined' ? globalThis : g || self, g.ReactMarkdown = f()); }(this, (function () { 'use strict';\" --footer:js=\"return ReactMarkdown;})));\"", - "format": "remark . -qfo --ignore-pattern test/ && prettier . -w --loglevel warn && xo --fix", - "test-api": "node --no-warnings --experimental-loader=./test/loader.js ./node_modules/.bin/uvu test \"\\.jsx$\"", - "test-coverage": "c8 --check-coverage --branches 100 --functions 100 --lines 100 --statements 100 --reporter lcov npm run test-api", - "test": "npm run build && npm run format && npm run test-coverage" + "prettier": { + "bracketSpacing": false, + "singleQuote": true, + "semi": false, + "tabWidth": 2, + "trailingComma": "none", + "useTabs": false }, "remarkConfig": { "plugins": [ - "preset-wooorm", - [ - "gfm", - { - "tablePipeAlign": false - } - ], + "remark-preset-wooorm", [ - "lint-table-pipe-alignment", - false - ], - [ - "lint-no-html", + "remark-lint-no-html", false ] ] }, + "repository": "remarkjs/react-markdown", + "scripts": { + "build": "tsc --build --clean && tsc --build && type-coverage", + "format": "remark --frail --output --quiet -- . && prettier --log-level warn --write -- . && xo --fix", + "test-api": "node --conditions development --experimental-loader=./script/load-jsx.js --no-warnings test.jsx", + "test-coverage": "c8 --100 --exclude script/ --reporter lcov -- npm run test-api", + "test": "npm run build && npm run format && npm run test-coverage" + }, + "sideEffects": false, "typeCoverage": { "atLeast": 100, - "detail": true, - "strict": true, - "ignoreCatch": true, - "#": "below is ignored because some proptypes will `any`", - "ignoreFiles": [ - "lib/react-markdown.d.ts", - "index.d.ts" - ] - }, - "prettier": { - "tabWidth": 2, - "useTabs": false, - "singleQuote": true, - "bracketSpacing": false, - "semi": false, - "trailingComma": "none" + "strict": true }, + "type": "module", + "version": "10.1.0", "xo": { - "prettier": true, - "extends": "xo-react", "envs": [ "shared-node-browser" ], + "extends": "plugin:react/jsx-runtime", "overrides": [ { "files": [ - "lib/**/*.js" - ], - "extends": [ - "plugin:es/restrict-to-es2019", - "plugin:security/recommended" - ], - "rules": { - "complexity": "off", - "security/detect-object-injection": "off" - } - }, - { - "files": [ - "test/**/*.jsx" + "**/*.jsx" ], "rules": { - "node/file-extension-in-import": "off", - "react/no-children-prop": "off", - "react/prop-types": "off" + "no-unused-vars": "off" } } - ] + ], + "prettier": true, + "rules": { + "complexity": "off", + "n/file-extension-in-import": "off", + "unicorn/prevent-abbreviations": "off" + } } } diff --git a/readme.md b/readme.md index 926e9369..949b1801 100644 --- a/readme.md +++ b/readme.md @@ -1,109 +1,114 @@ # react-markdown -[![Build][build-badge]][build] -[![Coverage][coverage-badge]][coverage] -[![Downloads][downloads-badge]][downloads] -[![Size][size-badge]][size] -[![Sponsors][sponsors-badge]][collective] -[![Backers][backers-badge]][collective] -[![Chat][chat-badge]][chat] +[![Build][badge-build-image]][badge-build-url] +[![Coverage][badge-coverage-image]][badge-coverage-url] +[![Downloads][badge-downloads-image]][badge-downloads-url] +[![Size][badge-size-image]][badge-size-url] React component to render markdown. ## Feature highlights -* [x] **[safe][security] by default** - (no `dangerouslySetInnerHTML` or XSS attacks) -* [x] **[components][]** - (pass your own component to use instead of `

` for `## hi`) -* [x] **[plugins][]** - (many plugins you can pick and choose from) -* [x] **[compliant][syntax]** - (100% to CommonMark, 100% to GFM with a plugin) +* [x] **[safe][section-security] by default** + (no `dangerouslySetInnerHTML` or XSS attacks) +* [x] **[components][section-components]** + (pass your own component to use instead of `

` for `## hi`) +* [x] **[plugins][section-plugins]** + (many plugins you can pick and choose from) +* [x] **[compliant][section-syntax]** + (100% to CommonMark, 100% to GFM with a plugin) ## Contents -* [What is this?](#what-is-this) -* [When should I use this?](#when-should-i-use-this) -* [Install](#install) -* [Use](#use) -* [API](#api) - * [`props`](#props) - * [`uriTransformer`](#uritransformer) -* [Examples](#examples) - * [Use a plugin](#use-a-plugin) - * [Use a plugin with options](#use-a-plugin-with-options) - * [Use custom components (syntax highlight)](#use-custom-components-syntax-highlight) - * [Use remark and rehype plugins (math)](#use-remark-and-rehype-plugins-math) -* [Plugins](#plugins) -* [Syntax](#syntax) -* [Types](#types) -* [Compatibility](#compatibility) -* [Architecture](#architecture) -* [Appendix A: HTML in markdown](#appendix-a-html-in-markdown) -* [Appendix B: Components](#appendix-b-components) -* [Security](#security) -* [Related](#related) -* [Contribute](#contribute) -* [License](#license) +* [What is this?](#what-is-this) +* [When should I use this?](#when-should-i-use-this) +* [Install](#install) +* [Use](#use) +* [API](#api) + * [`Markdown`](#markdown) + * [`MarkdownAsync`](#markdownasync) + * [`MarkdownHooks`](#markdownhooks) + * [`defaultUrlTransform(url)`](#defaulturltransformurl) + * [`AllowElement`](#allowelement) + * [`Components`](#components) + * [`ExtraProps`](#extraprops) + * [`HooksOptions`](#hooksoptions) + * [`Options`](#options) + * [`UrlTransform`](#urltransform) +* [Examples](#examples) + * [Use a plugin](#use-a-plugin) + * [Use a plugin with options](#use-a-plugin-with-options) + * [Use custom components (syntax highlight)](#use-custom-components-syntax-highlight) + * [Use remark and rehype plugins (math)](#use-remark-and-rehype-plugins-math) +* [Plugins](#plugins) +* [Syntax](#syntax) +* [Compatibility](#compatibility) +* [Architecture](#architecture) +* [Appendix A: HTML in markdown](#appendix-a-html-in-markdown) +* [Appendix B: Components](#appendix-b-components) +* [Appendix C: line endings in markdown (and JSX)](#appendix-c-line-endings-in-markdown-and-jsx) +* [Security](#security) +* [Related](#related) +* [Contribute](#contribute) +* [License](#license) ## What is this? This package is a [React][] component that can be given a string of markdown that it’ll safely render to React elements. -You can pass plugins to change how markdown is transformed to React elements and -pass components that will be used instead of normal HTML elements. +You can pass plugins to change how markdown is transformed and pass components +that will be used instead of normal HTML elements. -* to learn markdown, see this [cheatsheet and tutorial][cheat] -* to try out `react-markdown`, see [our demo][demo] +* to learn markdown, see this [cheatsheet and tutorial][commonmark-help] +* to try out `react-markdown`, see [our demo][github-io-react-markdown] ## When should I use this? There are other ways to use markdown in React out there so why use this one? -The two main reasons are that they often rely on `dangerouslySetInnerHTML` or -have bugs with how they handle markdown. -`react-markdown` uses a syntax tree to build the virtual dom which allows for -updating only the changing DOM instead of completely overwriting. -`react-markdown` is 100% CommonMark compliant and has plugins to support other -syntax extensions (such as GFM). - -These features are supported because we use [unified][], specifically [remark][] -for markdown and [rehype][] for HTML, which are popular tools to transform -content with plugins. +The three main reasons are that they often rely on `dangerouslySetInnerHTML`, +have bugs with how they handle markdown, or don’t let you swap elements for +components. +`react-markdown` builds a virtual DOM, so React only replaces what changed, +from a syntax tree. +That’s supported because we use [unified][github-unified], +specifically [remark][github-remark] for markdown and [rehype][github-rehype] +for HTML, +which are popular tools to transform content with plugins. This package focusses on making it easy for beginners to safely use markdown in React. When you’re familiar with unified, you can use a modern hooks based alternative -[`react-remark`][react-remark] or [`rehype-react`][rehype-react] manually. +[`react-remark`][github-react-remark] or [`rehype-react`][github-rehype-react] +manually. If you instead want to use JavaScript and JSX *inside* markdown files, use -[MDX][]. +[MDX][github-mdx]. ## Install This package is [ESM only][esm]. -In Node.js (version 12.20+, 14.14+, or 16.0+), install with [npm][]: +In Node.js (version 16+), install with [npm][npm-install]: ```sh npm install react-markdown ``` -In Deno with [Skypack][]: +In Deno with [`esm.sh`][esmsh]: ```js -import ReactMarkdown from 'https://cdn.skypack.dev/react-markdown@7?dts' +import Markdown from 'https://esm.sh/react-markdown@10' ``` -In browsers with [Skypack][]: +In browsers with [`esm.sh`][esmsh]: ```html ``` @@ -111,49 +116,51 @@ In browsers with [Skypack][]: A basic hello world: -```jsx +```js import React from 'react' -import ReactMarkdown from 'react-markdown' -import ReactDom from 'react-dom' +import {createRoot} from 'react-dom/client' +import Markdown from 'react-markdown' + +const markdown = '# Hi, *Pluto*!' -ReactDom.render(# Hello, *world*!, document.body) +createRoot(document.body).render({markdown}) ```
Show equivalent JSX -```jsx +```js

- Hello, world! + Hi, Pluto!

```
-Here is an example that shows passing the markdown as a string and how -to use a plugin ([`remark-gfm`][gfm], which adds support for strikethrough, -tables, tasklists and URLs directly): +Here is an example that shows how to use a plugin +([`remark-gfm`][github-remark-gfm], +which adds support for footnotes, strikethrough, tables, tasklists and +URLs directly): -```jsx +```js import React from 'react' -import ReactDom from 'react-dom' -import ReactMarkdown from 'react-markdown' +import {createRoot} from 'react-dom/client' +import Markdown from 'react-markdown' import remarkGfm from 'remark-gfm' -const markdown = `Just a link: https://reactjs.com.` +const markdown = `Just a link: www.nasa.gov.` -ReactDom.render( - , - document.body +createRoot(document.body).render( + {markdown} ) ```
Show equivalent JSX -```jsx +```js

- Just a link: https://reactjs.com. + Just a link: www.nasa.gov.

``` @@ -161,75 +168,214 @@ ReactDom.render( ## API -This package exports the following identifier: -[`uriTransformer`][uri-transformer]. -The default export is `ReactMarkdown`. - -### `props` - -* `children` (`string`, default: `''`)\ - markdown to parse -* `components` (`Record`, default: `{}`)\ - object mapping tag names to React components -* `remarkPlugins` (`Array`, default: `[]`)\ - list of [remark plugins][remark-plugins] to use -* `rehypePlugins` (`Array`, default: `[]`)\ - list of [rehype plugins][rehype-plugins] to use -* `className` (`string?`)\ - wrap the markdown in a `div` with this class name -* `skipHtml` (`boolean`, default: `false`)\ - ignore HTML in markdown completely -* `sourcePos` (`boolean`, default: `false`)\ - pass a prop to all components with a serialized position - (`data-sourcepos="3:1-3:13"`) -* `rawSourcePos` (`boolean`, default: `false`)\ - pass a prop to all components with their [position][] - (`sourcePosition: {start: {line: 3, column: 1}, end:…}`) -* `includeElementIndex` (`boolean`, default: `false`)\ - pass the `index` (number of elements before it) and `siblingCount` (number - of elements in parent) as props to all components -* `allowedElements` (`Array`, default: `undefined`)\ - tag names to allow (can’t combine w/ `disallowedElements`), all tag names - are allowed by default -* `disallowedElements` (`Array`, default: `undefined`)\ - tag names to disallow (can’t combine w/ `allowedElements`), all tag names - are allowed by default -* `allowElement` (`(element, index, parent) => boolean?`, optional)\ - function called to check if an element is allowed (when truthy) or not, - `allowedElements` or `disallowedElements` is used first! -* `unwrapDisallowed` (`boolean`, default: `false`)\ - extract (unwrap) the children of not allowed elements, by default, when - `strong` is disallowed, it and it’s children are dropped, but with - `unwrapDisallowed` the element itself is replaced by its children -* `linkTarget` (`string` or `(href, children, title) => string`, optional)\ - target to use on links (such as `_blank` for ` string`, default: - [`uriTransformer`][uri-transformer], optional)\ - change URLs on links, pass `null` to allow all URLs, see [security][] -* `transformImageUri` (`(src, alt, title) => string`, default: - [`uriTransformer`][uri-transformer], optional)\ - change URLs on images, pass `null` to allow all URLs, see [security][] - -### `uriTransformer` - -Our default URL transform, which you can overwrite (see props above). -It’s given a URL and cleans it, by allowing only `http:`, `https:`, `mailto:`, -and `tel:` URLs, absolute paths (`/example.png`), and hashes (`#some-place`). - -See the [source code here][uri]. +This package exports the identifiers +[`MarkdownAsync`][api-markdown-async], +[`MarkdownHooks`][api-markdown-hooks], +and +[`defaultUrlTransform`][api-default-url-transform]. +The default export is [`Markdown`][api-markdown]. + +It also exports the additional [TypeScript][] types +[`AllowElement`][api-allow-element], +[`Components`][api-components], +[`ExtraProps`][api-extra-props], +[`HooksOptions`][api-hooks-options], +[`Options`][api-options], +and +[`UrlTransform`][api-url-transform]. + +### `Markdown` + +Component to render markdown. + +This is a synchronous component. +When using async plugins, +see [`MarkdownAsync`][api-markdown-async] or +[`MarkdownHooks`][api-markdown-hooks]. + +###### Parameters + +* `options` ([`Options`][api-options]) + — props + +###### Returns + +React element (`ReactElement`). + +### `MarkdownAsync` + +Component to render markdown with support for async plugins +through async/await. + +Components returning promises are supported on the server. +For async support on the client, +see [`MarkdownHooks`][api-markdown-hooks]. + +###### Parameters + +* `options` ([`Options`][api-options]) + — props + +###### Returns + +Promise to a React element (`Promise`). + +### `MarkdownHooks` + +Component to render markdown with support for async plugins through hooks. + +This uses `useEffect` and `useState` hooks. +Hooks run on the client and do not immediately render something. +For async support on the server, +see [`MarkdownAsync`][api-markdown-async]. + +###### Parameters + +* `options` ([`Options`][api-options]) + — props + +###### Returns + +React node (`ReactNode`). + +### `defaultUrlTransform(url)` + +Make a URL safe. + +This follows how GitHub works. +It allows the protocols `http`, `https`, `irc`, `ircs`, `mailto`, and `xmpp`, +and URLs relative to the current protocol (such as `/something`). + +###### Parameters + +* `url` (`string`) + — URL + +###### Returns + +Safe URL (`string`). + +### `AllowElement` + +Filter elements (TypeScript type). + +###### Parameters + +* `node` ([`Element` from `hast`][github-hast-element]) + — element to check +* `index` (`number | undefined`) + — index of `element` in `parent` +* `parent` ([`Node` from `hast`][github-hast-nodes]) + — parent of `element` + +###### Returns + +Whether to allow `element` (`boolean`, optional). + +### `Components` + +Map tag names to components (TypeScript type). + +###### Type + +```ts +import type {ExtraProps} from 'react-markdown' +import type {ComponentProps, ElementType} from 'react' + +type Components = { + [Key in Extract]?: ElementType & ExtraProps> +} +``` + +### `ExtraProps` + +Extra fields we pass to components (TypeScript type). + +###### Fields + +* `node` ([`Element` from `hast`][github-hast-element], optional) + — original node + +### `HooksOptions` + +Configuration for [`MarkdownHooks`][api-markdown-hooks] (TypeScript type); +extends the regular [`Options`][api-options] with a `fallback` prop. + +###### Extends + +[`Options`][api-options]. + +###### Fields + +* `fallback` (`ReactNode`, optional) + — content to render while the processor processing the markdown + +### `Options` + +Configuration (TypeScript type). + +###### Fields + +* `allowElement` ([`AllowElement`][api-allow-element], optional) + — filter elements; + `allowedElements` / `disallowedElements` is used first +* `allowedElements` (`Array`, default: all tag names) + — tag names to allow; + cannot combine w/ `disallowedElements` +* `children` (`string`, optional) + — markdown +* `components` ([`Components`][api-components], optional) + — map tag names to components +* `disallowedElements` (`Array`, default: `[]`) + — tag names to disallow; + cannot combine w/ `allowedElements` +* `rehypePlugins` (`Array`, optional) + — list of [rehype plugins][github-rehype-plugins] to use +* `remarkPlugins` (`Array`, optional) + — list of [remark plugins][github-remark-plugins] to use +* `remarkRehypeOptions` + ([`Options` from `remark-rehype`][github-remark-rehype-options], + optional) + — options to pass through to `remark-rehype` +* `skipHtml` (`boolean`, default: `false`) + — ignore HTML in markdown completely +* `unwrapDisallowed` (`boolean`, default: `false`) + — extract (unwrap) what’s in disallowed elements; + normally when say `strong` is not allowed, it and it’s children are dropped, + with `unwrapDisallowed` the element itself is replaced by its children +* `urlTransform` ([`UrlTransform`][api-url-transform], default: + [`defaultUrlTransform`][api-default-url-transform]) + — change URLs + +### `UrlTransform` + +Transform URLs (TypeScript type). + +###### Parameters + +* `url` (`string`) + — URL +* `key` (`string`, example: `'href'`) + — property name +* `node` ([`Element` from `hast`][github-hast-element]) + — element to check + +###### Returns + +Transformed URL (`string`, optional). ## Examples ### Use a plugin This example shows how to use a remark plugin. -In this case, [`remark-gfm`][gfm], which adds support for strikethrough, tables, -tasklists and URLs directly: +In this case, [`remark-gfm`][github-remark-gfm], +which adds support for strikethrough, tables, tasklists and URLs directly: -```jsx +```js import React from 'react' -import ReactMarkdown from 'react-markdown' -import ReactDom from 'react-dom' +import {createRoot} from 'react-dom/client' +import Markdown from 'react-markdown' import remarkGfm from 'remark-gfm' const markdown = `A paragraph with *emphasis* and **strong importance**. @@ -246,16 +392,15 @@ A table: | - | - | ` -ReactDom.render( - , - document.body +createRoot(document.body).render( + {markdown} ) ```
Show equivalent JSX -```jsx +```js <>

A paragraph with emphasis and strong importance. @@ -266,21 +411,21 @@ ReactDom.render( https://reactjs.org.

-
    +
    • Lists
    • -
    • - todo +
    • + todo
    • -
    • - done +
    • + done

    A table:

    - - + +
    abab
    @@ -294,26 +439,28 @@ ReactDom.render( This example shows how to use a plugin and give it options. To do that, use an array with the plugin at the first place, and the options second. -[`remark-gfm`][gfm] has an option to allow only double tildes for strikethrough: +[`remark-gfm`][github-remark-gfm] has an option to allow only double tildes for +strikethrough: -```jsx +```js import React from 'react' -import ReactMarkdown from 'react-markdown' -import ReactDom from 'react-dom' +import {createRoot} from 'react-dom/client' +import Markdown from 'react-markdown' import remarkGfm from 'remark-gfm' -ReactDom.render( - - This ~is not~ strikethrough, but ~~this is~~! - , - document.body +const markdown = 'This ~is not~ strikethrough, but ~~this is~~!' + +createRoot(document.body).render( + + {markdown} + ) ```
    Show equivalent JSX -```jsx +```js

    This ~is not~ strikethrough, but this is!

    @@ -326,13 +473,15 @@ ReactDom.render( This example shows how you can overwrite the normal handling of an element by passing a component. In this case, we apply syntax highlighting with the seriously super amazing -[`react-syntax-highlighter`][react-syntax-highlighter] by -[**@conorhastings**][conor]: +[`react-syntax-highlighter`][github-react-syntax-highlighter] by +[**@conorhastings**][github-conorhastings]: -```jsx + + +```js import React from 'react' -import ReactDom from 'react-dom' -import ReactMarkdown from 'react-markdown' +import {createRoot} from 'react-dom/client' +import Markdown from 'react-markdown' import {Prism as SyntaxHighlighter} from 'react-syntax-highlighter' import {dark} from 'react-syntax-highlighter/dist/esm/styles/prism' @@ -344,36 +493,36 @@ console.log('It works!') ~~~ ` -ReactDom.render( - ) : ( - + {children} ) } }} - />, - document.body + /> ) ```
    Show equivalent JSX -```jsx +```js <>

    Here is some JavaScript code:

    @@ -386,43 +535,40 @@ ReactDom.render(
     
     ### Use remark and rehype plugins (math)
     
    -This example shows how a syntax extension (through [`remark-math`][math])
    +This example shows how a syntax extension
    +(through [`remark-math`][github-remark-math])
     is used to support math in markdown, and a transform plugin
    -([`rehype-katex`][katex]) to render that math.
    +([`rehype-katex`][github-rehype-katex]) to render that math.
     
    -```jsx
    +```js
     import React from 'react'
    -import ReactDom from 'react-dom'
    -import ReactMarkdown from 'react-markdown'
    -import remarkMath from 'remark-math'
    +import {createRoot} from 'react-dom/client'
    +import Markdown from 'react-markdown'
     import rehypeKatex from 'rehype-katex'
    -
    +import remarkMath from 'remark-math'
     import 'katex/dist/katex.min.css' // `rehype-katex` does not import the CSS for you
     
    -ReactDom.render(
    -  ,
    -  document.body
    +const markdown = `The lift coefficient ($C_L$) is a dimensionless coefficient.`
    +
    +createRoot(document.body).render(
    +  
    +    {markdown}
    +  
     )
     ```
     
     
    Show equivalent JSX -```jsx +```js

    The lift coefficient ( - - - - {/* … */} - - + + + {/* … */} + + ) is a dimensionless coefficient. @@ -433,17 +579,21 @@ ReactDom.render( ## Plugins -We use [unified][], specifically [remark][] for markdown and [rehype][] for -HTML, which are tools to transform content with plugins. +We use [unified][github-unified], +specifically [remark][github-remark] for markdown and +[rehype][github-rehype] for HTML, +which are tools to transform content with plugins. Here are three good ways to find plugins: -* [`awesome-remark`][awesome-remark] and [`awesome-rehype`][awesome-rehype] - — selection of the most awesome projects -* [List of remark plugins][remark-plugins] and - [list of rehype plugins][rehype-plugins] - — list of all plugins -* [`remark-plugin`][remark-plugin] and [`rehype-plugin`][rehype-plugin] topics - — any tagged repo on GitHub +* [`awesome-remark`][github-awesome-remark] and + [`awesome-rehype`][github-awesome-rehype] + — selection of the most awesome projects +* [List of remark plugins][github-remark-plugins] and + [list of rehype plugins][github-rehype-plugins] + — list of all plugins +* [`remark-plugin`][github-topic-remark-plugin] and + [`rehype-plugin`][github-topic-rehype-plugin] topics + — any tagged repo on GitHub ## Syntax @@ -451,22 +601,20 @@ Here are three good ways to find plugins: markdown implementations, by default. Some syntax extensions are supported through plugins. -We use [`micromark`][micromark] under the hood for our parsing. +We use [`micromark`][github-micromark] under the hood for our parsing. See its documentation for more information on markdown, CommonMark, and extensions. -## Types - -This package is fully typed with [TypeScript][]. -It exports `Options` and `Components` types, which specify the interface of the -accepted props and components. - ## Compatibility -Projects maintained by the unified collective are compatible with all maintained +Projects maintained by the unified collective are compatible with maintained versions of Node.js. -As of now, that is Node.js 12.20+, 14.14+, and 16.0+. -Our projects sometimes work with older versions, but this is not guaranteed. + +When we cut a new major release, we drop support for unmaintained versions of +Node. +This means we try to keep the current release line, `react-markdown@10`, +compatible with Node.js 16. + They work in all modern browsers (essentially: everything not IE 11). You can use a bundler (such as esbuild, webpack, or Rollup) to use this package in your project, and use its options (or plugins) to add support for legacy @@ -487,18 +635,19 @@ browsers.

    To understand what this project does, it’s important to first understand what -unified does: please read through the [`unifiedjs/unified`][unified] readme (the -part until you hit the API section is required reading). +unified does: please read through the [`unifiedjs/unified`][github-unified] +readme +(the part until you hit the API section is required reading). `react-markdown` is a unified pipeline — wrapped so that most folks don’t need to directly interact with unified. The processor goes through these steps: -* parse markdown to mdast (markdown syntax tree) -* transform through remark (markdown ecosystem) -* transform mdast to hast (HTML syntax tree) -* transform through rehype (HTML ecosystem) -* render hast to React with components +* parse markdown to mdast (markdown syntax tree) +* transform through remark (markdown ecosystem) +* transform mdast to hast (HTML syntax tree) +* transform through rehype (HTML ecosystem) +* render hast to React with components ## Appendix A: HTML in markdown @@ -507,39 +656,40 @@ because it is dangerous and defeats the purpose of this library. However, if you are in a trusted environment (you trust the markdown), and can spare the bundle size (±60kb minzipped), then you can use -[`rehype-raw`][raw]: +[`rehype-raw`][github-rehype-raw]: -```jsx +```js import React from 'react' -import ReactDom from 'react-dom' -import ReactMarkdown from 'react-markdown' +import {createRoot} from 'react-dom/client' +import Markdown from 'react-markdown' import rehypeRaw from 'rehype-raw' -const input = `
    +const markdown = `
    Some *emphasis* and strong!
    ` -ReactDom.render( - , - document.body +createRoot(document.body).render( + {markdown} ) ```
    Show equivalent JSX -```jsx -
    -

    Some emphasis and strong!

    +```js +
    +

    + Some emphasis and strong! +

    ```
    **Note**: HTML in markdown is still bound by how [HTML works in -CommonMark][cm-html]. +CommonMark][commonmark-html]. Make sure to use blank lines around block-level HTML that again contains markdown! @@ -547,13 +697,16 @@ markdown! You can also change the things that come from markdown: -```jsx - + em(props) { + const {node, ...rest} = props + return + } }} /> ``` @@ -563,216 +716,229 @@ markdown (such as `h1` for `# heading`). Normally, in markdown, those are: `a`, `blockquote`, `br`, `code`, `em`, `h1`, `h2`, `h3`, `h4`, `h5`, `h6`, `hr`, `img`, `li`, `ol`, `p`, `pre`, `strong`, and `ul`. -With [`remark-gfm`][gfm], you can also use: `del`, `input`, `table`, `tbody`, -`td`, `th`, `thead`, and `tr`. +With [`remark-gfm`][github-remark-gfm], +you can also use `del`, `input`, `table`, `tbody`, `td`, `th`, `thead`, and `tr`. Other remark or rehype plugins that add support for new constructs will also work with `react-markdown`. The props that are passed are what you probably would expect: an `a` (link) will -get `href` (and `title`) props, and `img` (image) an `src` (and `title`), etc. -There are some extra props passed. - -* `code` - * `inline` (`boolean?`) - — set to `true` for inline code - * `className` (`string?`) - — set to `language-js` or so when using ` ```js ` -* `h1`, `h2`, `h3`, `h4`, `h5`, `h6` - * `level` (`number` between 1 and 6) - — heading rank -* `input` (when using [`remark-gfm`][gfm]) - * `checked` (`boolean`) - — whether the item is checked - * `disabled` (`true`) - * `type` (`'checkbox'`) -* `li` - * `index` (`number`) - — number of preceding items (so first gets `0`, etc.) - * `ordered` (`boolean`) - — whether the parent is an `ol` or not - * `checked` (`boolean?`) - — `null` normally, `boolean` when using [`remark-gfm`][gfm]’s tasklists - * `className` (`string?`) - — set to `task-list-item` when using [`remark-gfm`][gfm] and the - item1 is a tasklist -* `ol`, `ul` - * `depth` (`number`) - — number of ancestral lists (so first gets `0`, etc.) - * `ordered` (`boolean`) - — whether it’s an `ol` or not - * `className` (`string?`) - — set to `contains-task-list` when using [`remark-gfm`][gfm] and the - list contains one or more tasklists -* `td`, `th` (when using [`remark-gfm`][gfm]) - * `style` (`Object?`) - — something like `{textAlign: 'left'}` depending on how the cell is - aligned - * `isHeader` (`boolean`) - — whether it’s a `th` or not -* `tr` (when using [`remark-gfm`][gfm]) - * `isHeader` (`boolean`) - — whether it’s in the `thead` or not - -Every component will receive a `node` (`Object`). -This is the original [hast](https://github.com/syntax-tree/hast) element being +get `href` (and `title`) props, and `img` (image) an `src`, `alt` and `title`, +etc. + +Every component will receive a `node`. +This is the original [`Element` from `hast`][github-hast-element] element being turned into a React element. -Every element will receive a `key` (`string`). -See [React’s docs](https://reactjs.org/docs/lists-and-keys.html#keys) for more -info. +## Appendix C: line endings in markdown (and JSX) + +You might have trouble with how line endings work in markdown and JSX. +We recommend the following, which solves all line ending problems: -Optionally, components will also receive: +```js +// If you write actual markdown in your code, put your markdown in a variable; +// **do not indent markdown**: +const markdown = ` +# This is perfect! +` + +// Pass the value as an expression as an only child: +const result = {markdown} +``` -* `data-sourcepos` (`string`) - — see `sourcePos` option -* `sourcePosition` (`Object`) - — see `rawSourcePos` option -* `index` and `siblingCount` (`number`) - — see `includeElementIndex` option -* `target` on `a` (`string`) - — see `linkTarget` option +👆 That works. +Read on for what doesn’t and why that is. + +You might try to write markdown directly in your JSX and find that it **does +not** work: + +```js + + # Hi + + This is **not** a paragraph. + +``` + +The is because in JSX the whitespace (including line endings) is collapsed to +a single space. +So the above example is equivalent to: + +```js + # Hi This is **not** a paragraph. +``` + +Instead, to pass markdown to `Markdown`, you can use an expression: +with a template literal: + +```js +{` +# Hi + +This is a paragraph. +`} +``` + +Template literals have another potential problem, because they keep whitespace +(including indentation) inside them. +That means that the following **does not** turn into a heading: + +```js +{` + # This is **not** a heading, it’s an indented code block +`} +``` ## Security Use of `react-markdown` is secure by default. -Overwriting `transformLinkUri` or `transformImageUri` to something insecure will -open you up to XSS vectors. +Overwriting `urlTransform` to something insecure will open you up to XSS +vectors. Furthermore, the `remarkPlugins`, `rehypePlugins`, and `components` you use may be insecure. To make sure the content is completely safe, even after what plugins do, -use [`rehype-sanitize`][sanitize]. +use [`rehype-sanitize`][github-rehype-sanitize]. It lets you define your own schema of what is and isn’t allowed. ## Related -* [`MDX`](https://github.com/mdx-js/mdx) - — JSX *in* markdown -* [`remark-gfm`](https://github.com/remarkjs/remark-gfm) - — add support for GitHub flavored markdown support -* [`react-remark`][react-remark] - — modern hook based alternative -* [`rehype-react`][rehype-react] - — turn HTML into React elements +* [`MDX`][github-mdx] + — JSX *in* markdown +* [`remark-gfm`][github-remark-gfm] + — add support for GitHub flavored markdown support +* [`react-remark`][github-react-remark] + — hook based alternative +* [`rehype-react`][github-rehype-react] + — turn HTML into React elements ## Contribute -See [`contributing.md`][contributing] in [`remarkjs/.github`][health] for ways -to get started. -See [`support.md`][support] for ways to get help. +See [`contributing.md`][health-contributing] in [`remarkjs/.github`][health] +for ways to get started. +See [`support.md`][health-support] for ways to get help. -This project has a [code of conduct][coc]. +This project has a [code of conduct][health-coc]. By interacting with this repository, organization, or community you agree to abide by its terms. ## License -[MIT][license] © [Espen Hovlandsdal][author] +[MIT][file-license] © [Espen Hovlandsdal][author] -[build-badge]: https://github.com/remarkjs/react-markdown/workflows/main/badge.svg +[api-allow-element]: #allowelement -[build]: https://github.com/remarkjs/react-markdown/actions +[api-components]: #components -[coverage-badge]: https://img.shields.io/codecov/c/github/remarkjs/react-markdown.svg +[api-default-url-transform]: #defaulturltransformurl -[coverage]: https://codecov.io/github/remarkjs/react-markdown +[api-extra-props]: #extraprops -[downloads-badge]: https://img.shields.io/npm/dm/react-markdown.svg +[api-hooks-options]: #hooksoptions -[downloads]: https://www.npmjs.com/package/react-markdown +[api-markdown]: #markdown -[size-badge]: https://img.shields.io/bundlephobia/minzip/react-markdown.svg +[api-markdown-async]: #markdownasync -[size]: https://bundlephobia.com/result?p=react-markdown +[api-markdown-hooks]: #markdownhooks -[sponsors-badge]: https://opencollective.com/unified/sponsors/badge.svg +[api-options]: #options -[backers-badge]: https://opencollective.com/unified/backers/badge.svg +[api-url-transform]: #urltransform -[collective]: https://opencollective.com/unified +[author]: https://espen.codes/ -[chat-badge]: https://img.shields.io/badge/chat-discussions-success.svg +[badge-build-image]: https://github.com/remarkjs/react-markdown/workflows/main/badge.svg -[chat]: https://github.com/remarkjs/remark/discussions +[badge-build-url]: https://github.com/remarkjs/react-markdown/actions -[npm]: https://docs.npmjs.com/cli/install +[badge-coverage-image]: https://img.shields.io/codecov/c/github/remarkjs/react-markdown.svg -[skypack]: https://www.skypack.dev +[badge-coverage-url]: https://codecov.io/github/remarkjs/react-markdown -[health]: https://github.com/remarkjs/.github +[badge-downloads-image]: https://img.shields.io/npm/dm/react-markdown.svg -[contributing]: https://github.com/remarkjs/.github/blob/HEAD/contributing.md +[badge-downloads-url]: https://www.npmjs.com/package/react-markdown -[support]: https://github.com/remarkjs/.github/blob/HEAD/support.md +[badge-size-image]: https://img.shields.io/bundlejs/size/react-markdown -[coc]: https://github.com/remarkjs/.github/blob/HEAD/code-of-conduct.md +[badge-size-url]: https://bundlejs.com/?q=react-markdown -[license]: license +[commonmark-help]: https://commonmark.org/help/ -[author]: https://espen.codes/ +[commonmark-html]: https://spec.commonmark.org/0.31.2/#html-blocks -[micromark]: https://github.com/micromark/micromark +[esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c -[remark]: https://github.com/remarkjs/remark +[esmsh]: https://esm.sh -[demo]: https://remarkjs.github.io/react-markdown/ +[file-license]: license -[position]: https://github.com/syntax-tree/unist#position +[github-awesome-rehype]: https://github.com/rehypejs/awesome-rehype -[gfm]: https://github.com/remarkjs/remark-gfm +[github-awesome-remark]: https://github.com/remarkjs/awesome-remark -[math]: https://github.com/remarkjs/remark-math +[github-conorhastings]: https://github.com/conorhastings -[katex]: https://github.com/remarkjs/remark-math/tree/main/packages/rehype-katex +[github-hast-element]: https://github.com/syntax-tree/hast#element -[raw]: https://github.com/rehypejs/rehype-raw +[github-hast-nodes]: https://github.com/syntax-tree/hast#nodes -[sanitize]: https://github.com/rehypejs/rehype-sanitize +[github-io-react-markdown]: https://remarkjs.github.io/react-markdown/ -[remark-plugins]: https://github.com/remarkjs/remark/blob/main/doc/plugins.md#list-of-plugins +[github-mdx]: https://github.com/mdx-js/mdx/ -[rehype-plugins]: https://github.com/rehypejs/rehype/blob/main/doc/plugins.md#list-of-plugins +[github-micromark]: https://github.com/micromark/micromark -[awesome-remark]: https://github.com/remarkjs/awesome-remark +[github-react-remark]: https://github.com/remarkjs/react-remark -[awesome-rehype]: https://github.com/rehypejs/awesome-rehype +[github-react-syntax-highlighter]: https://github.com/react-syntax-highlighter/react-syntax-highlighter -[remark-plugin]: https://github.com/topics/remark-plugin +[github-rehype]: https://github.com/rehypejs/rehype -[rehype-plugin]: https://github.com/topics/rehype-plugin +[github-rehype-katex]: https://github.com/remarkjs/remark-math/tree/main/packages/rehype-katex -[cm-html]: https://spec.commonmark.org/0.30/#html-blocks +[github-rehype-plugins]: https://github.com/rehypejs/rehype/blob/main/doc/plugins.md#list-of-plugins -[uri]: https://github.com/remarkjs/react-markdown/blob/main/lib/uri-transformer.js +[github-rehype-raw]: https://github.com/rehypejs/rehype-raw -[uri-transformer]: #uritransformer +[github-rehype-react]: https://github.com/rehypejs/rehype-react -[react]: http://reactjs.org +[github-rehype-sanitize]: https://github.com/rehypejs/rehype-sanitize -[cheat]: https://commonmark.org/help/ +[github-remark]: https://github.com/remarkjs/remark -[unified]: https://github.com/unifiedjs/unified +[github-remark-gfm]: https://github.com/remarkjs/remark-gfm -[rehype]: https://github.com/rehypejs/rehype +[github-remark-math]: https://github.com/remarkjs/remark-math -[react-remark]: https://github.com/remarkjs/react-remark +[github-remark-plugins]: https://github.com/remarkjs/remark/blob/main/doc/plugins.md#list-of-plugins -[rehype-react]: https://github.com/rehypejs/rehype-react +[github-remark-rehype-options]: https://github.com/remarkjs/remark-rehype#options -[mdx]: https://github.com/mdx-js/mdx/ +[github-topic-rehype-plugin]: https://github.com/topics/rehype-plugin -[typescript]: https://www.typescriptlang.org +[github-topic-remark-plugin]: https://github.com/topics/remark-plugin + +[github-unified]: https://github.com/unifiedjs/unified -[security]: #security +[health]: https://github.com/remarkjs/.github + +[health-coc]: https://github.com/remarkjs/.github/blob/main/code-of-conduct.md -[components]: #appendix-b-components +[health-contributing]: https://github.com/remarkjs/.github/blob/main/contributing.md -[plugins]: #plugins +[health-support]: https://github.com/remarkjs/.github/blob/main/support.md -[syntax]: #syntax +[npm-install]: https://docs.npmjs.com/cli/install -[react-syntax-highlighter]: https://github.com/react-syntax-highlighter/react-syntax-highlighter +[react]: http://reactjs.org -[conor]: https://github.com/conorhastings +[section-components]: #appendix-b-components -[esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c +[section-plugins]: #plugins + +[section-security]: #security + +[section-syntax]: #syntax + +[typescript]: https://www.typescriptlang.org diff --git a/script/load-jsx.js b/script/load-jsx.js new file mode 100644 index 00000000..d78a33c7 --- /dev/null +++ b/script/load-jsx.js @@ -0,0 +1,89 @@ +import fs from 'node:fs/promises' +import {fileURLToPath} from 'node:url' +import {transform} from 'esbuild' + +const {getFormat, load, transformSource} = createLoader() + +export {getFormat, load, transformSource} + +/** + * A tiny JSX loader. + */ +export function createLoader() { + return {load, getFormat, transformSource} + + // Node version 17. + /** + * @param {string} href + * @param {unknown} context + * @param {Function} defaultLoad + */ + async function load(href, context, defaultLoad) { + const url = new URL(href) + + if (!url.pathname.endsWith('.jsx')) { + return defaultLoad(href, context, defaultLoad) + } + + const {code, warnings} = await transform(String(await fs.readFile(url)), { + format: 'esm', + loader: 'jsx', + sourcefile: fileURLToPath(url), + sourcemap: 'both', + target: 'esnext' + }) + + if (warnings) { + for (const warning of warnings) { + console.log(warning.location) + console.log(warning.text) + } + } + + return {format: 'module', shortCircuit: true, source: code} + } + + // Pre version 17. + /** + * @param {string} href + * @param {unknown} context + * @param {Function} defaultGetFormat + */ + function getFormat(href, context, defaultGetFormat) { + const url = new URL(href) + + return url.pathname.endsWith('.jsx') + ? {format: 'module'} + : defaultGetFormat(href, context, defaultGetFormat) + } + + /** + * @param {Buffer} value + * @param {{url: string, [x: string]: unknown}} context + * @param {Function} defaultTransformSource + */ + async function transformSource(value, context, defaultTransformSource) { + const url = new URL(context.url) + + if (!url.pathname.endsWith('.jsx')) { + return defaultTransformSource(value, context, defaultTransformSource) + } + + const {code, warnings} = await transform(String(value), { + format: context.format === 'module' ? 'esm' : 'cjs', + loader: 'jsx', + sourcefile: fileURLToPath(url), + sourcemap: 'both', + target: 'esnext' + }) + + if (warnings) { + for (const warning of warnings) { + console.log(warning.location) + console.log(warning.text) + } + } + + return {source: code} + } +} diff --git a/test.jsx b/test.jsx new file mode 100644 index 00000000..1b254db0 --- /dev/null +++ b/test.jsx @@ -0,0 +1,1280 @@ +/* @jsxRuntime automatic @jsxImportSource react */ +/** + * @import {Root} from 'hast' + * @import {ComponentProps, ReactNode} from 'react' + * @import {ExtraProps} from 'react-markdown' + * @import {Plugin} from 'unified' + */ + +/** + * @typedef DeferredPlugin + * Deferred plugin. + * @property {Plugin<[]>} plugin + * Plugin. + * @property {(error: Error) => undefined} reject + * Reject the plugin. + * @property {() => undefined} resolve + * Resolve the plugin. + */ + +import assert from 'node:assert/strict' +import test from 'node:test' +import 'global-jsdom/register' +import {render, waitFor} from '@testing-library/react' +import concatStream from 'concat-stream' +import {Component} from 'react' +import {renderToPipeableStream, renderToStaticMarkup} from 'react-dom/server' +import Markdown, {MarkdownAsync, MarkdownHooks} from 'react-markdown' +import rehypeRaw from 'rehype-raw' +import rehypeStarryNight from 'rehype-starry-night' +import remarkGfm from 'remark-gfm' +import remarkToc from 'remark-toc' +import {visit} from 'unist-util-visit' + +const decoder = new TextDecoder() + +test('react-markdown (core)', async function (t) { + await t.test('should expose the public api', async function () { + assert.deepEqual(Object.keys(await import('react-markdown')).sort(), [ + 'MarkdownAsync', + 'MarkdownHooks', + 'default', + 'defaultUrlTransform' + ]) + }) +}) + +test('Markdown', async function (t) { + await t.test('should work', function () { + assert.equal(renderToStaticMarkup(), '

    a

    ') + }) + + await t.test('should throw w/ `source`', function () { + assert.throws(function () { + // @ts-expect-error: check how the runtime handles untyped `source`. + renderToStaticMarkup() + }, /Unexpected `source` prop, use `children` instead/) + }) + + await t.test('should throw w/ non-string children (number)', function () { + assert.throws(function () { + // @ts-expect-error: check how the runtime handles invalid `children`. + renderToStaticMarkup() + }, /Unexpected value `1` for `children` prop, expected `string`/) + }) + + await t.test('should throw w/ non-string children (boolean)', function () { + assert.throws(function () { + // @ts-expect-error: check how the runtime handles invalid `children`. + renderToStaticMarkup() + }, /Unexpected value `true` for `children` prop, expected `string`/) + }) + + await t.test('should support `null` as children', function () { + assert.equal(renderToStaticMarkup(), '') + }) + + await t.test('should support `undefined` as children', function () { + assert.equal(renderToStaticMarkup(), '') + }) + + await t.test('should warn w/ `allowDangerousHtml`', function () { + assert.throws(function () { + // @ts-expect-error: check how the runtime handles deprecated `allowDangerousHtml`. + renderToStaticMarkup() + }, /Unexpected `allowDangerousHtml` prop, remove it/) + }) + + await t.test('should support a block quote', function () { + assert.equal( + renderToStaticMarkup(), + '
    \n

    a

    \n
    ' + ) + }) + + await t.test('should support a break', function () { + assert.equal( + renderToStaticMarkup(), + '

    a
    \nb

    ' + ) + }) + + await t.test('should support a code (block, flow; indented)', function () { + assert.equal( + renderToStaticMarkup(), + '
    a\n
    ' + ) + }) + + await t.test('should support a code (block, flow; fenced)', function () { + assert.equal( + renderToStaticMarkup(), + '
    a\n
    ' + ) + }) + + await t.test('should support a delete (GFM)', function () { + assert.equal( + renderToStaticMarkup( + + ), + '

    a

    ' + ) + }) + + await t.test('should support an emphasis', function () { + assert.equal( + renderToStaticMarkup(), + '

    a

    ' + ) + }) + + await t.test('should support a footnote (GFM)', function () { + assert.equal( + renderToStaticMarkup( + + ), + '

    a1

    \n

    Footnotes

    \n
      \n
    1. \n

      y

      \n
    2. \n
    \n
    ' + ) + }) + + await t.test('should support a heading', function () { + assert.equal( + renderToStaticMarkup(), + '

    a

    ' + ) + }) + + await t.test('should support an html (default)', function () { + assert.equal( + renderToStaticMarkup(), + '

    <i>a</i>

    ' + ) + }) + + await t.test('should support an html (w/ `rehype-raw`)', function () { + assert.equal( + renderToStaticMarkup( + + ), + '

    a

    ' + ) + }) + + await t.test('should support an image', function () { + assert.equal( + renderToStaticMarkup(), + // Note: React weirdly adds `rel="preload"`. + '

    a

    ' + ) + }) + + await t.test('should support an image w/ a title', function () { + assert.equal( + renderToStaticMarkup(), + // Note: React weirdly adds `rel="preload"`. + '

    a

    ' + ) + }) + + await t.test('should support an image reference / definition', function () { + assert.equal( + renderToStaticMarkup(), + // Note: React weirdly adds `rel="preload"`. + '

    a

    ' + ) + }) + + await t.test('should support code (text, inline)', function () { + assert.equal( + renderToStaticMarkup(), + '

    a

    ' + ) + }) + + await t.test('should support a link', function () { + assert.equal( + renderToStaticMarkup(), + '

    a

    ' + ) + }) + + await t.test('should support a link w/ a title', function () { + assert.equal( + renderToStaticMarkup(), + '

    a

    ' + ) + }) + + await t.test('should support a link reference / definition', function () { + assert.equal( + renderToStaticMarkup(), + '

    a

    ' + ) + }) + + await t.test('should support prototype poluting identifiers', function () { + assert.equal( + renderToStaticMarkup( + + ), + '

    ' + ) + }) + + await t.test('should support duplicate definitions', function () { + assert.equal( + renderToStaticMarkup(), + '

    a

    ' + ) + }) + + await t.test('should support a list (unordered) / list item', function () { + assert.equal( + renderToStaticMarkup(), + '
      \n
    • a
    • \n
    ' + ) + }) + + await t.test('should support a list (ordered) / list item', function () { + assert.equal( + renderToStaticMarkup(), + '
      \n
    1. a
    2. \n
    ' + ) + }) + + await t.test('should support a paragraph', function () { + assert.equal(renderToStaticMarkup(), '

    a

    ') + }) + + await t.test('should support a strong', function () { + assert.equal( + renderToStaticMarkup(), + '

    a

    ' + ) + }) + + await t.test('should support a table (GFM)', function () { + assert.equal( + renderToStaticMarkup( + + ), + '
    a
    b
    ' + ) + }) + + await t.test('should support a table (GFM; w/ align)', function () { + assert.equal( + renderToStaticMarkup( + + ), + '
    abcd
    ' + ) + }) + + await t.test('should support a thematic break', function () { + assert.equal(renderToStaticMarkup(), '
    ') + }) + + await t.test('should support ab absolute path', function () { + assert.equal( + renderToStaticMarkup(), + '

    ' + ) + }) + + await t.test('should support an absolute URL', function () { + assert.equal( + renderToStaticMarkup(), + '

    ' + ) + }) + + await t.test('should support a URL w/ uppercase protocol', function () { + assert.equal( + renderToStaticMarkup(), + '

    ' + ) + }) + + await t.test('should make a `javascript:` URL safe', function () { + assert.equal( + renderToStaticMarkup(), + '

    ' + ) + }) + + await t.test('should make a `vbscript:` URL safe', function () { + assert.equal( + renderToStaticMarkup(), + '

    ' + ) + }) + + await t.test('should make a `VBSCRIPT:` URL safe', function () { + assert.equal( + renderToStaticMarkup(), + '

    ' + ) + }) + + await t.test('should make a `file:` URL safe', function () { + assert.equal( + renderToStaticMarkup(), + '

    ' + ) + }) + + await t.test('should allow an empty URL', function () { + assert.equal( + renderToStaticMarkup(), + '

    ' + ) + }) + + await t.test('should support search (`?`) in a URL', function () { + assert.equal( + renderToStaticMarkup(), + '

    ' + ) + }) + + await t.test('should support hash (`&`) in a URL', function () { + assert.equal( + renderToStaticMarkup(), + '

    ' + ) + }) + + await t.test('should support hash (`#`) in a URL', function () { + assert.equal( + renderToStaticMarkup(), + '

    ' + ) + }) + + await t.test('should support `urlTransform` (`href` on `a`)', function () { + assert.equal( + renderToStaticMarkup( + + ), + '

    a

    ' + ) + }) + + await t.test('should support `urlTransform` w/ empty URLs', function () { + assert.equal( + renderToStaticMarkup( + + ), + '

    ' + ) + }) + + await t.test('should support `urlTransform` (`src` on `img`)', function () { + assert.equal( + renderToStaticMarkup( + + ), + '

    a

    ' + ) + }) + + await t.test('should support `skipHtml`', function () { + const actual = renderToStaticMarkup( + + ) + assert.equal(actual, '

    abc

    ') + }) + + await t.test( + 'should support `allowedElements` (drop unlisted nodes)', + function () { + assert.equal( + renderToStaticMarkup( + + ), + '

    \n
      \n
    • b
    • \n
    ' + ) + } + ) + + await t.test('should support `allowedElements` as a function', function () { + assert.equal( + renderToStaticMarkup( + + ), + '

    b

    ' + ) + }) + await t.test('should support `disallowedElements`', function () { + assert.equal( + renderToStaticMarkup( + + ), + '

    \n
      \n
    • b
    • \n
    ' + ) + }) + + await t.test( + 'should fail for both `allowedElements` and `disallowedElements`', + function () { + assert.throws(function () { + renderToStaticMarkup( + + ) + }, /Unexpected combined `allowedElements` and `disallowedElements`, expected one or the other/) + } + ) + + await t.test( + 'should support `unwrapDisallowed` w/ `allowedElements`', + function () { + assert.equal( + renderToStaticMarkup( + + ), + '

    a

    ' + ) + } + ) + + await t.test( + 'should support `unwrapDisallowed` w/ `disallowedElements`', + function () { + assert.equal( + renderToStaticMarkup( + + ), + '

    a

    ' + ) + } + ) + + await t.test('should support `remarkRehypeOptions`', function () { + assert.equal( + renderToStaticMarkup( + + ), + '

    1

    \n

    Footnotes

    \n
      \n
    1. \n

      a

      \n
    2. \n
    \n
    ' + ) + }) + + await t.test('should support `components`', function () { + assert.equal( + renderToStaticMarkup(), + '

    a

    ' + ) + }) + + await t.test('should support `components` as functions', function () { + assert.equal( + renderToStaticMarkup( + + } + }} + /> + ), + '
    a
    ' + ) + }) + + await t.test('should fail on an invalid component', function () { + assert.throws(function () { + renderToStaticMarkup( + + ) + }, /Element type is invalid/) + }) + + await t.test('should support `components` (headings)', function () { + let calls = 0 + + assert.equal( + renderToStaticMarkup( + + ), + '

    a

    \n

    b

    ' + ) + + assert.equal(calls, 2) + + /** + * @param {ComponentProps<'h1'> & ExtraProps} props + */ + function heading(props) { + const {node, ...rest} = props + assert(node) + assert(node.tagName === 'h1' || node.tagName === 'h2') + calls++ + return + } + }) + + await t.test('should support `components` (code)', function () { + let calls = 0 + assert.equal( + renderToStaticMarkup( + + } + }} + /> + ), + '
    a\n
    \n
    b\n
    \n

    c

    ' + ) + + assert.equal(calls, 3) + }) + + await t.test('should support `components` (li)', function () { + let calls = 0 + + assert.equal( + renderToStaticMarkup( + + } + }} + remarkPlugins={[remarkGfm]} + /> + ), + '
      \n
    • a
    • \n
    \n
      \n
    1. b
    2. \n
    ' + ) + + assert.equal(calls, 2) + }) + + await t.test('should support `components` (ol)', function () { + let calls = 0 + + assert.equal( + renderToStaticMarkup( + + } + }} + /> + ), + '
      \n
    1. a
    2. \n
    ' + ) + + assert.equal(calls, 1) + }) + + await t.test('should support `components` (ul)', function () { + let calls = 0 + + assert.equal( + renderToStaticMarkup( + + } + }} + /> + ), + '
      \n
    • a
    • \n
    ' + ) + + assert.equal(calls, 1) + }) + + await t.test('should support `components` (tr)', function () { + let calls = 0 + + assert.equal( + renderToStaticMarkup( + + } + }} + remarkPlugins={[remarkGfm]} + /> + ), + '
    a
    b
    ' + ) + + assert.equal(calls, 2) + }) + + await t.test('should support `components` (td, th)', function () { + let tdCalls = 0 + let thCalls = 0 + + assert.equal( + renderToStaticMarkup( + + }, + th(props) { + const {node, ...rest} = props + assert(node) + assert(node.tagName === 'th') + thCalls++ + return + } + }} + remarkPlugins={[remarkGfm]} + /> + ), + '
    a
    b
    ' + ) + + assert.equal(tdCalls, 1) + assert.equal(thCalls, 1) + }) + + await t.test('should pass `node` to components', function () { + let calls = 0 + assert.equal( + renderToStaticMarkup( + + } + }} + /> + ), + '

    a

    ' + ) + + assert.equal(calls, 1) + }) + + await t.test('should support plugins (`remark-gfm`)', function () { + assert.equal( + renderToStaticMarkup( + + ), + '

    a b c

    ' + ) + }) + + await t.test('should support plugins (`remark-toc`)', function () { + assert.equal( + renderToStaticMarkup( + + ), + `

    a

    +

    Contents

    +
      +
    • b +
        +
      • c
      • +
      +
    • +
    • d
    • +
    +

    b

    +

    c

    +

    d

    ` + ) + }) + + await t.test('should support aria properties', function () { + assert.equal( + renderToStaticMarkup(), + '

    c

    ' + ) + + function plugin() { + /** + * @param {Root} tree + * @returns {undefined} + */ + return function (tree) { + tree.children.unshift({ + type: 'element', + tagName: 'input', + properties: {id: 'a', ariaDescribedBy: 'b', required: true}, + children: [] + }) + } + } + }) + + await t.test('should support data properties', function () { + assert.equal( + renderToStaticMarkup(), + '

    b

    ' + ) + + function plugin() { + /** + * @param {Root} tree + * @returns {undefined} + */ + return function (tree) { + tree.children.unshift({ + type: 'element', + tagName: 'i', + properties: {dataWhatever: 'a', dataIgnoreThis: undefined}, + children: [] + }) + } + } + }) + + await t.test('should support comma separated properties', function () { + assert.equal( + renderToStaticMarkup(), + '

    c

    ' + ) + + function plugin() { + /** + * @param {Root} tree + * @returns {undefined} + */ + return function (tree) { + tree.children.unshift({ + type: 'element', + tagName: 'i', + properties: {accept: ['a', 'b']}, + children: [] + }) + } + } + }) + + await t.test('should support `style` properties', function () { + assert.equal( + renderToStaticMarkup(), + '

    a

    ' + ) + + function plugin() { + /** + * @param {Root} tree + * @returns {undefined} + */ + return function (tree) { + tree.children.unshift({ + type: 'element', + tagName: 'i', + properties: {style: 'color: red; font-weight: bold'}, + children: [] + }) + } + } + }) + + await t.test( + 'should support `style` properties w/ vendor prefixes', + function () { + assert.equal( + renderToStaticMarkup( + + ), + '

    a

    ' + ) + + function plugin() { + /** + * @param {Root} tree + * @returns {undefined} + */ + return function (tree) { + tree.children.unshift({ + type: 'element', + tagName: 'i', + properties: {style: '-ms-b: 1; -webkit-c: 2'}, + children: [] + }) + } + } + } + ) + + await t.test('should support broken `style` properties', function () { + assert.equal( + renderToStaticMarkup(), + '

    a

    ' + ) + + function plugin() { + /** + * @param {Root} tree + * @returns {undefined} + */ + return function (tree) { + tree.children.unshift({ + type: 'element', + tagName: 'i', + properties: {style: 'broken'}, + children: [] + }) + } + } + }) + + await t.test('should support SVG elements', function () { + assert.equal( + renderToStaticMarkup(), + 'SVG `<circle>` element

    a

    ' + ) + + function plugin() { + /** + * @param {Root} tree + * @returns {undefined} + */ + return function (tree) { + tree.children.unshift({ + type: 'element', + tagName: 'svg', + properties: { + viewBox: '0 0 500 500', + xmlns: 'http://www.w3.org/2000/svg' + }, + children: [ + { + type: 'element', + tagName: 'title', + properties: {}, + children: [{type: 'text', value: 'SVG `` element'}] + }, + { + type: 'element', + tagName: 'circle', + properties: {cx: 120, cy: 120, r: 100}, + children: [] + }, + // `strokeMiterLimit` in hast, `strokeMiterlimit` in React. + { + type: 'element', + tagName: 'path', + properties: {strokeMiterLimit: -1}, + children: [] + } + ] + }) + } + } + }) + + await t.test('should support comments (ignore them)', function () { + const input = 'a' + const actual = renderToStaticMarkup( + + ) + const expected = '

    a

    ' + assert.equal(actual, expected) + + function plugin() { + /** + * @param {Root} tree + * @returns {undefined} + */ + return function (tree) { + tree.children.unshift({type: 'comment', value: 'things!'}) + } + } + }) + + await t.test('should support table cells w/ style', function () { + assert.equal( + renderToStaticMarkup( + + ), + '
    a
    ' + ) + + function plugin() { + /** + * @param {Root} tree + * @returns {undefined} + */ + return function (tree) { + visit(tree, 'element', function (node) { + if (node.tagName === 'th') { + node.properties = {...node.properties, style: 'color: red'} + } + }) + } + } + }) + + await t.test('should not fail on a plugin replacing `root`', function () { + assert.equal( + renderToStaticMarkup(), + '' + ) + + function plugin() { + /** + * @returns {Root} + */ + return function () { + // @ts-expect-error: check how non-roots are handled. + return {type: 'comment', value: 'things!'} + } + } + }) +}) + +test('MarkdownAsync', async function (t) { + await t.test('should support `MarkdownAsync` (1)', async function () { + assert.throws(function () { + renderToStaticMarkup() + }, /A component suspended while responding to synchronous input/) + }) + + await t.test('should support `MarkdownAsync` (2)', async function () { + return new Promise(function (resolve, reject) { + renderToPipeableStream() + .pipe( + concatStream({encoding: 'u8'}, function (data) { + assert.equal(decoder.decode(data), '

    a

    ') + resolve() + }) + ) + .on('error', reject) + }) + }) + + await t.test( + 'should support async plugins w/ `MarkdownAsync` (`rehype-starry-night`)', + async function () { + return new Promise(function (resolve) { + renderToPipeableStream( + + ).pipe( + concatStream({encoding: 'u8'}, function (data) { + assert.equal( + decoder.decode(data), + '
    console.log(3.14)\n
    ' + ) + resolve() + }) + ) + }) + } + ) +}) + +// Note: hooks are not supported on the “server”. +test('MarkdownHooks', async function (t) { + await t.test('should support `MarkdownHooks`', async function () { + const plugin = deferPlugin() + const result = render( + + ) + + assert.equal(result.container.innerHTML, '') + + plugin.resolve() + + await waitFor(function () { + assert.notEqual(result.container.innerHTML, '') + }) + + assert.equal(result.container.innerHTML, '

    a

    ') + }) + + await t.test( + 'should support async plugins w/ `MarkdownHooks` (`rehype-starry-night`)', + async function () { + const plugin = deferPlugin() + const result = render( + + ) + + assert.equal(result.container.innerHTML, '') + + plugin.resolve() + + await waitFor(function () { + assert.notEqual(result.container.innerHTML, '') + }) + + assert.equal( + result.container.innerHTML, + '
    console.log(3.14)\n
    ' + ) + } + ) + + await t.test('should support `fallback`', async function () { + const plugin = deferPlugin() + const result = render( + + ) + + assert.equal(result.container.innerHTML, 'Loading') + + plugin.resolve() + + await waitFor(function () { + assert.notEqual(result.container.innerHTML, 'Loading') + }) + + assert.equal(result.container.innerHTML, '

    a

    ') + }) + + await t.test('should support plugins that error', async function () { + const plugin = deferPlugin() + const result = render( + + + + ) + + assert.equal(result.container.innerHTML, '') + + console.info('\nNote: the below error (`Error: rejected`) is expected.\n') + + plugin.reject(new Error('rejected')) + + await waitFor(function () { + assert.notEqual(result.container.innerHTML, '') + }) + + console.info('Note: the above error (`Error: rejected`) was expected.') + + assert.equal(result.container.innerHTML, 'Error: rejected') + }) + + await t.test('should support rerenders', async function () { + const pluginA = deferPlugin() + const pluginB = deferPlugin() + + const result = render( + + ) + + assert.equal(result.container.innerHTML, '') + + result.rerender( + + ) + + assert.equal(result.container.innerHTML, '') + + pluginA.resolve() + pluginB.resolve() + + await waitFor(function () { + assert.notEqual(result.container.innerHTML, '') + }) + + assert.equal(result.container.innerHTML, '

    b

    ') + }) +}) + +/** + * Create an async unified plugin that waits until a promise is resolved or + * rejected from the outside. + * + * @returns {DeferredPlugin} + * Deferred plugin object. + */ +function deferPlugin() { + /** @type {(error: Error) => void} */ + let hoistedReject + /** @type {() => void} */ + let hoistedResolve + /** @type {Promise} */ + const promise = new Promise(function (resolve, reject) { + hoistedResolve = resolve + hoistedReject = reject + }) + + return { + plugin() { + return function () { + return promise + } + }, + reject(error) { + hoistedReject(error) + }, + resolve() { + hoistedResolve() + } + } +} + +/** + * Basic error boundary. + */ +class ErrorBoundary extends Component { + /** + * @param {Error} error + * Error. + * @returns {undefined} + * Nothing. + */ + componentDidCatch(error) { + this.setState({error}) + } + + render() { + const props = /** @type {{children: ReactNode}} */ (this.props) + + return this.state.error ? String(this.state.error) : props.children + } + + state = { + /** + * @type {Error | undefined} + * Error. + */ + error: undefined + } +} diff --git a/test/fixtures/runthrough.html b/test/fixtures/runthrough.html deleted file mode 100644 index 8dd0c334..00000000 --- a/test/fixtures/runthrough.html +++ /dev/null @@ -1,161 +0,0 @@ -

    h1 Heading

    -

    h2 Heading

    -

    h3 Heading

    -

    h4 Heading

    -
    h5 Heading
    -
    h6 Heading
    -

    Horizontal Rules

    -
    -
    -
    -

    Emphasis

    -

    This is bold text

    -

    This is bold text

    -

    This is italic text

    -

    This is italic text

    -

    Strikethrough

    -

    Blockquotes

    -
    -

    Blockquotes can also be nested...

    -
    -

    ...by using additional greater-than signs right next to each other...

    -
    -

    ...or with spaces between arrows.

    -
    -
    -
    -

    Lists

    -

    Unordered

    -
      -
    • Create a list by starting a line with +, -, or *
    • -
    • Sub-lists are made by indenting 2 spaces: -
        -
      • Marker character change forces new list start: -
          -
        • Ac tristique libero volutpat at
        • -
        -
          -
        • Facilisis in pretium nisl aliquet
        • -
        -
          -
        • Nulla volutpat aliquam velit
        • -
        -
      • -
      -
    • -
    • Very easy!
    • -
    -

    Ordered

    -
      -
    1. Lorem ipsum dolor sit amet
    2. -
    3. Consectetur adipiscing elit
    4. -
    5. Integer molestie lorem at massa
    6. -
    -

    Or:

    -
      -
    1. You can use sequential numbers...
    2. -
    3. ...or keep all the numbers as 1.
    4. -
    -

    Start numbering with offset:

    -
      -
    1. foo
    2. -
    3. bar
    4. -
    -

    Loose lists?

    -
      -
    • -

      foo

      -
    • -
    • -

      bar

      -
    • -
    -

    Code

    -

    Inline code

    -

    Indented code

    -
    // Some comments
    -line 1 of code
    -line 2 of code
    -line 3 of code
    -
    -

    Block code "fences"

    -
    Sample text here...
    -
    -

    Syntax highlighting

    -
    var foo = function (bar) {
    -  return bar++;
    -};
    -
    -console.log(foo(5));
    -
    -

    Tables

    - - - - - - - - - - - - - - - - - - - - - -
    TagUse
    pParagraph
    tableTable
    emEmphasis
    -

    Left/right aligned columns

    - - - - - - - - - - - - - - - - - - - - - -
    ProjectStars
    React80 759
    Vue.js73 322
    sse-channel50
    -

    Links

    -

    Espen.Codes

    -

    Sanity

    -

    Autoconverted link https://github.com/remarkjs/react-markdown

    -

    Link references

    -

    Images

    -

    React Markdown -Mead

    -

    Like links, Images also have a footnote style syntax

    -

    Alt text

    -

    With a reference later in the document defining the URL location:

    -

    Hard breaks

    -

    Yeah, hard breaks
    -can be useful too.

    -

    HTML entities

    -

    Some characters, like æ, & and similar should be handled properly.

    -

    HTML

    -

    Does anyone actually like the fact that you can embed HTML in markdown?

    -, as long as it doesn't contain any attributes. If you -have weird ordering on your tags, it won't work either. It does support nested -tags, however. And with the rehype-raw plugin, it can now properly handle HTML! Which is pretty sweet.

    -

    -

    Cool, eh?

    \ No newline at end of file diff --git a/test/fixtures/runthrough.md b/test/fixtures/runthrough.md deleted file mode 100644 index c643ab4c..00000000 --- a/test/fixtures/runthrough.md +++ /dev/null @@ -1,171 +0,0 @@ -# h1 Heading -## h2 Heading -### h3 Heading -#### h4 Heading -##### h5 Heading -###### h6 Heading - - -## Horizontal Rules - -___ - ---- - -*** - - -## Emphasis - -**This is bold text** - -__This is bold text__ - -*This is italic text* - -_This is italic text_ - -~~Strikethrough~~ - - -## Blockquotes - - -> Blockquotes can also be nested... ->> ...by using additional greater-than signs right next to each other... -> > > ...or with spaces between arrows. - - -## Lists - -Unordered - -+ Create a list by starting a line with `+`, `-`, or `*` -+ Sub-lists are made by indenting 2 spaces: - - Marker character change forces new list start: - * Ac tristique libero volutpat at - + Facilisis in pretium nisl aliquet - - Nulla volutpat aliquam velit -+ Very easy! - -Ordered - -1. Lorem ipsum dolor sit amet -2. Consectetur adipiscing elit -3. Integer molestie lorem at massa - -Or: - -1. You can use sequential numbers... -1. ...or keep all the numbers as `1.` - -Start numbering with offset: - -57. foo -1. bar - -Loose lists? - -- foo - -- bar - - -## Code - -Inline `code` - -Indented code - - // Some comments - line 1 of code - line 2 of code - line 3 of code - - -Block code "fences" - -``` -Sample text here... -``` - -Syntax highlighting - -``` js -var foo = function (bar) { - return bar++; -}; - -console.log(foo(5)); -``` - -## Tables - -| Tag | Use | -| ------ | ----------- | -| p | Paragraph | -| table | Table | -| em | Emphasis | - -Left/right aligned columns - -| Project | Stars | -| :------ | -----------:| -| React | 80 759 | -| Vue.js | 73 322 | -| sse-channel | 50 | - - -## Links - -[Espen.Codes](https://espen.codes/) - -[Sanity](https://www.sanity.io/ "Sanity, the headless CMS and PaaS") - -Autoconverted link https://github.com/remarkjs/react-markdown - -[Link references][React] - -[React]: https://reactjs.org "React, A JavaScript library for building user interfaces" - - -## Images - -![React Markdown](https://espen.codes/assets/projects/react-markdown/320x180.png) -![Mead](https://espen.codes/assets/projects/mead/320x180.png "Mead, on-the-fly image transformer") - -Like links, Images also have a footnote style syntax - -![Alt text][someref] - -With a reference later in the document defining the URL location: - -[someref]: https://public.sanity.io/modell_@2x.png "Headless CMS" - -## Hard breaks - -Yeah, hard breaks\ -can be useful too. - -## HTML entities - -Some characters, like æ, & and similar should be handled properly. - -## HTML - -Does anyone actually like the fact that you can embed HTML in markdown? - -