From 36e49163a8ff927c009da6e4a61c840416df1299 Mon Sep 17 00:00:00 2001 From: peolic <66393006+peolic@users.noreply.github.com> Date: Mon, 17 Jan 2022 19:59:40 +0200 Subject: [PATCH 001/125] Add support for passing options to `remark-rehype` Closes GH-669. Reviewed-by: Titus Wormer --- lib/react-markdown.js | 6 +++++- package.json | 2 +- readme.md | 4 ++++ test/test.jsx | 19 +++++++++++++++++++ 4 files changed, 29 insertions(+), 2 deletions(-) diff --git a/lib/react-markdown.js b/lib/react-markdown.js index 48ba44e3..58ef8169 100644 --- a/lib/react-markdown.js +++ b/lib/react-markdown.js @@ -13,6 +13,7 @@ * @property {PluggableList} [plugins=[]] **deprecated**: use `remarkPlugins` instead * @property {PluggableList} [remarkPlugins=[]] * @property {PluggableList} [rehypePlugins=[]] + * @property {import('remark-rehype').Options} [remarkRehypeOptions={}] * * @typedef LayoutOptions * @property {string} [className] @@ -87,7 +88,10 @@ export function ReactMarkdown(options) { .use(remarkParse) // TODO: deprecate `plugins` in v8.0.0. .use(options.remarkPlugins || options.plugins || []) - .use(remarkRehype, {allowDangerousHtml: true}) + .use(remarkRehype, { + ...options.remarkRehypeOptions, + allowDangerousHtml: true + }) .use(options.rehypePlugins || []) .use(rehypeFilter, options) diff --git a/package.json b/package.json index 67bce621..811d17af 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "property-information": "^6.0.0", "react-is": "^17.0.0", "remark-parse": "^10.0.0", - "remark-rehype": "^9.0.0", + "remark-rehype": "^10.0.0", "space-separated-tokens": "^2.0.0", "style-to-object": "^0.3.0", "unified": "^10.0.0", diff --git a/readme.md b/readme.md index 926e9369..9cf21150 100644 --- a/readme.md +++ b/readme.md @@ -175,6 +175,8 @@ The default export is `ReactMarkdown`. list of [remark plugins][remark-plugins] to use * `rehypePlugins` (`Array`, default: `[]`)\ list of [rehype plugins][rehype-plugins] to use +* `remarkRehypeOptions` (`Object?`, default: `undefined`)\ + options to pass through to [`remark-rehype`][remark-rehype] * `className` (`string?`)\ wrap the markdown in a `div` with this class name * `skipHtml` (`boolean`, default: `false`)\ @@ -733,6 +735,8 @@ abide by its terms. [rehype-plugins]: https://github.com/rehypejs/rehype/blob/main/doc/plugins.md#list-of-plugins +[remark-rehype]: https://github.com/remarkjs/remark-rehype + [awesome-remark]: https://github.com/remarkjs/awesome-remark [awesome-rehype]: https://github.com/rehypejs/awesome-rehype diff --git a/test/test.jsx b/test/test.jsx index 384c0aa1..2b823a0d 100644 --- a/test/test.jsx +++ b/test/test.jsx @@ -874,6 +874,25 @@ test('should render image references', () => { ) }) +test('should render footnote with custom options', () => { + const input = [ + 'This is a statement[^1] with a citation.', + '', + '[^1]: This is a footnote for the citation.' + ].join('\n') + + assert.equal( + asHtml( + + ), + '

This is a statement1 with a citation.

\n

Footnotes

\n
    \n
  1. \n

    This is a footnote for the citation.

    \n
  2. \n
\n
' + ) +}) + test('should support definitions with funky keys', () => { const input = '[][__proto__] and [][constructor]\n\n[__proto__]: a\n[constructor]: b' From bfa57ec39eb17ac3e7fb947524b76519f02b45f6 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Mon, 17 Jan 2022 19:02:04 +0100 Subject: [PATCH 002/125] Update dev-dependencies --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 811d17af..c2c38323 100644 --- a/package.json +++ b/package.json @@ -104,7 +104,7 @@ "@types/react-is": "^17.0.0", "c8": "^7.0.0", "esbuild": "^0.14.0", - "eslint-config-xo-react": "^0.25.0", + "eslint-config-xo-react": "^0.26.0", "eslint-plugin-es": "^4.0.0", "eslint-plugin-react": "^7.0.0", "eslint-plugin-react-hooks": "^4.0.0", From cd845c9f38e0280b67fa33cc8e1fcf9d2cd0cc72 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Mon, 17 Jan 2022 19:13:34 +0100 Subject: [PATCH 003/125] Remove deprecated `plugins` option MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This removes the already deprecated option `plugins`. It’s renamed to `remarkPlugins`. --- changelog.md | 7 +++++++ lib/react-markdown.js | 5 ++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/changelog.md b/changelog.md index 06335805..e4ab4b1d 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,13 @@ All notable changes will be documented in this file. +## 8.0.0 - 2022-01-17 + +### Change `plugins` to `remarkPlugins` + +This removes the already deprecated option `plugins`. +It’s renamed to `remarkPlugins`. + ## 7.0.1 - 2021-08-26 * [`ec387c2`](https://github.com/remarkjs/react-markdown/commit/ec387c2) diff --git a/lib/react-markdown.js b/lib/react-markdown.js index 58ef8169..6dbb085c 100644 --- a/lib/react-markdown.js +++ b/lib/react-markdown.js @@ -10,7 +10,6 @@ * @property {string} children * * @typedef PluginOptions - * @property {PluggableList} [plugins=[]] **deprecated**: use `remarkPlugins` instead * @property {PluggableList} [remarkPlugins=[]] * @property {PluggableList} [rehypePlugins=[]] * @property {import('remark-rehype').Options} [remarkRehypeOptions={}] @@ -42,6 +41,7 @@ const changelog = /** @type {Record} */ const deprecated = { + plugins: {to: 'plugins', id: 'change-plugins-to-remarkplugins'}, renderers: {to: 'components', id: 'change-renderers-to-components'}, astPlugins: {id: 'remove-buggy-html-in-markdown-parser'}, allowDangerousHtml: {id: 'remove-buggy-html-in-markdown-parser'}, @@ -86,8 +86,7 @@ export function ReactMarkdown(options) { const processor = unified() .use(remarkParse) - // TODO: deprecate `plugins` in v8.0.0. - .use(options.remarkPlugins || options.plugins || []) + .use(options.remarkPlugins || []) .use(remarkRehype, { ...options.remarkRehypeOptions, allowDangerousHtml: true From bd8e53b4969a0a6f5cfd0fb1d4fe5d97d2cfa630 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Mon, 17 Jan 2022 19:20:47 +0100 Subject: [PATCH 004/125] 8.0.0 --- changelog.md | 41 ++++++++++++++++++++++++++++++++++++----- package.json | 2 +- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/changelog.md b/changelog.md index e4ab4b1d..e9b29ef5 100644 --- a/changelog.md +++ b/changelog.md @@ -1,13 +1,44 @@ -# Change Log +# Changelog All notable changes will be documented in this file. ## 8.0.0 - 2022-01-17 -### Change `plugins` to `remarkPlugins` - -This removes the already deprecated option `plugins`. -It’s renamed to `remarkPlugins`. + + +* [`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 diff --git a/package.json b/package.json index c2c38323..515839c2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-markdown", - "version": "7.1.2", + "version": "8.0.0", "description": "React component to render markdown", "license": "MIT", "keywords": [ From c23ecf6c4f3c20d7b98ee7d851c7acb8304aaae8 Mon Sep 17 00:00:00 2001 From: Nathan Bierema Date: Mon, 14 Mar 2022 06:12:40 -0400 Subject: [PATCH 005/125] Add missing dependency for types Closes GH-675. Reviewed-by: Titus Wormer --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 515839c2..c788f9d8 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ ], "dependencies": { "@types/hast": "^2.0.0", + "@types/prop-types": "^15.0.0", "@types/unist": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^2.0.0", From e13149c230a5eec661a0501e5c56899b86dc1498 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Mon, 14 Mar 2022 11:14:48 +0100 Subject: [PATCH 006/125] Update dev-dependencies --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c788f9d8..2c9c34ba 100644 --- a/package.json +++ b/package.json @@ -122,7 +122,7 @@ "type-coverage": "^2.0.0", "typescript": "~4.4.0", "uvu": "^0.5.0", - "xo": "^0.47.0" + "xo": "^0.48.0" }, "scripts": { "prepack": "npm run build && npm run format", From 85feb966f65dcc461a32486ead235c59d4ee25ab Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Mon, 14 Mar 2022 11:16:10 +0100 Subject: [PATCH 007/125] 8.0.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2c9c34ba..52cec11f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-markdown", - "version": "8.0.0", + "version": "8.0.1", "description": "React component to render markdown", "license": "MIT", "keywords": [ From 704c3c61a99b23af01fa36e682eb8c7825d0b2dc Mon Sep 17 00:00:00 2001 From: Nathan Bierema Date: Thu, 31 Mar 2022 10:12:30 -0400 Subject: [PATCH 008/125] Fix TypeScript bug by adding workaround Related-to: microsoft/TypeScript#48242. Closes GH-676. --- lib/react-markdown.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/react-markdown.js b/lib/react-markdown.js index 6dbb085c..9291e7f3 100644 --- a/lib/react-markdown.js +++ b/lib/react-markdown.js @@ -12,7 +12,7 @@ * @typedef PluginOptions * @property {PluggableList} [remarkPlugins=[]] * @property {PluggableList} [rehypePlugins=[]] - * @property {import('remark-rehype').Options} [remarkRehypeOptions={}] + * @property {import('remark-rehype').Options | undefined} [remarkRehypeOptions={}] * * @typedef LayoutOptions * @property {string} [className] From 6e87778f9f37ade237cd340fd60a50e7f28b99ad Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Thu, 31 Mar 2022 16:18:22 +0200 Subject: [PATCH 009/125] Update dev-dependencies --- package.json | 6 +++--- test/test.jsx | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 52cec11f..42448545 100644 --- a/package.json +++ b/package.json @@ -105,14 +105,14 @@ "@types/react-is": "^17.0.0", "c8": "^7.0.0", "esbuild": "^0.14.0", - "eslint-config-xo-react": "^0.26.0", + "eslint-config-xo-react": "^0.27.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", + "react": "^18.0.0", + "react-dom": "^18.0.0", "rehype-raw": "^6.0.0", "remark-cli": "^10.0.0", "remark-gfm": "^3.0.0", diff --git a/test/test.jsx b/test/test.jsx index 2b823a0d..e904b8d1 100644 --- a/test/test.jsx +++ b/test/test.jsx @@ -17,7 +17,7 @@ import gfm from 'remark-gfm' import {visit} from 'unist-util-visit' import raw from 'rehype-raw' import toc from 'remark-toc' -import ReactDom from 'react-dom/server.js' +import ReactDom from 'react-dom/server' import Markdown from '../index.js' const own = {}.hasOwnProperty @@ -636,7 +636,7 @@ test('should pass `index: number`, `ordered: boolean`, `checked: boolean | null` /> ) const expected = - '
    \n
  • a
  • \n
  • b
  • \n
  • c
  • \n
' + '
    \n
  • a
  • \n
  • b
  • \n
  • c
  • \n
' assert.equal(actual, expected) }) @@ -1134,7 +1134,7 @@ test('supports checkbox lists', () => { const actual = asHtml() assert.equal( actual, - '
    \n
  • Foo
  • \n
  • Bar
  • \n
\n
\n
    \n
  • Foo
  • \n
  • Bar
  • \n
' + '
    \n
  • Foo
  • \n
  • Bar
  • \n
\n
\n
    \n
  • Foo
  • \n
  • Bar
  • \n
' ) }) From 27122277cc55dccb7fa22f77c26cce2fdbf622f0 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Thu, 31 Mar 2022 16:18:26 +0200 Subject: [PATCH 010/125] Update `react-is` --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 42448545..f8c5f8ae 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "hast-util-whitespace": "^2.0.0", "prop-types": "^15.0.0", "property-information": "^6.0.0", - "react-is": "^17.0.0", + "react-is": "^18.0.0", "remark-parse": "^10.0.0", "remark-rehype": "^10.0.0", "space-separated-tokens": "^2.0.0", From b0ff51d7cbe2fb5fe2d01dd7b171739a95f098d4 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Thu, 31 Mar 2022 16:20:35 +0200 Subject: [PATCH 011/125] 8.0.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f8c5f8ae..88e4bbc2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-markdown", - "version": "8.0.1", + "version": "8.0.2", "description": "React component to render markdown", "license": "MIT", "keywords": [ From 8143c12d24000d8a8a016d0d41f05709298bafea Mon Sep 17 00:00:00 2001 From: Titus Date: Sat, 2 Apr 2022 11:46:56 +0200 Subject: [PATCH 012/125] Replace skypack w/ esm.sh --- readme.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/readme.md b/readme.md index 9cf21150..6223cfc9 100644 --- a/readme.md +++ b/readme.md @@ -93,17 +93,17 @@ In Node.js (version 12.20+, 14.14+, or 16.0+), install with [npm][]: 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 ReactMarkdown from 'https://esm.sh/react-markdown@7' ``` -In browsers with [Skypack][]: +In browsers with [`esm.sh`][esmsh]: ```html ``` @@ -699,7 +699,7 @@ abide by its terms. [npm]: https://docs.npmjs.com/cli/install -[skypack]: https://www.skypack.dev +[esmsh]: https://esm.sh [health]: https://github.com/remarkjs/.github From a2fb833f1dc34ec73273260340ba7d5029699672 Mon Sep 17 00:00:00 2001 From: Nick Mitchell Date: Wed, 20 Apr 2022 08:56:35 -0400 Subject: [PATCH 013/125] Fix prop types of plugins Closes GH-683. Related-to: remarkjs/remark#945. Reviewed-by: Christian Murphy Reviewed-by: Titus Wormer --- lib/react-markdown.js | 28 ++++++++++++++++++++++++-- test/test.jsx | 46 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/lib/react-markdown.js b/lib/react-markdown.js index 9291e7f3..f01aa068 100644 --- a/lib/react-markdown.js +++ b/lib/react-markdown.js @@ -141,14 +141,38 @@ ReactMarkdown.propTypes = { PropTypes.oneOfType([ PropTypes.object, PropTypes.func, - PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.object, PropTypes.func])) + PropTypes.arrayOf( + PropTypes.oneOfType([ + PropTypes.bool, + PropTypes.string, + PropTypes.object, + PropTypes.func, + PropTypes.arrayOf( + // prettier-ignore + // type-coverage:ignore-next-line + PropTypes.any + ) + ]) + ) ]) ), rehypePlugins: PropTypes.arrayOf( PropTypes.oneOfType([ PropTypes.object, PropTypes.func, - PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.object, PropTypes.func])) + PropTypes.arrayOf( + PropTypes.oneOfType([ + PropTypes.bool, + PropTypes.string, + PropTypes.object, + PropTypes.func, + PropTypes.arrayOf( + // prettier-ignore + // type-coverage:ignore-next-line + PropTypes.any + ) + ]) + ) ]) ), // Transform options: diff --git a/test/test.jsx b/test/test.jsx index e904b8d1..ac8af80c 100644 --- a/test/test.jsx +++ b/test/test.jsx @@ -1424,4 +1424,50 @@ test('should crash on a plugin replacing `root`', () => { }, /Expected a `root` node/) }) +test('should support remark plugins with array parameter', async () => { + const error = console.error + /** @type {string} */ + let message = '' + + console.error = (/** @type {string} */ d) => { + message = d + } + + const input = 'a' + /** @type {import('unified').Plugin>, Root>} */ + const plugin = () => () => {} + + const actual = asHtml( + + ) + const expected = '

a

' + assert.equal(actual, expected) + + assert.not.match(message, /Warning: Failed/, 'Prop types should be valid') + console.error = error +}) + +test('should support rehype plugins with array parameter', async () => { + const error = console.error + /** @type {string} */ + let message = '' + + console.error = (/** @type {string} */ d) => { + message = d + } + + const input = 'a' + /** @type {import('unified').Plugin>, Root>} */ + const plugin = () => () => {} + + const actual = asHtml( + + ) + const expected = '

a

' + assert.equal(actual, expected) + + assert.not.match(message, /Warning: Failed/, 'Prop types should be valid') + console.error = error +}) + test.run() From 9f1e96587836cf1149870869f20c7aedd1aca55e Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Wed, 20 Apr 2022 14:58:48 +0200 Subject: [PATCH 014/125] Update dev-dependencies --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 88e4bbc2..31284e4d 100644 --- a/package.json +++ b/package.json @@ -100,8 +100,8 @@ "react": ">=16" }, "devDependencies": { - "@types/react": "^17.0.0", - "@types/react-dom": "^17.0.0", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", "@types/react-is": "^17.0.0", "c8": "^7.0.0", "esbuild": "^0.14.0", From 03327fb02062c38494cf4189fa013db4059c3f39 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Wed, 20 Apr 2022 15:00:55 +0200 Subject: [PATCH 015/125] 8.0.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 31284e4d..6573f0fb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-markdown", - "version": "8.0.2", + "version": "8.0.3", "description": "React component to render markdown", "license": "MIT", "keywords": [ From cfe075b913cb5a8aee43c7b8360d22d8554ff394 Mon Sep 17 00:00:00 2001 From: Carlos Ballena Date: Sun, 5 Jun 2022 06:49:29 -0400 Subject: [PATCH 016/125] Add clarification of `alt` on `img` in docs Reviewed-by: Titus Wormer Closes GH-692. --- readme.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 6223cfc9..caceef38 100644 --- a/readme.md +++ b/readme.md @@ -571,7 +571,8 @@ 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. +get `href` (and `title`) props, and `img` (image) an `src`, `alt` and `title`, +etc. There are some extra props passed. * `code` From 75ec1f8a4f64e12ba2f0c1ad95101a1ac5426bb1 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Tue, 4 Oct 2022 18:16:33 +0200 Subject: [PATCH 017/125] Update dev-dependencies --- lib/ast-to-react.js | 2 +- lib/complex-types.ts | 2 +- package.json | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/ast-to-react.js b/lib/ast-to-react.js index 31a06a19..2134381a 100644 --- a/lib/ast-to-react.js +++ b/lib/ast-to-react.js @@ -444,6 +444,6 @@ function flattenPosition(pos) { ':', pos.end.column ] - .map((d) => String(d)) + .map(String) .join('') } diff --git a/lib/complex-types.ts b/lib/complex-types.ts index c639e7ae..5319a88b 100644 --- a/lib/complex-types.ts +++ b/lib/complex-types.ts @@ -4,7 +4,7 @@ import type {Element} from 'hast' /* File for types which are not handled correctly in JSDoc mode */ -export interface ReactMarkdownProps { +export type ReactMarkdownProps = { node: Element children: ReactNode[] /** diff --git a/package.json b/package.json index 6573f0fb..5ddd578b 100644 --- a/package.json +++ b/package.json @@ -104,7 +104,7 @@ "@types/react-dom": "^18.0.0", "@types/react-is": "^17.0.0", "c8": "^7.0.0", - "esbuild": "^0.14.0", + "esbuild": "^0.15.0", "eslint-config-xo-react": "^0.27.0", "eslint-plugin-es": "^4.0.0", "eslint-plugin-react": "^7.0.0", @@ -114,15 +114,15 @@ "react": "^18.0.0", "react-dom": "^18.0.0", "rehype-raw": "^6.0.0", - "remark-cli": "^10.0.0", + "remark-cli": "^11.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", + "typescript": "^4.0.0", "uvu": "^0.5.0", - "xo": "^0.48.0" + "xo": "^0.52.0" }, "scripts": { "prepack": "npm run build && npm run format", @@ -195,7 +195,7 @@ "test/**/*.jsx" ], "rules": { - "node/file-extension-in-import": "off", + "n/file-extension-in-import": "off", "react/no-children-prop": "off", "react/prop-types": "off" } From 2598e279323fd6f8cf782766a052c2060c3209d4 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Tue, 4 Oct 2022 18:17:23 +0200 Subject: [PATCH 018/125] Fix support for Node loaders --- test/loader.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/loader.js b/test/loader.js index 66df2219..4ff58e8f 100644 --- a/test/loader.js +++ b/test/loader.js @@ -42,7 +42,7 @@ export function createLoader() { } } - return {format: 'module', source: code} + return {format: 'module', source: code, shortCircuit: true} } // Pre version 17. From 48e52c520f13fdab330ffe16ab25a812e3edbee2 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Tue, 4 Oct 2022 18:19:15 +0200 Subject: [PATCH 019/125] Update Node in Actions --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fe284ad1..69924a47 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,5 +17,5 @@ jobs: strategy: matrix: node: - - lts/erbium + - lts/fermium - node From 85bb1e19d6e87c59e6bd864ebda5b1f04bc01937 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Tue, 4 Oct 2022 18:25:47 +0200 Subject: [PATCH 020/125] Fix fixture for reorder prop --- test/test.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test.jsx b/test/test.jsx index ac8af80c..4e1b9341 100644 --- a/test/test.jsx +++ b/test/test.jsx @@ -889,7 +889,7 @@ test('should render footnote with custom options', () => { remarkRehypeOptions={{clobberPrefix: 'main-'}} /> ), - '

This is a statement1 with a citation.

\n

Footnotes

\n
    \n
  1. \n

    This is a footnote for the citation.

    \n
  2. \n
\n
' + '

This is a statement1 with a citation.

\n

Footnotes

\n
    \n
  1. \n

    This is a footnote for the citation.

    \n
  2. \n
\n
' ) }) From a80dfdee2703d84ac2120d28b0e4998a5b417c85 Mon Sep 17 00:00:00 2001 From: Christian Murphy Date: Thu, 17 Nov 2022 08:13:43 -0700 Subject: [PATCH 021/125] Update actions Reviewed-by: Remco Haszing Reviewed-by: Titus Wormer Closes GH-712. --- .github/workflows/main.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 69924a47..89dc06c8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,13 +7,13 @@ jobs: name: ${{matrix.node}} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: dcodeIO/setup-node-nvm@master + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 with: node-version: ${{matrix.node}} - run: npm install - run: npm test - - uses: codecov/codecov-action@v1 + - uses: codecov/codecov-action@v3 strategy: matrix: node: From 9b2044012ff1592a78be6b944fd99d34e155b0a8 Mon Sep 17 00:00:00 2001 From: Lucas Rosa Date: Thu, 1 Dec 2022 04:46:04 -0300 Subject: [PATCH 022/125] Fix type of `td`, `th` props Closes GH-713. Closes GH-714. Reviewed-by: Christian Murphy Reviewed-by: Titus Wormer --- lib/ast-to-react.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/ast-to-react.js b/lib/ast-to-react.js index 2134381a..afa847e6 100644 --- a/lib/ast-to-react.js +++ b/lib/ast-to-react.js @@ -58,7 +58,8 @@ * @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<'td'> & ReactMarkdownProps & {style?: Record, isHeader: false}} TableDataCellProps + * @typedef {ComponentPropsWithoutRef<'th'> & ReactMarkdownProps & {style?: Record, isHeader: true}} TableHeaderCellProps * @typedef {ComponentPropsWithoutRef<'tr'> & ReactMarkdownProps & {isHeader: boolean}} TableRowProps * @typedef {ComponentPropsWithoutRef<'ul'> & ReactMarkdownProps & {depth: number, ordered: false}} UnorderedListProps * @@ -66,7 +67,8 @@ * @typedef {ComponentType} HeadingComponent * @typedef {ComponentType} LiComponent * @typedef {ComponentType} OrderedListComponent - * @typedef {ComponentType} TableCellComponent + * @typedef {ComponentType} TableDataCellComponent + * @typedef {ComponentType} TableHeaderCellComponent * @typedef {ComponentType} TableRowComponent * @typedef {ComponentType} UnorderedListComponent * @@ -80,8 +82,8 @@ * @property {HeadingComponent|ReactMarkdownNames} h6 * @property {LiComponent|ReactMarkdownNames} li * @property {OrderedListComponent|ReactMarkdownNames} ol - * @property {TableCellComponent|ReactMarkdownNames} td - * @property {TableCellComponent|ReactMarkdownNames} th + * @property {TableDataCellComponent|ReactMarkdownNames} td + * @property {TableHeaderCellComponent|ReactMarkdownNames} th * @property {TableRowComponent|ReactMarkdownNames} tr * @property {UnorderedListComponent|ReactMarkdownNames} ul * From 1009b1bbabea833f0b0eae14a77585958d953d1c Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Thu, 1 Dec 2022 11:53:35 +0400 Subject: [PATCH 023/125] Update dev-dependency --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5ddd578b..f2fee6a8 100644 --- a/package.json +++ b/package.json @@ -122,7 +122,7 @@ "type-coverage": "^2.0.0", "typescript": "^4.0.0", "uvu": "^0.5.0", - "xo": "^0.52.0" + "xo": "^0.53.0" }, "scripts": { "prepack": "npm run build && npm run format", From 25d651c69e79220355575828f25b90deeba851de Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Thu, 1 Dec 2022 11:56:46 +0400 Subject: [PATCH 024/125] 8.0.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f2fee6a8..0fb2f14d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-markdown", - "version": "8.0.3", + "version": "8.0.4", "description": "React component to render markdown", "license": "MIT", "keywords": [ From 72ee489a897cc60a07a6e15d3ee86dde17afc307 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Tue, 13 Dec 2022 10:35:11 +0400 Subject: [PATCH 025/125] Update dev-dependencies --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 0fb2f14d..d3a95b78 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,7 @@ "remark-parse": "^10.0.0", "remark-rehype": "^10.0.0", "space-separated-tokens": "^2.0.0", - "style-to-object": "^0.3.0", + "style-to-object": "^0.4.0", "unified": "^10.0.0", "unist-util-visit": "^4.0.0", "vfile": "^5.0.0" @@ -104,7 +104,7 @@ "@types/react-dom": "^18.0.0", "@types/react-is": "^17.0.0", "c8": "^7.0.0", - "esbuild": "^0.15.0", + "esbuild": "^0.16.0", "eslint-config-xo-react": "^0.27.0", "eslint-plugin-es": "^4.0.0", "eslint-plugin-react": "^7.0.0", From 402fea3e9ebcb6f8f181f67085add7a18306ef53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Esp=C3=ADn?= Date: Thu, 15 Dec 2022 07:08:34 +0100 Subject: [PATCH 026/125] Fix typo in `plugins` deprecation message Closes GH-719. --- lib/react-markdown.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/react-markdown.js b/lib/react-markdown.js index f01aa068..d0760619 100644 --- a/lib/react-markdown.js +++ b/lib/react-markdown.js @@ -41,7 +41,7 @@ const changelog = /** @type {Record} */ const deprecated = { - plugins: {to: 'plugins', id: 'change-plugins-to-remarkplugins'}, + plugins: {to: 'remarkPlugins', id: 'change-plugins-to-remarkplugins'}, renderers: {to: 'components', id: 'change-renderers-to-components'}, astPlugins: {id: 'remove-buggy-html-in-markdown-parser'}, allowDangerousHtml: {id: 'remove-buggy-html-in-markdown-parser'}, From 4f98f73cca811dc9fb98ee0adce44da981a57b9f Mon Sep 17 00:00:00 2001 From: Marcelo Reyna Date: Sun, 11 Dec 2022 14:02:29 -0800 Subject: [PATCH 027/125] Remove deprecated and unneeded `defaultProps` Closes GH-718. --- lib/ast-to-react.js | 9 +++++++-- lib/react-markdown.js | 3 --- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/ast-to-react.js b/lib/ast-to-react.js index afa847e6..975cbc40 100644 --- a/lib/ast-to-react.js +++ b/lib/ast-to-react.js @@ -107,6 +107,7 @@ 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' +import {uriTransformer} from './uri-transformer.js' const own = {}.hasOwnProperty @@ -162,6 +163,10 @@ export function childrenToReact(context, node) { */ function toReact(context, node, index, parent) { const options = context.options + const transform = + options.transformLinkUri === undefined + ? uriTransformer + : options.transformLinkUri const parentSchema = context.schema /** @type {ReactMarkdownNames} */ // @ts-expect-error assume a known HTML/SVG element. @@ -234,8 +239,8 @@ function toReact(context, node, index, parent) { : options.linkTarget } - if (name === 'a' && options.transformLinkUri) { - properties.href = options.transformLinkUri( + if (name === 'a' && transform) { + properties.href = transform( String(properties.href || ''), node.children, typeof properties.title === 'string' ? properties.title : null diff --git a/lib/react-markdown.js b/lib/react-markdown.js index d0760619..92a032c7 100644 --- a/lib/react-markdown.js +++ b/lib/react-markdown.js @@ -32,7 +32,6 @@ 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 @@ -124,8 +123,6 @@ export function ReactMarkdown(options) { return result } -ReactMarkdown.defaultProps = {transformLinkUri: uriTransformer} - ReactMarkdown.propTypes = { // Core options: children: PropTypes.string, From e63707f57cdc841f66c130ae6aeb60878e24e212 Mon Sep 17 00:00:00 2001 From: Christian Murphy Date: Thu, 12 Jan 2023 02:14:12 -0700 Subject: [PATCH 028/125] Add `ignore-scripts` to `.npmrc` Closes GH-721. Reviewed-by: Titus Wormer --- .npmrc | 1 + 1 file changed, 1 insertion(+) diff --git a/.npmrc b/.npmrc index 43c97e71..9951b11b 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,2 @@ package-lock=false +ignore-scripts=true From d640d401a8b556f1713f8575c7f1eacd9abc4c3c Mon Sep 17 00:00:00 2001 From: Christian Murphy Date: Tue, 17 Jan 2023 11:27:36 -0700 Subject: [PATCH 029/125] Update to use `node16` module resolution in `tsconfig.json` Reviewed-by: Remco Haszing Reviewed-by: Titus Wormer Closes GH-723. --- lib/ast-to-react.js | 4 ++-- test/test.jsx | 2 +- tsconfig.json | 11 +++-------- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/lib/ast-to-react.js b/lib/ast-to-react.js index 975cbc40..da2cbe8f 100644 --- a/lib/ast-to-react.js +++ b/lib/ast-to-react.js @@ -19,7 +19,7 @@ * @typedef {import('hast').DocType} Doctype * @typedef {import('property-information').Info} Info * @typedef {import('property-information').Schema} Schema - * @typedef {import('./complex-types').ReactMarkdownProps} ReactMarkdownProps + * @typedef {import('./complex-types.js').ReactMarkdownProps} ReactMarkdownProps * * @typedef Raw * @property {'raw'} type @@ -87,7 +87,7 @@ * @property {TableRowComponent|ReactMarkdownNames} tr * @property {UnorderedListComponent|ReactMarkdownNames} ul * - * @typedef {Partial & SpecialComponents>} Components + * @typedef {Partial & SpecialComponents>} Components * * @typedef Options * @property {boolean} [sourcePos=false] diff --git a/test/test.jsx b/test/test.jsx index 4e1b9341..8b06635b 100644 --- a/test/test.jsx +++ b/test/test.jsx @@ -1158,7 +1158,7 @@ test('should pass index of a node under its parent to components if `includeElem test('should be able to render components with forwardRef in HOC', () => { /** * @typedef {import('react').Ref} Ref - * @typedef {JSX.IntrinsicElements['a'] & import('../lib/ast-to-react').ReactMarkdownProps} Props + * @typedef {JSX.IntrinsicElements['a'] & import('../lib/ast-to-react.js').ReactMarkdownProps} Props */ /** diff --git a/tsconfig.json b/tsconfig.json index 70285598..73fae2a1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,19 +2,14 @@ "include": ["lib/**/*.js", "test/**/*.jsx", "test/**/*.js", "index.js"], "exclude": ["**/*.min.js"], "compilerOptions": { - "target": "ES2020", - "lib": ["ES2020", "DOM"], - "module": "ES2020", - "moduleResolution": "node", + "target": "es2021", + "lib": ["es2020", "dom"], + "module": "node16", "jsx": "react", - "allowJs": true, "checkJs": true, "declaration": true, "emitDeclarationOnly": true, - "allowSyntheticDefaultImports": true, "skipLibCheck": true, - "noImplicitAny": false, - "noImplicitThis": true, "strict": true } } From 560d6d0bf3523f39ff704e58743b03eea3182fa8 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Tue, 17 Jan 2023 19:30:28 +0100 Subject: [PATCH 030/125] Update dev-dependencies --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d3a95b78..6d045a9a 100644 --- a/package.json +++ b/package.json @@ -104,7 +104,7 @@ "@types/react-dom": "^18.0.0", "@types/react-is": "^17.0.0", "c8": "^7.0.0", - "esbuild": "^0.16.0", + "esbuild": "^0.17.0", "eslint-config-xo-react": "^0.27.0", "eslint-plugin-es": "^4.0.0", "eslint-plugin-react": "^7.0.0", From 998ba08eff7ffe93d5f00fe528025571f404b1af Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Tue, 17 Jan 2023 19:35:00 +0100 Subject: [PATCH 031/125] Update `tsconfig.json` --- package.json | 6 +++--- tsconfig.json | 17 ++++++++++------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 6d045a9a..18c6998b 100644 --- a/package.json +++ b/package.json @@ -126,10 +126,10 @@ }, "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;})));\"", + "build": "tsc --build --clean && tsc --build && 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-api": "node --no-warnings --experimental-loader=./test/loader.js --conditions development ./node_modules/.bin/uvu test \"\\.jsx$\"", + "test-coverage": "c8 --check-coverage --100 --reporter lcov npm run test-api", "test": "npm run build && npm run format && npm run test-coverage" }, "remarkConfig": { diff --git a/tsconfig.json b/tsconfig.json index 73fae2a1..8583ce16 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,15 +1,18 @@ { - "include": ["lib/**/*.js", "test/**/*.jsx", "test/**/*.js", "index.js"], - "exclude": ["**/*.min.js"], + "include": ["**/*.js", "**/*.jsx"], + "exclude": ["coverage/", "node_modules/", "**/*.min.js"], "compilerOptions": { - "target": "es2021", - "lib": ["es2020", "dom"], - "module": "node16", - "jsx": "react", "checkJs": true, "declaration": true, "emitDeclarationOnly": true, + "exactOptionalPropertyTypes": true, + "forceConsistentCasingInFileNames": true, + "lib": ["es2020"], + "module": "node16", + "newLine": "lf", "skipLibCheck": true, - "strict": true + "strict": true, + "target": "es2021", + "jsx": "preserve" } } From 69583d23723e20fa5f4e3e5fa9d727310e753ec4 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Tue, 17 Jan 2023 19:46:07 +0100 Subject: [PATCH 032/125] Remove `path`, use `fs/promises` in tests --- test/loader.js | 38 ++++++++++++++++++++------------------ test/test.jsx | 19 ++++++------------- 2 files changed, 26 insertions(+), 31 deletions(-) diff --git a/test/loader.js b/test/loader.js index 4ff58e8f..28915afc 100644 --- a/test/loader.js +++ b/test/loader.js @@ -1,6 +1,5 @@ -import {promises as fs} from 'node:fs' -import path from 'node:path' -import {URL, fileURLToPath} from 'node:url' +import fs from 'node:fs/promises' +import {fileURLToPath} from 'node:url' import {transform, transformSync} from 'esbuild' const {load, getFormat, transformSource} = createLoader() @@ -15,20 +14,19 @@ export function createLoader() { // Node version 17. /** - * @param {string} url + * @param {string} href * @param {unknown} context * @param {Function} defaultLoad */ - async function load(url, context, defaultLoad) { - if (path.extname(url) !== '.jsx') { - return defaultLoad(url, context, defaultLoad) - } + async function load(href, context, defaultLoad) { + const url = new URL(href) - const fp = fileURLToPath(new URL(url)) - const value = await fs.readFile(fp) + if (!url.pathname.endsWith('.jsx')) { + return defaultLoad(href, context, defaultLoad) + } - const {code, warnings} = await transform(String(value), { - sourcefile: fp, + const {code, warnings} = await transform(String(await fs.readFile(url)), { + sourcefile: fileURLToPath(url), sourcemap: 'both', loader: 'jsx', target: 'esnext', @@ -47,14 +45,16 @@ export function createLoader() { // Pre version 17. /** - * @param {string} url + * @param {string} href * @param {unknown} context * @param {Function} defaultGetFormat */ - function getFormat(url, context, defaultGetFormat) { - return path.extname(url) === '.jsx' + function getFormat(href, context, defaultGetFormat) { + const url = new URL(href) + + return url.pathname.endsWith('.jsx') ? {format: 'module'} - : defaultGetFormat(url, context, defaultGetFormat) + : defaultGetFormat(href, context, defaultGetFormat) } /** @@ -63,12 +63,14 @@ export function createLoader() { * @param {Function} defaultTransformSource */ async function transformSource(value, context, defaultTransformSource) { - if (path.extname(context.url) !== '.jsx') { + const url = new URL(context.url) + + if (!url.pathname.endsWith('.jsx')) { return defaultTransformSource(value, context, defaultTransformSource) } const {code, warnings} = transformSync(String(value), { - sourcefile: fileURLToPath(context.url), + sourcefile: fileURLToPath(url), sourcemap: 'both', loader: 'jsx', target: 'esnext', diff --git a/test/test.jsx b/test/test.jsx index 8b06635b..d238017a 100644 --- a/test/test.jsx +++ b/test/test.jsx @@ -1,15 +1,10 @@ /** - * @typedef {import('unist').Node} Node - * @typedef {import('unist').Position} Position * @typedef {import('hast').Root} Root * @typedef {import('hast').Element} Element - * @typedef {import('hast').Text} Text * @typedef {import('react').ReactNode} ReactNode - * @typedef {import('../index.js').Components} Components */ -import fs from 'node:fs' -import path from 'node:path' +import fs from 'node:fs/promises' import {test} from 'uvu' import * as assert from 'uvu/assert' import React from 'react' @@ -1057,13 +1052,11 @@ test('should throw on invalid component', () => { ) }) -test('can render the whole spectrum of markdown within a single run', () => { - const input = String( - fs.readFileSync(path.join('test', 'fixtures', 'runthrough.md')) - ) - const expected = String( - fs.readFileSync(path.join('test', 'fixtures', 'runthrough.html')) - ) +test('can render the whole spectrum of markdown within a single run', async () => { + const inputUrl = new URL('fixtures/runthrough.md', import.meta.url) + const expectedUrl = new URL('fixtures/runthrough.html', import.meta.url) + const input = String(await fs.readFile(inputUrl)) + const expected = String(await fs.readFile(expectedUrl)) const actual = asHtml( From b81001f14614f9921840917b642efb51c7c72100 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Tue, 17 Jan 2023 19:46:15 +0100 Subject: [PATCH 033/125] Refactor to use more explicit names --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 18c6998b..098151a1 100644 --- a/package.json +++ b/package.json @@ -134,19 +134,19 @@ }, "remarkConfig": { "plugins": [ - "preset-wooorm", + "remark-preset-wooorm", [ - "gfm", + "remark-gfm", { "tablePipeAlign": false } ], [ - "lint-table-pipe-alignment", + "remark-lint-table-pipe-alignment", false ], [ - "lint-no-html", + "remark-lint-no-html", false ] ] From 5ab03c4d93032f22366a51523c64d3e1b88e522b Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Tue, 17 Jan 2023 19:55:37 +0100 Subject: [PATCH 034/125] Use Node test runner --- .github/workflows/main.yml | 2 +- package.json | 4 ++-- test/test.jsx | 14 ++++++-------- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 89dc06c8..fb633871 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,5 +17,5 @@ jobs: strategy: matrix: node: - - lts/fermium + - lts/gallium - node diff --git a/package.json b/package.json index 098151a1..c9770baf 100644 --- a/package.json +++ b/package.json @@ -100,6 +100,7 @@ "react": ">=16" }, "devDependencies": { + "@types/node": "^18.0.0", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "@types/react-is": "^17.0.0", @@ -121,14 +122,13 @@ "rimraf": "^3.0.0", "type-coverage": "^2.0.0", "typescript": "^4.0.0", - "uvu": "^0.5.0", "xo": "^0.53.0" }, "scripts": { "prepack": "npm run build && npm run format", "build": "tsc --build --clean && tsc --build && 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 --conditions development ./node_modules/.bin/uvu test \"\\.jsx$\"", + "test-api": "node --no-warnings --experimental-loader=./test/loader.js --conditions development test/test.jsx", "test-coverage": "c8 --check-coverage --100 --reporter lcov npm run test-api", "test": "npm run build && npm run format && npm run test-coverage" }, diff --git a/test/test.jsx b/test/test.jsx index d238017a..78c35dad 100644 --- a/test/test.jsx +++ b/test/test.jsx @@ -4,9 +4,9 @@ * @typedef {import('react').ReactNode} ReactNode */ +import assert from 'node:assert/strict' import fs from 'node:fs/promises' -import {test} from 'uvu' -import * as assert from 'uvu/assert' +import test from 'node:test' import React from 'react' import gfm from 'remark-gfm' import {visit} from 'unist-util-visit' @@ -728,7 +728,7 @@ test('should pass on raw source position to non-tag components if rawSourcePos o rawSourcePos components={{ em({node, sourcePosition, ...props}) { - assert.equal(sourcePosition, { + assert.deepEqual(sourcePosition, { start: {line: 1, column: 1, offset: 0}, end: {line: 1, column: 6, offset: 5} }) @@ -754,7 +754,7 @@ test('should pass on raw source position to non-tag components if rawSourcePos o components={{ // @ts-expect-error JSX types currently only handle element returns not string returns em({sourcePosition}) { - assert.equal(sourcePosition, { + assert.deepEqual(sourcePosition, { start: {line: 1, column: 1, offset: 0}, end: {line: 1, column: 6, offset: 5} }) @@ -1436,7 +1436,7 @@ test('should support remark plugins with array parameter', async () => { const expected = '

a

' assert.equal(actual, expected) - assert.not.match(message, /Warning: Failed/, 'Prop types should be valid') + assert.doesNotMatch(message, /Warning: Failed/, 'Prop types should be valid') console.error = error }) @@ -1459,8 +1459,6 @@ test('should support rehype plugins with array parameter', async () => { const expected = '

a

' assert.equal(actual, expected) - assert.not.match(message, /Warning: Failed/, 'Prop types should be valid') + assert.doesNotMatch(message, /Warning: Failed/, 'Prop types should be valid') console.error = error }) - -test.run() From 469ebc22a83daa9448434c623721a419f364303d Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Tue, 17 Jan 2023 20:01:57 +0100 Subject: [PATCH 035/125] Add tests for exposed identifiers --- test/test.jsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/test.jsx b/test/test.jsx index 78c35dad..8636ae9f 100644 --- a/test/test.jsx +++ b/test/test.jsx @@ -14,6 +14,7 @@ import raw from 'rehype-raw' import toc from 'remark-toc' import ReactDom from 'react-dom/server' import Markdown from '../index.js' +import * as mod from '../index.js' const own = {}.hasOwnProperty @@ -25,6 +26,14 @@ function asHtml(input) { return ReactDom.renderToStaticMarkup(input) } +test('ReactMarkdown', () => { + assert.deepEqual( + Object.keys(mod).sort(), + ['default', 'uriTransformer'], + 'should expose the public api' + ) +}) + test('can render the most basic of documents (single paragraph)', () => { assert.equal(asHtml(Test), '

Test

') }) From 8c40d4eff3489b98569d292900da80cc35d6237b Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Tue, 17 Jan 2023 20:18:48 +0100 Subject: [PATCH 036/125] 8.0.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c9770baf..56c7f722 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-markdown", - "version": "8.0.4", + "version": "8.0.5", "description": "React component to render markdown", "license": "MIT", "keywords": [ From 5aad6226898626b24cb6de04d177746785205e65 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Fri, 10 Feb 2023 17:11:44 +0100 Subject: [PATCH 037/125] Add test for nullish properties --- test/test.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test.jsx b/test/test.jsx index 8636ae9f..0daf8fc8 100644 --- a/test/test.jsx +++ b/test/test.jsx @@ -1263,7 +1263,7 @@ test('should support data properties', () => { root.children.unshift({ type: 'element', tagName: 'i', - properties: {dataWhatever: 'a'}, + properties: {dataWhatever: 'a', dataIgnoreThis: undefined}, children: [] }) } From a3de85b4d77486ce26f4efa9c8ab07f8e40ad061 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Fri, 10 Feb 2023 17:14:36 +0100 Subject: [PATCH 038/125] Remove ununsed dev-dependency --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 56c7f722..42827ebc 100644 --- a/package.json +++ b/package.json @@ -119,7 +119,6 @@ "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.0.0", "xo": "^0.53.0" From 33ab0158b1ece600c7672519f5191d8dd50a8d2a Mon Sep 17 00:00:00 2001 From: Nathan Bierema Date: Mon, 20 Mar 2023 08:39:51 -0400 Subject: [PATCH 039/125] Update to TS 5 Closes GH-734. Reviewed-by: Titus Wormer --- lib/ast-to-react.js | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/ast-to-react.js b/lib/ast-to-react.js index da2cbe8f..57344426 100644 --- a/lib/ast-to-react.js +++ b/lib/ast-to-react.js @@ -4,7 +4,7 @@ */ /** - * @template T + * @template {import('react').ElementType} T * @typedef {import('react').ComponentPropsWithoutRef} ComponentPropsWithoutRef */ diff --git a/package.json b/package.json index 42827ebc..f2a0b86f 100644 --- a/package.json +++ b/package.json @@ -120,7 +120,7 @@ "remark-preset-wooorm": "^9.0.0", "remark-toc": "^8.0.0", "type-coverage": "^2.0.0", - "typescript": "^4.0.0", + "typescript": "^5.0.0", "xo": "^0.53.0" }, "scripts": { From 298b4f1a91d54ddae2652b7ccbbb353b87eccb2b Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Mon, 20 Mar 2023 13:47:34 +0100 Subject: [PATCH 040/125] 8.0.6 --- changelog.md | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 2 +- 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index e9b29ef5..481cf7ce 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,62 @@ All notable changes will be documented in this file. +## 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 diff --git a/package.json b/package.json index f2a0b86f..ba585077 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-markdown", - "version": "8.0.5", + "version": "8.0.6", "description": "React component to render markdown", "license": "MIT", "keywords": [ From 9034dbdbe45739face4b7c86bea70ed38c16ae1e Mon Sep 17 00:00:00 2001 From: david qiu Date: Thu, 6 Apr 2023 00:33:02 -0700 Subject: [PATCH 041/125] Fix types in syntax highlight example Closes GH-736. Reviewed-by: Remco Haszing Reviewed-by: Christian Murphy Reviewed-by: Titus Wormer --- readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index caceef38..2b00a70e 100644 --- a/readme.md +++ b/readme.md @@ -354,14 +354,14 @@ ReactDom.render( const match = /language-(\w+)/.exec(className || '') return !inline && match ? ( ) : ( - + {children} ) From 184a1a320c8a5bb12fae622727e9d33cd54308e2 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Tue, 11 Apr 2023 11:54:59 +0200 Subject: [PATCH 042/125] Update dev-dependencies --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ba585077..8743fdd8 100644 --- a/package.json +++ b/package.json @@ -121,7 +121,7 @@ "remark-toc": "^8.0.0", "type-coverage": "^2.0.0", "typescript": "^5.0.0", - "xo": "^0.53.0" + "xo": "^0.54.0" }, "scripts": { "prepack": "npm run build && npm run format", From c289176413e36bd9a8aa1c9e1124a8bb51583899 Mon Sep 17 00:00:00 2001 From: Titus Date: Wed, 12 Apr 2023 12:07:46 +0200 Subject: [PATCH 043/125] Fix performance for keys Closes GH-703. Closes GH-738. Reviewed-by: Christian Murphy Reviewed-by: Merlijn Vos Reviewed-by: Remco Haszing --- lib/ast-to-react.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/lib/ast-to-react.js b/lib/ast-to-react.js index 57344426..5248e5e1 100644 --- a/lib/ast-to-react.js +++ b/lib/ast-to-react.js @@ -221,12 +221,7 @@ function toReact(context, node, index, parent) { ) } - properties.key = [ - name, - position.start.line, - position.start.column, - index - ].join('-') + properties.key = index if (name === 'a' && options.linkTarget) { properties.target = From 4a6065ba509f1232b94d3b79b6d426626aede6d2 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Wed, 12 Apr 2023 12:09:31 +0200 Subject: [PATCH 044/125] 8.0.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8743fdd8..f6ed4f40 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-markdown", - "version": "8.0.6", + "version": "8.0.7", "description": "React component to render markdown", "license": "MIT", "keywords": [ From 0b5af9e8efc99d1267b7121c60d6b10f223699aa Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Mon, 17 Apr 2023 12:05:32 +0200 Subject: [PATCH 045/125] Refactor tests --- test/test.jsx | 57 ++++++++++++++++++++++++++++++++------------------- 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/test/test.jsx b/test/test.jsx index 0daf8fc8..429f734a 100644 --- a/test/test.jsx +++ b/test/test.jsx @@ -8,13 +8,12 @@ import assert from 'node:assert/strict' import fs from 'node:fs/promises' import test from 'node:test' import React from 'react' -import gfm from 'remark-gfm' -import {visit} from 'unist-util-visit' -import raw from 'rehype-raw' -import toc from 'remark-toc' import ReactDom from 'react-dom/server' +import rehypeRaw from 'rehype-raw' +import remarkGfm from 'remark-gfm' +import remarkToc from 'remark-toc' +import {visit} from 'unist-util-visit' import Markdown from '../index.js' -import * as mod from '../index.js' const own = {}.hasOwnProperty @@ -26,9 +25,9 @@ function asHtml(input) { return ReactDom.renderToStaticMarkup(input) } -test('ReactMarkdown', () => { +test('ReactMarkdown', async () => { assert.deepEqual( - Object.keys(mod).sort(), + Object.keys(await import('../index.js')).sort(), ['default', 'uriTransformer'], 'should expose the public api' ) @@ -584,7 +583,7 @@ test('should pass `isHeader: boolean` to `tr`s', () => { const actual = asHtml( const actual = asHtml( ) assert.equal( @@ -828,7 +827,7 @@ test('should render tables', () => { ].join('\n') assert.equal( - asHtml(), + asHtml(), '

Languages are fun, right?

\n
IDEnglishNorwegianItalian
1oneenuno
2twotodue
3threetretre
' ) }) @@ -837,7 +836,7 @@ test('should render partial tables', () => { const input = 'User is writing a table by hand\n\n| Test | Test |\n|-|-|' assert.equal( - asHtml(), + asHtml(), '

User is writing a table by hand

\n
TestTest
' ) }) @@ -889,7 +888,7 @@ test('should render footnote with custom options', () => { asHtml( ), @@ -1068,7 +1067,11 @@ test('can render the whole spectrum of markdown within a single run', async () = const expected = String(await fs.readFile(expectedUrl)) const actual = asHtml( - + ) assert.equal(actual, expected) @@ -1127,13 +1130,17 @@ test('should support turning off the default URI transform', () => { test('can use parser plugins', () => { const input = 'a ~b~ c' - const actual = asHtml() + const actual = asHtml( + + ) assert.equal(actual, '

a b c

') }) test('supports checkbox lists', () => { const input = '- [ ] Foo\n- [x] Bar\n\n---\n\n- Foo\n- Bar' - const actual = asHtml() + const actual = asHtml( + + ) assert.equal( actual, '
    \n
  • Foo
  • \n
  • Bar
  • \n
\n
\n
    \n
  • Foo
  • \n
  • Bar
  • \n
' @@ -1202,7 +1209,9 @@ test('should render table of contents plugin', () => { '## Third Section' ].join('\n') - const actual = asHtml() + const actual = asHtml( + + ) assert.equal( actual, '

Header

\n

Table of Contents

\n\n

First Section

\n

Second Section

\n

Subsection

\n

Third Section

' @@ -1231,7 +1240,9 @@ test('should pass `node` as prop to all non-tag/non-fragment components', () => test('should support formatting at the start of a GFM tasklist (GH-494)', () => { const input = '- [ ] *a*' - const actual = asHtml() + const actual = asHtml( + + ) const expected = '
    \n
  • a
  • \n
' assert.equal(actual, expected) @@ -1408,7 +1419,11 @@ test('should support table cells w/ style', () => { } const actual = asHtml( - + ) const expected = '
a
' From d044e34b4f22caa24545c0b355d2205de42dfc32 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Tue, 9 May 2023 12:13:33 +0200 Subject: [PATCH 046/125] Update dev-dependencies --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index f6ed4f40..88ae3f18 100644 --- a/package.json +++ b/package.json @@ -100,10 +100,10 @@ "react": ">=16" }, "devDependencies": { - "@types/node": "^18.0.0", + "@types/node": "^20.0.0", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", - "@types/react-is": "^17.0.0", + "@types/react-is": "^18.0.0", "c8": "^7.0.0", "esbuild": "^0.17.0", "eslint-config-xo-react": "^0.27.0", From aa6cfd82afd35552b30bf1ac1b5cdb7a8c67b7c0 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Sat, 17 Jun 2023 09:23:10 +0200 Subject: [PATCH 047/125] Update dev-dependencies --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 88ae3f18..a15fcd9d 100644 --- a/package.json +++ b/package.json @@ -104,8 +104,8 @@ "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "@types/react-is": "^18.0.0", - "c8": "^7.0.0", - "esbuild": "^0.17.0", + "c8": "^8.0.0", + "esbuild": "^0.18.0", "eslint-config-xo-react": "^0.27.0", "eslint-plugin-es": "^4.0.0", "eslint-plugin-react": "^7.0.0", From 0242d11ae4dfb2e83b579e55653832d454a3887a Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Sat, 17 Jun 2023 09:23:20 +0200 Subject: [PATCH 048/125] Fix tests for changes in `@types/react` --- test/test.jsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/test.jsx b/test/test.jsx index 429f734a..aef80589 100644 --- a/test/test.jsx +++ b/test/test.jsx @@ -760,7 +760,6 @@ test('should pass on raw source position to non-tag components if rawSourcePos o rawSourcePos rehypePlugins={[rehypeRaw]} components={{ - // @ts-expect-error JSX types currently only handle element returns not string returns em({sourcePosition}) { assert.deepEqual(sourcePosition, { start: {line: 1, column: 1, offset: 0}, @@ -1224,7 +1223,6 @@ test('should pass `node` as prop to all non-tag/non-fragment components', () => { From de29396951ce2d9cf2b0d050d229da4546cf77ac Mon Sep 17 00:00:00 2001 From: Gal Abra Date: Mon, 25 Sep 2023 20:41:58 +0300 Subject: [PATCH 049/125] Removing `linkTarget` option Closes GH-761. Closes GH-762. Reviewed-by: Titus Wormer Reviewed-by: Christian Murphy Reviewed-by: Remco Haszing --- lib/ast-to-react.js | 20 ---------------- lib/react-markdown.js | 1 - readme.md | 4 ---- test/test.jsx | 56 ------------------------------------------- 4 files changed, 81 deletions(-) diff --git a/lib/ast-to-react.js b/lib/ast-to-react.js index 5248e5e1..ef02544e 100644 --- a/lib/ast-to-react.js +++ b/lib/ast-to-react.js @@ -42,14 +42,6 @@ * @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? @@ -96,7 +88,6 @@ * @property {boolean} [includeElementIndex=false] * @property {null|false|TransformLink} [transformLinkUri] * @property {TransformImage} [transformImageUri] - * @property {TransformLinkTargetType|TransformLinkTarget} [linkTarget] * @property {Components} [components] */ @@ -223,17 +214,6 @@ function toReact(context, node, index, parent) { properties.key = index - 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' && transform) { properties.href = transform( String(properties.href || ''), diff --git a/lib/react-markdown.js b/lib/react-markdown.js index 92a032c7..df97b321 100644 --- a/lib/react-markdown.js +++ b/lib/react-markdown.js @@ -178,7 +178,6 @@ ReactMarkdown.propTypes = { 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/readme.md b/readme.md index 2b00a70e..30be3fdc 100644 --- a/readme.md +++ b/readme.md @@ -203,8 +203,6 @@ The default export is `ReactMarkdown`. 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][] @@ -632,8 +630,6 @@ Optionally, components will also receive: — see `rawSourcePos` option * `index` and `siblingCount` (`number`) — see `includeElementIndex` option -* `target` on `a` (`string`) - — see `linkTarget` option ## Security diff --git a/test/test.jsx b/test/test.jsx index aef80589..ed0418f3 100644 --- a/test/test.jsx +++ b/test/test.jsx @@ -231,62 +231,6 @@ test('should handle titles of links', () => { assert.equal(actual, '

Empty:

') }) -test('should use target attribute for links if specified', () => { - const input = 'This is [a link](https://espen.codes/) to Espen.Codes.' - const actual = asHtml() - assert.equal( - actual, - '

This is a link to Espen.Codes.

' - ) -}) - -test('should call function to get target attribute for links if specified', () => { - const input = 'This is [a link](https://espen.codes/) to Espen.Codes.' - const actual = asHtml( - (uri.startsWith('http') ? '_blank' : undefined)} - /> - ) - assert.equal( - actual, - '

This is a link to Espen.Codes.

' - ) -}) - -test('should handle links with custom target transformer', () => { - const input = 'Empty: []()' - - const actual = asHtml( - { - assert.equal(uri, '', '`uri` should be an empty string') - assert.equal(title, null, '`title` should be null') - return undefined - }} - /> - ) - - assert.equal(actual, '

Empty:

') -}) - -test('should handle links w/ titles with custom target transformer', () => { - const input = 'Empty: [](a "b")' - - const actual = asHtml( - { - assert.equal(title, 'b', '`title` should be given') - return undefined - }} - /> - ) - - assert.equal(actual, '

Empty:

') -}) - test('should support images without alt, url, or title', () => { const input = '![]()' const actual = asHtml() From 9ef49296657d6d65c788fa18bcd317360b2e8807 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Mon, 25 Sep 2023 19:45:54 +0200 Subject: [PATCH 050/125] Update dev-dependencies --- package.json | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index a15fcd9d..e7de0cec 100644 --- a/package.json +++ b/package.json @@ -105,13 +105,13 @@ "@types/react-dom": "^18.0.0", "@types/react-is": "^18.0.0", "c8": "^8.0.0", - "esbuild": "^0.18.0", + "esbuild": "^0.19.0", "eslint-config-xo-react": "^0.27.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", + "prettier": "^3.0.0", "react": "^18.0.0", "react-dom": "^18.0.0", "rehype-raw": "^6.0.0", @@ -121,12 +121,12 @@ "remark-toc": "^8.0.0", "type-coverage": "^2.0.0", "typescript": "^5.0.0", - "xo": "^0.54.0" + "xo": "^0.56.0" }, "scripts": { "prepack": "npm run build && npm run format", "build": "tsc --build --clean && tsc --build && 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", + "format": "remark . -qfo --ignore-pattern test/ && prettier . -w --log-level warn && xo --fix", "test-api": "node --no-warnings --experimental-loader=./test/loader.js --conditions development test/test.jsx", "test-coverage": "c8 --check-coverage --100 --reporter lcov npm run test-api", "test": "npm run build && npm run format && npm run test-coverage" @@ -175,6 +175,9 @@ "envs": [ "shared-node-browser" ], + "rules": { + "unicorn/prefer-string-replace-all": "off" + }, "overrides": [ { "files": [ From 68ffc6ad506d811fffc99b1cf5c0dad9a89777e5 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Mon, 25 Sep 2023 19:50:17 +0200 Subject: [PATCH 051/125] Refactor `tsconfig.json` --- .gitignore | 1 + lib/{complex-types.ts => complex-types.d.ts} | 0 tsconfig.json | 18 +++++++++--------- 3 files changed, 10 insertions(+), 9 deletions(-) rename lib/{complex-types.ts => complex-types.d.ts} (100%) diff --git a/.gitignore b/.gitignore index fdbc06a8..a78a71ba 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ node_modules/ .DS_Store react-markdown.min.js yarn.lock +!/lib/complex-types.d.ts diff --git a/lib/complex-types.ts b/lib/complex-types.d.ts similarity index 100% rename from lib/complex-types.ts rename to lib/complex-types.d.ts diff --git a/tsconfig.json b/tsconfig.json index 8583ce16..31c68dc0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,18 +1,18 @@ { - "include": ["**/*.js", "**/*.jsx"], - "exclude": ["coverage/", "node_modules/", "**/*.min.js"], "compilerOptions": { "checkJs": true, + "customConditions": ["development"], "declaration": true, "emitDeclarationOnly": true, "exactOptionalPropertyTypes": true, - "forceConsistentCasingInFileNames": true, - "lib": ["es2020"], + "jsx": "preserve", + "lib": ["dom", "es2020"], "module": "node16", - "newLine": "lf", - "skipLibCheck": true, "strict": true, - "target": "es2021", - "jsx": "preserve" - } + // To do: remove after update. + "skipLibCheck": true, + "target": "es2020" + }, + "exclude": ["coverage/", "node_modules/", "**/*.min.js"], + "include": ["**/*.js", "**/*.jsx", "lib/complex-types.d.ts"] } From f6d01f354bf0bacf31806d9996423e3c7b437469 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Mon, 25 Sep 2023 19:55:52 +0200 Subject: [PATCH 052/125] Refactor `package.json` --- .remarkignore | 1 + package.json | 58 +++++++++++++++++++++++++-------------------------- 2 files changed, 29 insertions(+), 30 deletions(-) create mode 100644 .remarkignore diff --git a/.remarkignore b/.remarkignore new file mode 100644 index 00000000..ebb6519a --- /dev/null +++ b/.remarkignore @@ -0,0 +1 @@ +/test/fixtures/ diff --git a/package.json b/package.json index e7de0cec..150a2367 100644 --- a/package.json +++ b/package.json @@ -4,15 +4,15 @@ "description": "React component to render markdown", "license": "MIT", "keywords": [ - "remark", - "unified", - "markdown", + "ast", "commonmark", + "component", "gfm", - "ast", + "markdown", "react", "react-component", - "component" + "remark", + "unified" ], "repository": "remarkjs/react-markdown", "bugs": "https://github.com/remarkjs/react-markdown/issues", @@ -124,12 +124,20 @@ "xo": "^0.56.0" }, "scripts": { - "prepack": "npm run build && npm run format", "build": "tsc --build --clean && tsc --build && 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 --log-level warn && xo --fix", - "test-api": "node --no-warnings --experimental-loader=./test/loader.js --conditions development test/test.jsx", - "test-coverage": "c8 --check-coverage --100 --reporter lcov npm run test-api", - "test": "npm run build && npm run format && npm run test-coverage" + "format": "remark . --frail --output --quiet && prettier . --log-level warn --write && xo --fix", + "prepack": "npm run build && npm run format", + "test": "npm run build && npm run format && npm run test-coverage", + "test-api": "node --conditions development --experimental-loader=./test/loader.js --no-warnings test/test.jsx", + "test-coverage": "c8 --100 --reporter lcov npm run test-api" + }, + "prettier": { + "bracketSpacing": false, + "singleQuote": true, + "semi": false, + "tabWidth": 2, + "trailingComma": "none", + "useTabs": false }, "remarkConfig": { "plugins": [ @@ -153,31 +161,18 @@ "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" + "lib/react-markdown.d.ts" + ], + "strict": true }, "xo": { - "prettier": true, - "extends": "xo-react", "envs": [ "shared-node-browser" ], - "rules": { - "unicorn/prefer-string-replace-all": "off" - }, + "extends": "xo-react", "overrides": [ { "files": [ @@ -198,10 +193,13 @@ ], "rules": { "n/file-extension-in-import": "off", - "react/no-children-prop": "off", - "react/prop-types": "off" + "react/no-children-prop": "off" } } - ] + ], + "prettier": true, + "rules": { + "unicorn/prefer-string-replace-all": "off" + } } } From 231976d75f8ce8052bf268751f0441306b76ac76 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Mon, 25 Sep 2023 19:56:43 +0200 Subject: [PATCH 053/125] Refactor Actions --- .github/workflows/main.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fb633871..626f97d8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,7 +1,3 @@ -name: main -on: - - pull_request - - push jobs: main: name: ${{matrix.node}} @@ -19,3 +15,7 @@ jobs: node: - lts/gallium - node +name: main +on: + - pull_request + - push From 107c8ba4d4db873139b5b2890ce493d8c319eb5f Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Mon, 25 Sep 2023 19:56:52 +0200 Subject: [PATCH 054/125] Refactor `.npmrc` --- .npmrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.npmrc b/.npmrc index 9951b11b..3757b304 100644 --- a/.npmrc +++ b/.npmrc @@ -1,2 +1,2 @@ -package-lock=false ignore-scripts=true +package-lock=false From c383a45b75685204a1804ea4361d08d19bbeb1cf Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Mon, 25 Sep 2023 20:06:59 +0200 Subject: [PATCH 055/125] Update `@types/hast`, utilities, plugins, etc --- lib/ast-to-react.js | 2 +- package.json | 24 ++++++++++++------------ test/fixtures/runthrough.html | 8 ++++---- test/fixtures/runthrough.md | 2 +- test/test.jsx | 5 +++-- 5 files changed, 21 insertions(+), 20 deletions(-) diff --git a/lib/ast-to-react.js b/lib/ast-to-react.js index ef02544e..d32c06b0 100644 --- a/lib/ast-to-react.js +++ b/lib/ast-to-react.js @@ -16,7 +16,7 @@ * @typedef {import('hast').Root} Root * @typedef {import('hast').Text} Text * @typedef {import('hast').Comment} Comment - * @typedef {import('hast').DocType} Doctype + * @typedef {import('hast').Doctype} Doctype * @typedef {import('property-information').Info} Info * @typedef {import('property-information').Schema} Schema * @typedef {import('./complex-types.js').ReactMarkdownProps} ReactMarkdownProps diff --git a/package.json b/package.json index 150a2367..6f0526c5 100644 --- a/package.json +++ b/package.json @@ -79,21 +79,21 @@ "react-markdown.min.js" ], "dependencies": { - "@types/hast": "^2.0.0", + "@types/hast": "^3.0.0", "@types/prop-types": "^15.0.0", - "@types/unist": "^2.0.0", + "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", - "hast-util-whitespace": "^2.0.0", + "hast-util-whitespace": "^3.0.0", "prop-types": "^15.0.0", "property-information": "^6.0.0", "react-is": "^18.0.0", - "remark-parse": "^10.0.0", - "remark-rehype": "^10.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", "space-separated-tokens": "^2.0.0", "style-to-object": "^0.4.0", - "unified": "^10.0.0", - "unist-util-visit": "^4.0.0", - "vfile": "^5.0.0" + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=16", @@ -114,11 +114,11 @@ "prettier": "^3.0.0", "react": "^18.0.0", "react-dom": "^18.0.0", - "rehype-raw": "^6.0.0", + "rehype-raw": "^7.0.0", "remark-cli": "^11.0.0", - "remark-gfm": "^3.0.0", + "remark-gfm": "^4.0.0", "remark-preset-wooorm": "^9.0.0", - "remark-toc": "^8.0.0", + "remark-toc": "^9.0.0", "type-coverage": "^2.0.0", "typescript": "^5.0.0", "xo": "^0.56.0" @@ -143,7 +143,7 @@ "plugins": [ "remark-preset-wooorm", [ - "remark-gfm", + "./node_modules/remark-preset-wooorm/node_modules/remark-gfm/index.js", { "tablePipeAlign": false } diff --git a/test/fixtures/runthrough.html b/test/fixtures/runthrough.html index 8dd0c334..824af77d 100644 --- a/test/fixtures/runthrough.html +++ b/test/fixtures/runthrough.html @@ -152,10 +152,10 @@

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 + +

We used to have a known bug where inline HTML wasn't handled well. You can do basic tags like +code, 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.

+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 index c643ab4c..7fec387f 100644 --- a/test/fixtures/runthrough.md +++ b/test/fixtures/runthrough.md @@ -159,7 +159,7 @@ Does anyone actually like the fact that you can embed HTML in markdown? src="https://foo.bar/" width="640" height="480" -/> +> We used to have a known bug where inline HTML wasn't handled well. You can do basic tags like code, as long as it doesn't contain any attributes. If you diff --git a/test/test.jsx b/test/test.jsx index ed0418f3..b21bfb7a 100644 --- a/test/test.jsx +++ b/test/test.jsx @@ -835,7 +835,7 @@ test('should render footnote with custom options', () => { remarkRehypeOptions={{clobberPrefix: 'main-'}} /> ), - '

This is a statement1 with a citation.

\n

Footnotes

\n
    \n
  1. \n

    This is a footnote for the citation.

    \n
  2. \n
\n
' + '

This is a statement1 with a citation.

\n

Footnotes

\n
    \n
  1. \n

    This is a footnote for the citation.

    \n
  2. \n
\n
' ) }) @@ -1155,9 +1155,10 @@ test('should render table of contents plugin', () => { const actual = asHtml( ) + assert.equal( actual, - '

Header

\n

Table of Contents

\n\n

First Section

\n

Second Section

\n

Subsection

\n

Third Section

' + '

Header

\n

Table of Contents

\n\n

First Section

\n

Second Section

\n

Subsection

\n

Third Section

' ) }) From de252e8187d3458241a9e0b9ea16cab0f013ca92 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Tue, 26 Sep 2023 18:30:56 +0200 Subject: [PATCH 056/125] Refactor code-style --- index.js | 2 +- lib/ast-to-react.js | 264 ++-- lib/complex-types.d.ts | 24 +- lib/react-markdown.js | 155 ++- lib/rehype-filter.js | 60 +- lib/uri-transformer.js | 11 +- package.json | 7 +- readme.md | 83 +- test/fixtures/runthrough.html | 161 --- test/fixtures/runthrough.md | 171 --- test/loader.js | 26 +- test/test.jsx | 2373 +++++++++++++++------------------ tsconfig.json | 4 +- 13 files changed, 1442 insertions(+), 1899 deletions(-) delete mode 100644 test/fixtures/runthrough.html delete mode 100644 test/fixtures/runthrough.md diff --git a/index.js b/index.js index 918d3ff5..6e83f36e 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,5 @@ /** - * @typedef {import('./lib/react-markdown.js').ReactMarkdownOptions} Options + * @typedef {import('./lib/react-markdown.js').Options} Options * @typedef {import('./lib/ast-to-react.js').Components} Components */ diff --git a/lib/ast-to-react.js b/lib/ast-to-react.js index d32c06b0..9076b38e 100644 --- a/lib/ast-to-react.js +++ b/lib/ast-to-react.js @@ -1,103 +1,87 @@ +/// + /** - * @template T - * @typedef {import('react').ComponentType} ComponentType + * @typedef {import('react').ComponentPropsWithoutRef} ComponentPropsWithoutRef + * @template {import('react').ElementType} T */ /** - * @template {import('react').ElementType} T - * @typedef {import('react').ComponentPropsWithoutRef} ComponentPropsWithoutRef + * @typedef {import('react').ComponentType} ComponentType + * @template T */ /** - * @typedef {import('react').ReactNode} ReactNode - * @typedef {import('unist').Position} Position * @typedef {import('hast').Element} Element - * @typedef {import('hast').ElementContent} ElementContent + * @typedef {import('hast').Parents} Parents * @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.js').ReactMarkdownProps} ReactMarkdownProps * - * @typedef Raw - * @property {'raw'} type - * @property {string} value + * @typedef {import('react').ReactNode} ReactNode + * + * @typedef {import('unist').Position} Position * - * @typedef Context - * @property {Options} options + * @typedef {import('./complex-types.js').ReactMarkdownProps} ReactMarkdownProps + * @typedef {import('./complex-types.js').NormalComponents} NormalComponents + * @typedef {import('./react-markdown.js').Options} Options + */ + +/** + * @typedef State + * Info passed around. + * @property {Readonly} options + * Configuration. * @property {Schema} 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 {keyof JSX.IntrinsicElements} ReactMarkdownNames - * - * To do: is `data-sourcepos` typeable? + * Depth. * * @typedef {ComponentPropsWithoutRef<'code'> & ReactMarkdownProps & {inline?: boolean}} CodeProps + * Props passed to components for `code`. + * to do: always pass `inline`? * @typedef {ComponentPropsWithoutRef<'h1'> & ReactMarkdownProps & {level: number}} HeadingProps - * @typedef {ComponentPropsWithoutRef<'li'> & ReactMarkdownProps & {checked: boolean|null, index: number, ordered: boolean}} LiProps + * Props passed to components for `h1`, `h2`, etc. + * @typedef {ComponentPropsWithoutRef<'li'> & ReactMarkdownProps & {checked: boolean | null, index: number, ordered: boolean}} LiProps + * Props passed to components for `li`. + * to do: use `undefined`. * @typedef {ComponentPropsWithoutRef<'ol'> & ReactMarkdownProps & {depth: number, ordered: true}} OrderedListProps - * @typedef {ComponentPropsWithoutRef<'td'> & ReactMarkdownProps & {style?: Record, isHeader: false}} TableDataCellProps - * @typedef {ComponentPropsWithoutRef<'th'> & ReactMarkdownProps & {style?: Record, isHeader: true}} TableHeaderCellProps + * Props passed to components for `ol`. + * @typedef {ComponentPropsWithoutRef<'td'> & ReactMarkdownProps & {isHeader: false}} TableDataCellProps + * Props passed to components for `td`. + * @typedef {ComponentPropsWithoutRef<'th'> & ReactMarkdownProps & {isHeader: true}} TableHeaderCellProps + * Props passed to components for `th`. * @typedef {ComponentPropsWithoutRef<'tr'> & ReactMarkdownProps & {isHeader: boolean}} TableRowProps + * Props passed to components for `tr`. * @typedef {ComponentPropsWithoutRef<'ul'> & ReactMarkdownProps & {depth: number, ordered: false}} UnorderedListProps - * - * @typedef {ComponentType} CodeComponent - * @typedef {ComponentType} HeadingComponent - * @typedef {ComponentType} LiComponent - * @typedef {ComponentType} OrderedListComponent - * @typedef {ComponentType} TableDataCellComponent - * @typedef {ComponentType} TableHeaderCellComponent - * @typedef {ComponentType} TableRowComponent - * @typedef {ComponentType} UnorderedListComponent + * Props passed to components for `ul`. * * @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 {TableDataCellComponent|ReactMarkdownNames} td - * @property {TableHeaderCellComponent|ReactMarkdownNames} th - * @property {TableRowComponent|ReactMarkdownNames} tr - * @property {UnorderedListComponent|ReactMarkdownNames} ul + * @property {ComponentType | keyof JSX.IntrinsicElements} code + * @property {ComponentType | keyof JSX.IntrinsicElements} h1 + * @property {ComponentType | keyof JSX.IntrinsicElements} h2 + * @property {ComponentType | keyof JSX.IntrinsicElements} h3 + * @property {ComponentType | keyof JSX.IntrinsicElements} h4 + * @property {ComponentType | keyof JSX.IntrinsicElements} h5 + * @property {ComponentType | keyof JSX.IntrinsicElements} h6 + * @property {ComponentType | keyof JSX.IntrinsicElements} li + * @property {ComponentType | keyof JSX.IntrinsicElements} ol + * @property {ComponentType | keyof JSX.IntrinsicElements} td + * @property {ComponentType | keyof JSX.IntrinsicElements} th + * @property {ComponentType | keyof JSX.IntrinsicElements} tr + * @property {ComponentType | keyof JSX.IntrinsicElements} 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 {Components} [components] + * @typedef {Partial & SpecialComponents>} Components + * Components. */ import React from 'react' import ReactIs from 'react-is' +import {stringify as commas} from 'comma-separated-tokens' import {whitespace} from 'hast-util-whitespace' -import {svg, find, hastToReact} from 'property-information' +import {find, hastToReact, svg} 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' +import {stringifyPosition} from 'unist-util-stringify-position' import {uriTransformer} from './uri-transformer.js' const own = {}.hasOwnProperty @@ -107,21 +91,23 @@ const own = {}.hasOwnProperty const tableElements = new Set(['table', 'thead', 'tbody', 'tfoot', 'tr']) /** - * @param {Context} context - * @param {Element|Root} node + * @param {State} state + * Info passed around. + * @param {Readonly} node + * Node to transform. + * @returns {Array} + * Nodes. */ -export function childrenToReact(context, node) { +export function childrenToReact(state, 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] + const child = node.children[childIndex] if (child.type === 'element') { - children.push(toReact(context, child, childIndex, node)) + children.push(toReact(state, child, childIndex, node)) } else if (child.type === 'text') { // Currently, a warning is triggered by react for *any* white space in // tables. @@ -137,7 +123,7 @@ export function childrenToReact(context, node) { ) { children.push(child.value) } - } else if (child.type === 'raw' && !context.options.skipHtml) { + } else if (child.type === 'raw' && !state.options.skipHtml) { // Default behavior is to show (encoded) HTML. children.push(child.value) } @@ -147,21 +133,26 @@ export function childrenToReact(context, node) { } /** - * @param {Context} context - * @param {Element} node + * @param {State} state + * Info passed around. + * @param {Readonly} node + * Node to transform. * @param {number} index - * @param {Element|Root} parent + * Position of `node` in `parent`. + * @param {Readonly} parent + * Parent of `node`. + * @returns {ReactNode} + * Node. */ -function toReact(context, node, index, parent) { - const options = context.options +function toReact(state, node, index, parent) { + const options = state.options const transform = options.transformLinkUri === undefined ? uriTransformer : options.transformLinkUri - const parentSchema = context.schema - /** @type {ReactMarkdownNames} */ - // @ts-expect-error assume a known HTML/SVG element. - const name = node.tagName + const parentSchema = state.schema + // Assume a known HTML/SVG element. + const name = /** @type {keyof JSX.IntrinsicElements} */ (node.tagName) /** @type {Record} */ const properties = {} let schema = parentSchema @@ -170,36 +161,30 @@ function toReact(context, node, index, parent) { if (parentSchema.space === 'html' && name === 'svg') { schema = svg - context.schema = schema + state.schema = schema } if (node.properties) { for (property in node.properties) { if (own.call(node.properties, property)) { - addProperty(properties, property, node.properties[property], context) + addProperty(state, properties, property, node.properties[property]) } } } if (name === 'ol' || name === 'ul') { - context.listDepth++ + state.listDepth++ } - const children = childrenToReact(context, node) + const children = childrenToReact(state, node) if (name === 'ol' || name === 'ul') { - context.listDepth-- + state.listDepth-- } // Restore parent schema. - context.schema = parentSchema + state.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] @@ -218,6 +203,7 @@ function toReact(context, node, index, parent) { properties.href = transform( String(properties.href || ''), node.children, + // To do: pass `undefined`. typeof properties.title === 'string' ? properties.title : null ) } @@ -247,6 +233,7 @@ function toReact(context, node, index, parent) { properties.src = options.transformImageUri( String(properties.src || ''), String(properties.alt || ''), + // To do: pass `undefined`. typeof properties.title === 'string' ? properties.title : null ) } @@ -254,21 +241,30 @@ function toReact(context, node, index, parent) { if (!basic && name === 'li' && parent.type === 'element') { const input = getInputElement(node) properties.checked = - input && input.properties ? Boolean(input.properties.checked) : null + // To do: pass `undefined`. + input ? 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 + properties.depth = state.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 + let style = /** @type {Record | undefined} */ ( + properties.style + ) + + if (!style) { + style = {} + properties.style = style + } + + style.textAlign = String(properties.align) + delete properties.align } @@ -283,7 +279,7 @@ function toReact(context, node, index, parent) { // If `sourcePos` is given, pass source information (line/column info from markdown source). if (options.sourcePos) { - properties['data-sourcepos'] = flattenPosition(position) + properties['data-sourcepos'] = stringifyPosition(node) } if (!basic && options.rawSourcePos) { @@ -307,8 +303,10 @@ function toReact(context, node, index, parent) { } /** - * @param {Element|Root} node - * @returns {Element?} + * @param {Readonly} node + * Node to check. + * @returns {Element | undefined} + * `input` element, if found. */ function getInputElement(node) { let index = -1 @@ -320,35 +318,43 @@ function getInputElement(node) { return child } } - - return null } /** - * @param {Element|Root} parent - * @param {Element} [node] + * @param {Readonly} parent + * Node. + * @param {Readonly} [node] + * Node in parent (optional). * @returns {number} + * Siblings before `node`. */ 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++ + const child = parent.children[index] + if (child === node) break + if (child.type === 'element') count++ } return count } /** + * @param {State} state + * Info passed around. * @param {Record} props + * Properties. * @param {string} prop + * Property. * @param {unknown} value - * @param {Context} ctx + * Value. + * @returns {undefined} + * Nothing. */ -function addProperty(props, prop, value, ctx) { - const info = find(ctx.schema, prop) +function addProperty(state, props, prop, value) { + const info = find(state.schema, prop) let result = value // Ignore nullish and `NaN` values. @@ -380,7 +386,9 @@ function addProperty(props, prop, value, ctx) { /** * @param {string} value + * Style. * @returns {Record} + * Style. */ function parseStyle(value) { /** @type {Record} */ @@ -396,7 +404,9 @@ function parseStyle(value) { /** * @param {string} name + * Name. * @param {string} v + * Value. */ function iterator(name, v) { const k = name.slice(0, 4) === '-ms-' ? `ms-${name.slice(4)}` : name @@ -406,26 +416,12 @@ function parseStyle(value) { /** * @param {unknown} _ + * Whole match. * @param {string} $1 + * Letter. + * @returns {string} + * Replacement. */ 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(String) - .join('') -} diff --git a/lib/complex-types.d.ts b/lib/complex-types.d.ts index 5319a88b..b460319a 100644 --- a/lib/complex-types.d.ts +++ b/lib/complex-types.d.ts @@ -1,24 +1,32 @@ -import type {ReactNode, ComponentType, ComponentPropsWithoutRef} from 'react' -import type {Position} from 'unist' +// File for types which are not handled correctly in JSDoc mode. import type {Element} from 'hast' +import type {ComponentPropsWithoutRef, ComponentType, ReactNode} from 'react' +import type {Position} from 'unist' -/* File for types which are not handled correctly in JSDoc mode */ - +/** + * Props passed to components. + */ export type ReactMarkdownProps = { - node: Element - children: ReactNode[] /** - * Passed when `options.rawSourcePos` is given + * Passed when `options.sourcePos` is given. */ - sourcePosition?: Position + 'data-sourcepos': string | undefined /** * Passed when `options.includeElementIndex` is given */ index?: number + /** + * Original hast node. + */ + node: Element /** * Passed when `options.includeElementIndex` is given */ siblingCount?: number + /** + * Passed when `options.rawSourcePos` is given + */ + sourcePosition?: Position | undefined } export type NormalComponents = { diff --git a/lib/react-markdown.js b/lib/react-markdown.js index df97b321..2aeb1372 100644 --- a/lib/react-markdown.js +++ b/lib/react-markdown.js @@ -1,74 +1,151 @@ /** - * @typedef {import('react').ReactNode} ReactNode + * @typedef {import('hast').Element} Element + * @typedef {import('hast').ElementContent} ElementContent + * @typedef {import('hast').Parents} Parents + * @typedef {import('remark-rehype').Options} RemarkRehypeOptions * @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} [remarkPlugins=[]] - * @property {PluggableList} [rehypePlugins=[]] - * @property {import('remark-rehype').Options | undefined} [remarkRehypeOptions={}] - * - * @typedef LayoutOptions - * @property {string} [className] - * - * @typedef {CoreOptions & PluginOptions & LayoutOptions & FilterOptions & TransformOptions} ReactMarkdownOptions + * @typedef {import('./ast-to-react.js').Components} Components + */ + +/** + * @callback AllowElement + * Decide if `element` should be allowed. + * @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 Deprecation + * Deprecation. * @property {string} id + * ID in readme. * @property {string} [to] + * Field to use instead (optional). + * + * @typedef Options + * Configuration. + * @property {AllowElement | null | undefined} [allowElement] + * Function called to check if an element is allowed (when truthy) or not, + * `allowedElements` or `disallowedElements` is used first! + * @property {ReadonlyArray | null | undefined} [allowedElements] + * Tag names to allow (cannot combine w/ `disallowedElements`), all tag names + * are allowed by default. + * @property {string | null | undefined} [children] + * Markdown to parse. + * @property {string | null | undefined} [className] + * Wrap the markdown in a `div` with this class name. + * @property {Components | null | undefined} [components] + * Map tag names to React components. + * @property {ReadonlyArray | null | undefined} [disallowedElements] + * Tag names to disallow (cannot combine w/ `allowedElements`), all tag names + * are allowed by default. + * @property {boolean | null | undefined} [includeElementIndex=false] + * Pass the `index` (number of elements before it) and `siblingCount` (number + * of elements in parent) as props to all components (default: `false`). + * @property {boolean | null | undefined} [rawSourcePos=false] + * Pass a `sourcePosition` prop to all components with their position + * (default: `false`). + * @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} [sourcePos=false] + * Pass a `data-sourcepos` prop to all components with a serialized position + * (default: `false`). + * @property {TransformLink | false | null | undefined} [transformLinkUri] + * Change URLs on images (default: `uriTransformer`); + * pass `false` to allow all URLs, which is unsafe + * @property {TransformImage | false | null | undefined} [transformImageUri] + * Change URLs on links (default: `uriTransformer`); + * pass `false` to allow all URLs, which is unsafe + * @property {boolean | null | undefined} [unwrapDisallowed=false] + * Extract (unwrap) the children of not allowed elements (default: `false`); + * normally when say `strong` is disallowed, it and it’s children are dropped, + * with `unwrapDisallowed` the element itself is replaced by its children. + * + * @callback TransformImage + * Transform URLs on images. + * @param {string} src + * URL to transform. + * @param {string} alt + * Alt text. + * @param {string | null} title + * Title. + * To do: pass `undefined`. + * @returns {string | null | undefined} + * Transformed URL (optional). + * + * @callback TransformLink + * Transform URLs on links. + * @param {string} href + * URL to transform. + * @param {ReadonlyArray} children + * Content. + * @param {string | null} title + * Title. + * To do: pass `undefined`. + * @returns {string} + * Transformed URL (optional). */ 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 remarkParse from 'remark-parse' +import remarkRehype from 'remark-rehype' +import {unified} from 'unified' +import {VFile} from 'vfile' import {childrenToReact} from './ast-to-react.js' +import rehypeFilter from './rehype-filter.js' const own = {}.hasOwnProperty const changelog = 'https://github.com/remarkjs/react-markdown/blob/main/changelog.md' -/** @type {Record} */ +// Mutable because we `delete` any time it’s used and a message is sent. +/** @type {Record>} */ const deprecated = { - plugins: {to: 'remarkPlugins', id: 'change-plugins-to-remarkplugins'}, - 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' + id: 'replace-allownode-allowedtypes-and-disallowedtypes', + to: 'allowElement' }, allowedTypes: { - to: 'allowedElements', - id: 'replace-allownode-allowedtypes-and-disallowedtypes' + id: 'replace-allownode-allowedtypes-and-disallowedtypes', + to: 'allowedElements' }, disallowedTypes: { - to: 'disallowedElements', - id: 'replace-allownode-allowedtypes-and-disallowedtypes' + id: 'replace-allownode-allowedtypes-and-disallowedtypes', + to: 'disallowedElements' }, + escapeHtml: {id: 'remove-buggy-html-in-markdown-parser'}, includeNodeIndex: { - to: 'includeElementIndex', - id: 'change-includenodeindex-to-includeelementindex' - } + id: 'change-includenodeindex-to-includeelementindex', + to: 'includeElementIndex' + }, + plugins: {id: 'change-plugins-to-remarkplugins', to: 'remarkPlugins'}, + renderers: {id: 'change-renderers-to-components', to: 'components'}, + source: {id: 'change-source-to-children', to: 'children'} } /** - * React component to render markdown. + * Component to render markdown. * - * @param {ReactMarkdownOptions} options + * @param {Readonly} options + * Configuration (required). + * Note: React types require that props are passed. * @returns {ReactElement} + * React element. */ export function ReactMarkdown(options) { for (const key in deprecated) { @@ -97,7 +174,7 @@ export function ReactMarkdown(options) { if (typeof options.children === 'string') { file.value = options.children - } else if (options.children !== undefined && options.children !== null) { + } else if (options.children !== null && options.children !== undefined) { console.warn( `[react-markdown] Warning: please pass a string as \`children\` (not: \`${options.children}\`)` ) diff --git a/lib/rehype-filter.js b/lib/rehype-filter.js index 3d6a00d8..9da34f48 100644 --- a/lib/rehype-filter.js +++ b/lib/rehype-filter.js @@ -1,42 +1,42 @@ -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] + * @typedef {import('hast').Root} Root + * @typedef {import('./react-markdown.js').Options} Options */ +import {visit} from 'unist-util-visit' + /** - * @type {import('unified').Plugin<[Options], Root>} + * Filter nodes. + * + * @param {Readonly} options + * Configuration (required). + * @returns + * Transform (optional). */ export default function rehypeFilter(options) { - if (options.allowedElements && options.disallowedElements) { - throw new TypeError( - 'Only one of `allowedElements` and `disallowedElements` should be defined' - ) - } - if ( + options.allowElement || options.allowedElements || - options.disallowedElements || - options.allowElement + options.disallowedElements ) { - return (tree) => { - visit(tree, 'element', (node, index, parent_) => { - const parent = /** @type {Element|Root} */ (parent_) - /** @type {boolean|undefined} */ + if (options.allowedElements && options.disallowedElements) { + throw new TypeError( + 'Only one of `allowedElements` and `disallowedElements` should be defined' + ) + } + + /** + * Transform. + * + * @param {Root} tree + * Tree. + * @returns {undefined} + * Nothing. + */ + return function (tree) { + visit(tree, 'element', function (node, index, parent) { + /** @type {boolean | undefined} */ let remove if (options.allowedElements) { @@ -49,7 +49,7 @@ export default function rehypeFilter(options) { remove = !options.allowElement(node, index, parent) } - if (remove && typeof index === 'number') { + if (remove && parent && typeof index === 'number') { if (options.unwrapDisallowed && node.children) { parent.children.splice(index, 1, ...node.children) } else { diff --git a/lib/uri-transformer.js b/lib/uri-transformer.js index 0bcfa5bb..eca407fb 100644 --- a/lib/uri-transformer.js +++ b/lib/uri-transformer.js @@ -1,11 +1,15 @@ const protocols = ['http', 'https', 'mailto', 'tel'] /** - * @param {string} uri + * Make a URL safe. + * + * @param {string} value + * URL. * @returns {string} + * Safe URL. */ -export function uriTransformer(uri) { - const url = (uri || '').trim() +export function uriTransformer(value) { + const url = (value || '').trim() const first = url.charAt(0) if (first === '#' || first === '/') { @@ -40,6 +44,7 @@ export function uriTransformer(uri) { return url } + // To do: is there an alternative? // eslint-disable-next-line no-script-url return 'javascript:void(0)' } diff --git a/package.json b/package.json index 6f0526c5..459523e5 100644 --- a/package.json +++ b/package.json @@ -84,6 +84,7 @@ "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", + "mdast-util-to-hast": "^13.0.2", "prop-types": "^15.0.0", "property-information": "^6.0.0", "react-is": "^18.0.0", @@ -92,6 +93,7 @@ "space-separated-tokens": "^2.0.0", "style-to-object": "^0.4.0", "unified": "^11.0.0", + "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, @@ -106,7 +108,6 @@ "@types/react-is": "^18.0.0", "c8": "^8.0.0", "esbuild": "^0.19.0", - "eslint-config-xo-react": "^0.27.0", "eslint-plugin-es": "^4.0.0", "eslint-plugin-react": "^7.0.0", "eslint-plugin-react-hooks": "^4.0.0", @@ -172,7 +173,7 @@ "envs": [ "shared-node-browser" ], - "extends": "xo-react", + "extends": "plugin:react/jsx-runtime", "overrides": [ { "files": [ @@ -193,7 +194,7 @@ ], "rules": { "n/file-extension-in-import": "off", - "react/no-children-prop": "off" + "no-unused-vars": "off" } } ], diff --git a/readme.md b/readme.md index 30be3fdc..acb8265d 100644 --- a/readme.md +++ b/readme.md @@ -167,48 +167,48 @@ The default export is `ReactMarkdown`. ### `props` -* `children` (`string`, 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! +* `allowedElements` (`Array`, optional)\ + tag names to allow (cannot combine w/ `disallowedElements`), all tag names + are allowed by default +* `children` (`string`, optional)\ 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 -* `remarkRehypeOptions` (`Object?`, default: `undefined`)\ - options to pass through to [`remark-rehype`][remark-rehype] * `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:…}`) +* `components` (`Record`, optional)\ + map tag names to React components +* `disallowedElements` (`Array`, optional)\ + tag names to disallow (cannot combine w/ `allowedElements`), all tag names + are allowed by default * `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 -* `transformLinkUri` (`(href, children, title) => string`, default: - [`uriTransformer`][uri-transformer], optional)\ - change URLs on links, pass `null` to allow all URLs, see [security][] +* `rawSourcePos` (`boolean`, default: `false`)\ + pass a `sourcePosition` prop to all components with their [position][] +* `rehypePlugins` (`Array`, optional)\ + list of [rehype plugins][rehype-plugins] to use +* `remarkPlugins` (`Array`, optional)\ + list of [remark plugins][remark-plugins] to use +* `remarkRehypeOptions` (`Object?`, optional)\ + options to pass through to [`remark-rehype`][remark-rehype] +* `skipHtml` (`boolean`, default: `false`)\ + ignore HTML in markdown completely +* `sourcePos` (`boolean`, default: `false`)\ + pass a `data-sourcepos` prop to all components with a serialized position * `transformImageUri` (`(src, alt, title) => string`, default: - [`uriTransformer`][uri-transformer], optional)\ - change URLs on images, pass `null` to allow all URLs, see [security][] + [`uriTransformer`][uri-transformer])\ + change URLs on images; + pass `false` to allow all URLs, which is unsafe (see [security][]) +* `transformLinkUri` (`(href, children, title) => string`, default: + [`uriTransformer`][uri-transformer])\ + change URLs on links; + pass `false` to allow all URLs, which is unsafe (see [security][]) +* `unwrapDisallowed` (`boolean`, default: `false`)\ + extract (unwrap) the children of not allowed elements; + normally when say `strong` is disallowed, it and it’s children are dropped, + with `unwrapDisallowed` the element itself is replaced by its children ### `uriTransformer` @@ -348,18 +348,19 @@ ReactDom.render( ) : ( - + {children} ) @@ -549,11 +550,15 @@ You can also change the things that come from markdown: ```jsx + em(props) { + const {node, ...rest} = props + return + } }} /> ``` diff --git a/test/fixtures/runthrough.html b/test/fixtures/runthrough.html deleted file mode 100644 index 824af77d..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?

- -

We used to have a known bug where inline HTML wasn't handled well. You can do basic tags like -code, 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 7fec387f..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? - - - -We used to have a known bug where inline HTML wasn't handled well. You can do basic tags like -code, 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? diff --git a/test/loader.js b/test/loader.js index 28915afc..d78a33c7 100644 --- a/test/loader.js +++ b/test/loader.js @@ -1,10 +1,10 @@ import fs from 'node:fs/promises' import {fileURLToPath} from 'node:url' -import {transform, transformSync} from 'esbuild' +import {transform} from 'esbuild' -const {load, getFormat, transformSource} = createLoader() +const {getFormat, load, transformSource} = createLoader() -export {load, getFormat, transformSource} +export {getFormat, load, transformSource} /** * A tiny JSX loader. @@ -26,21 +26,21 @@ export function createLoader() { } const {code, warnings} = await transform(String(await fs.readFile(url)), { + format: 'esm', + loader: 'jsx', sourcefile: fileURLToPath(url), sourcemap: 'both', - loader: 'jsx', - target: 'esnext', - format: 'esm' + target: 'esnext' }) - if (warnings && warnings.length > 0) { + if (warnings) { for (const warning of warnings) { console.log(warning.location) console.log(warning.text) } } - return {format: 'module', source: code, shortCircuit: true} + return {format: 'module', shortCircuit: true, source: code} } // Pre version 17. @@ -69,15 +69,15 @@ export function createLoader() { return defaultTransformSource(value, context, defaultTransformSource) } - const {code, warnings} = transformSync(String(value), { + const {code, warnings} = await transform(String(value), { + format: context.format === 'module' ? 'esm' : 'cjs', + loader: 'jsx', sourcefile: fileURLToPath(url), sourcemap: 'both', - loader: 'jsx', - target: 'esnext', - format: context.format === 'module' ? 'esm' : 'cjs' + target: 'esnext' }) - if (warnings && warnings.length > 0) { + if (warnings) { for (const warning of warnings) { console.log(warning.location) console.log(warning.text) diff --git a/test/test.jsx b/test/test.jsx index b21bfb7a..fbc38f6c 100644 --- a/test/test.jsx +++ b/test/test.jsx @@ -1,1431 +1,1216 @@ +/* @jsxRuntime automatic @jsxImportSource react */ /** * @typedef {import('hast').Root} Root - * @typedef {import('hast').Element} Element - * @typedef {import('react').ReactNode} ReactNode + * @typedef {import('../lib/ast-to-react.js').HeadingProps} HeadingProps */ import assert from 'node:assert/strict' -import fs from 'node:fs/promises' import test from 'node:test' -import React from 'react' -import ReactDom from 'react-dom/server' +import {renderToStaticMarkup} from 'react-dom/server' import rehypeRaw from 'rehype-raw' import remarkGfm from 'remark-gfm' import remarkToc from 'remark-toc' import {visit} from 'unist-util-visit' import Markdown from '../index.js' -const own = {}.hasOwnProperty - -/** - * @param {ReturnType} input - * @returns {string} - */ -function asHtml(input) { - return ReactDom.renderToStaticMarkup(input) -} - -test('ReactMarkdown', async () => { - assert.deepEqual( - Object.keys(await import('../index.js')).sort(), - ['default', 'uriTransformer'], - 'should expose the public api' - ) -}) - -test('can render the most basic of documents (single paragraph)', () => { - assert.equal(asHtml(Test), '

Test

') -}) - -test('should warn when passed `source`', () => { - const warn = console.warn - /** @type {unknown} */ - let message - - console.warn = (/** @type {unknown} */ d) => { - message = d - } - - // @ts-expect-error runtime - assert.equal(asHtml(b), '

b

') - assert.equal( - message, - '[react-markdown] Warning: please use `children` instead of `source` (see for more info)' - ) - - console.warn = warn -}) - -test('should warn when passed non-string children (number)', () => { - const {error, warn} = console - /** @type {unknown} */ - let message - - console.error = () => {} - console.warn = (/** @type {unknown} */ d) => { - message = d - } - - // @ts-expect-error runtime - assert.equal(asHtml(), '') - assert.equal( - message, - '[react-markdown] Warning: please pass a string as `children` (not: `1`)' - ) - - console.error = error - console.warn = warn -}) +test('react-markdown', async function (t) { + await t.test('should expose the public api', async function () { + assert.deepEqual(Object.keys(await import('../index.js')).sort(), [ + 'default', + 'uriTransformer' + ]) + }) + + await t.test('should work', function () { + assert.equal(asHtml(a), '

a

') + }) + + await t.test('should warn w/ `source`', function () { + const warn = console.warn + /** @type {unknown} */ + let message + + console.warn = capture + + // @ts-expect-error: check how the runtime handles untyped `source`. + assert.equal(asHtml(b), '

b

') + assert.equal( + message, + '[react-markdown] Warning: please use `children` instead of `source` (see for more info)' + ) -test('should warn when passed non-string children (boolean)', () => { - const {error, warn} = console - /** @type {unknown} */ - let message - - console.error = () => {} - console.warn = (/** @type {unknown} */ d) => { - message = d - } - - // @ts-expect-error runtime - assert.equal(asHtml(), '') - assert.equal( - message, - '[react-markdown] Warning: please pass a string as `children` (not: `false`)' - ) + console.warn = warn - console.error = error - console.warn = warn -}) + /** + * @param {unknown} d + * @returns {undefined} + */ + function capture(d) { + message = d + } + }) -test('should not warn when passed `null` as children', () => { - // @ts-expect-error: types do not allow `null`. - assert.equal(asHtml(), '') -}) + await t.test('should warn w/ non-string children (number)', function () { + const {error, warn} = console + /** @type {unknown} */ + let message -test('should not warn when passed `undefined` as children', () => { - // @ts-expect-error: types do not allow `undefined`. - assert.equal(asHtml(), '') -}) + console.error = function () {} + console.warn = capture -test('should warn when passed `allowDangerousHtml`', () => { - const warn = console.warn - /** @type {unknown} */ - let message + // @ts-expect-error: check how the runtime handles invalid `children`. + assert.equal(asHtml(), '') + assert.equal( + message, + '[react-markdown] Warning: please pass a string as `children` (not: `1`)' + ) - console.warn = (/** @type {unknown} */ d) => { - message = d - } + console.error = error + console.warn = warn - // @ts-expect-error runtime - assert.equal(asHtml(a), '

a

') - assert.equal( - message, - '[react-markdown] Warning: please remove `allowDangerousHtml` (see for more info)' - ) + /** + * @param {unknown} d + * @returns {undefined} + */ + function capture(d) { + message = d + } + }) - console.warn = warn -}) + await t.test('should warn w/ non-string children (boolean)', function () { + const {error, warn} = console + /** @type {unknown} */ + let message -test('uses passed classname for root component', () => { - assert.equal( - asHtml(Test), - '

Test

' - ) -}) + console.error = function () {} + console.warn = capture -test('should handle multiple paragraphs properly', () => { - const input = 'React is awesome\nAnd so is markdown\n\nCombining = epic' - assert.equal( - asHtml(), - '

React is awesome\nAnd so is markdown

\n

Combining = epic

' - ) -}) + // @ts-expect-error: check how the runtime handles invalid `children`. + assert.equal(asHtml(), '') + assert.equal( + message, + '[react-markdown] Warning: please pass a string as `children` (not: `false`)' + ) -test('should handle multiline paragraphs properly (softbreak, paragraphs)', () => { - const input = 'React is awesome\nAnd so is markdown \nCombining = epic' - const actual = asHtml() - assert.equal( - actual, - '

React is awesome\nAnd so is markdown
\nCombining = epic

' - ) -}) + console.error = error + console.warn = warn -test('should handle emphasis', () => { - const input = 'React is _totally_ *awesome*' - const actual = asHtml() - assert.equal(actual, '

React is totally awesome

') -}) + /** + * @param {unknown} d + * @returns {undefined} + */ + function capture(d) { + message = d + } + }) -test('should handle bold/strong text', () => { - const input = 'React is __totally__ **awesome**' - const actual = asHtml() - assert.equal( - actual, - '

React is totally awesome

' - ) -}) + await t.test('should support `null` as children', function () { + assert.equal(asHtml(), '') + }) -test('should handle links without title attribute', () => { - const input = 'This is [a link](https://espen.codes/) to Espen.Codes.' - const actual = asHtml() - assert.equal( - actual, - '

This is a link to Espen.Codes.

' - ) -}) + await t.test('should support `undefined` as children', function () { + assert.equal(asHtml(), '') + }) -test('should handle links with title attribute', () => { - const input = - 'This is [a link](https://espen.codes/ "some title") to Espen.Codes.' - const actual = asHtml() - assert.equal( - actual, - '

This is a link to Espen.Codes.

' - ) -}) + await t.test('should warn w/ `allowDangerousHtml`', function () { + const warn = console.warn + /** @type {unknown} */ + let message -test('should handle links with uppercase protocol', () => { - const input = 'This is [a link](HTTPS://ESPEN.CODES/) to Espen.Codes.' - const actual = asHtml() - assert.equal( - actual, - '

This is a link to Espen.Codes.

' - ) -}) + console.warn = capture -test('should handle links with custom uri transformer', () => { - const input = 'This is [a link](https://espen.codes/) to Espen.Codes.' - const actual = asHtml( - uri.replace(/^https?:/, '')} - /> - ) - assert.equal( - actual, - '

This is a link to Espen.Codes.

' - ) -}) + // @ts-expect-error: check how the runtime handles deprecated `allowDangerousHtml`. + assert.equal(asHtml(a), '

a

') + assert.equal( + message, + '[react-markdown] Warning: please remove `allowDangerousHtml` (see for more info)' + ) -test('should handle empty links with custom uri transformer', () => { - const input = 'Empty: []()' - - const actual = asHtml( - { - assert.equal(uri, '', '`uri` should be an empty string') - assert.equal(title, null, '`title` should be null') - return '' - }} - /> - ) + console.warn = warn - assert.equal(actual, '

Empty:

') -}) + /** + * @param {unknown} d + * @returns {undefined} + */ + function capture(d) { + message = d + } + }) -test('should handle titles of links', () => { - const input = 'Empty: [](# "x")' - const actual = asHtml() - assert.equal(actual, '

Empty:

') -}) + await t.test('should support `className`', function () { + assert.equal( + asHtml(a), + '

a

' + ) + }) -test('should support images without alt, url, or title', () => { - const input = '![]()' - const actual = asHtml() - const expected = '

' - assert.equal(actual, expected) -}) + await t.test('should support a block quote', function () { + assert.equal( + asHtml(), + '
\n

a

\n
' + ) + }) -test('should handle images without title attribute', () => { - const input = 'This is ![an image](/ninja.png).' - const actual = asHtml() - assert.equal(actual, '

This is an image.

') -}) + await t.test('should support a break', function () { + assert.equal(asHtml(), '

a
\nb

') + }) -test('should handle images with title attribute', () => { - const input = 'This is ![an image](/ninja.png "foo bar").' - const actual = asHtml() - assert.equal( - actual, - '

This is an image.

' - ) -}) + await t.test('should support a code (block, flow; indented)', function () { + assert.equal( + asHtml(), + '
a\n
' + ) + }) -test('should handle images with custom uri transformer', () => { - const input = 'This is ![an image](/ninja.png).' - const actual = asHtml( - uri.replace(/\.png$/, '.jpg')} - /> - ) - assert.equal(actual, '

This is an image.

') -}) + await t.test('should support a code (block, flow; fenced)', function () { + assert.equal( + asHtml(), + '
a\n
' + ) + }) -test('should handle images with custom uri transformer', () => { - const input = 'Empty: ![]()' - const actual = asHtml( - { - assert.equal(uri, '', '`uri` should be an empty string') - assert.equal(alt, '', '`alt` should be an empty string') - assert.equal(title, null, '`title` should be null') - return '' - }} - /> - ) - assert.equal(actual, '

Empty:

') -}) + await t.test('should support a delete (GFM)', function () { + assert.equal( + asHtml(), + '

a

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

a

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

a1

\n

Footnotes

\n
    \n
  1. \n

    y

    \n
  2. \n
\n
' + ) + }) -test('should handle images w/ titles with custom uri transformer', () => { - const input = 'Empty: ![](a "b")' - const actual = asHtml( - { - assert.equal(title, 'b', '`title` should be passed') - return src - }} - /> - ) - assert.equal(actual, '

Empty:

') -}) + await t.test('should support a heading', function () { + assert.equal(asHtml(), '

a

') + }) -test('should handle image references with custom uri transformer', () => { - const input = - 'This is ![The Waffle Ninja][ninja].\n\n[ninja]: https://some.host/img.png' - const actual = asHtml( - uri.replace(/\.png$/, '.jpg')} - /> - ) - assert.equal( - actual, - '

This is The Waffle Ninja.

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

<i>a</i>

' + ) + }) -test('should support images references without alt, url, or title', () => { - const input = '![][a]\n\n[a]: <>' - const actual = asHtml() - const expected = '

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

a

' + ) + }) -test('should handle images with special characters in alternative text', () => { - const input = "This is ![a ninja's image](/ninja.png)." - const actual = asHtml() - assert.equal( - actual, - '

This is a ninja's image.

' - ) -}) + await t.test('should support an image', function () { + assert.equal( + asHtml(), + '

a

' + ) + }) -test('should be able to render headers', () => { - assert.equal(asHtml(), '

Awesome

') - assert.equal(asHtml(), '

Awesome

') - assert.equal(asHtml(), '

Awesome

') - assert.equal(asHtml(), '

Awesome

') - assert.equal( - asHtml(), - '
Awesome
' - ) -}) + await t.test('should support an image w/ a title', function () { + assert.equal( + asHtml(), + '

a

' + ) + }) -test('should be able to render inline code', () => { - const input = 'Just call `renderToStaticMarkup()`, already' - const actual = asHtml() - assert.equal( - actual, - '

Just call renderToStaticMarkup(), already

' - ) -}) + await t.test('should support an image reference / definition', function () { + assert.equal( + asHtml(), + '

a

' + ) + }) -test('should handle code tags without any language specification', () => { - const input = "```\nvar foo = require('bar');\nfoo();\n```" - const actual = asHtml() - assert.equal( - actual, - '
var foo = require('bar');\nfoo();\n
' - ) -}) + await t.test('should support code (text, inline)', function () { + assert.equal(asHtml(), '

a

') + }) -test('should handle code tags with language specification', () => { - const input = "```js\nvar foo = require('bar');\nfoo();\n```" - const actual = asHtml() - assert.equal( - actual, - '
var foo = require('bar');\nfoo();\n
' - ) -}) + await t.test('should support a link', function () { + assert.equal( + asHtml(), + '

a

' + ) + }) -test('should only use first language definition on code blocks', () => { - const input = "```js foo bar\nvar foo = require('bar');\nfoo();\n```" - const actual = asHtml() - assert.equal( - actual, - '
var foo = require('bar');\nfoo();\n
' - ) -}) + await t.test('should support a link w/ a title', function () { + assert.equal( + asHtml(), + '

a

' + ) + }) -test('should support character references in code blocks', () => { - const input = `~~~js ololo i can haz class names !@#$%^&*()_ - woop - ~~~` - const actual = asHtml() - assert.equal( - actual, - '
  woop\n
' - ) -}) + await t.test('should support a link reference / definition', function () { + assert.equal( + asHtml(), + '

a

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

' + ) + }) -test('should handle code blocks by indentation', () => { - const input = [ - '', - '
\n', - '', - '© 2014 Foo Bar\n', - '
' - ].join(' ') - assert.equal( - asHtml(), - '
<footer class="footer">\n    &copy; 2014 Foo Bar\n</footer>\n
' - ) -}) + await t.test('should support duplicate definitions', function () { + assert.equal( + asHtml(), + '

a

' + ) + }) -test('should handle blockquotes', () => { - const input = '> Moo\n> Tools\n> FTW\n' - const actual = asHtml() - assert.equal(actual, '
\n

Moo\nTools\nFTW

\n
') -}) + await t.test('should support a list (unordered) / list item', function () { + assert.equal(asHtml(), '
    \n
  • a
  • \n
') + }) -test('should handle nested blockquotes', () => { - const input = [ - '> > Lots of ex-Mootoolers on the React team\n>\n', - "> Totally didn't know that.\n>\n", - "> > There's a reason why it turned out so awesome\n>\n", - "> Haha I guess you're right!" - ].join('') - - const actual = asHtml() - assert.equal( - actual, - '
\n
\n

Lots of ex-Mootoolers on the React team

\n
\n

Totally didn't know that.

\n
\n

There's a reason why it turned out so awesome

\n
\n

Haha I guess you're right!

\n
' - ) -}) + await t.test('should support a list (ordered) / list item', function () { + assert.equal( + asHtml(), + '
    \n
  1. a
  2. \n
' + ) + }) -test('should handle tight, unordered lists', () => { - const input = '* Unordered\n* Lists\n* Are cool\n' - const actual = asHtml() - assert.equal( - actual, - '
    \n
  • Unordered
  • \n
  • Lists
  • \n
  • Are cool
  • \n
' - ) -}) + await t.test('should support a paragraph', function () { + assert.equal(asHtml(), '

a

') + }) -test('should handle loose, unordered lists', () => { - const input = '- foo\n\n- bar' - const actual = asHtml() - assert.equal( - actual, - '
    \n
  • \n

    foo

    \n
  • \n
  • \n

    bar

    \n
  • \n
' - ) -}) + await t.test('should support a strong', function () { + assert.equal( + asHtml(), + '

a

' + ) + }) + + await t.test('should support a table (GFM)', function () { + assert.equal( + asHtml( + + ), + '
a
b
' + ) + }) + + await t.test('should support a table (GFM; w/ align)', function () { + assert.equal( + asHtml( + + ), + '
abcd
' + ) + }) -test('should handle tight, unordered lists with sublists', () => { - const input = '* Unordered\n * Lists\n * Are cool\n' - const actual = asHtml() - assert.equal( - actual, - '
    \n
  • Unordered\n
      \n
    • Lists\n
        \n
      • Are cool
      • \n
      \n
    • \n
    \n
  • \n
' - ) -}) + await t.test('should support a thematic break', function () { + assert.equal(asHtml(), '
') + }) -test('should handle loose, unordered lists with sublists', () => { - const input = '- foo\n\n - bar' - const actual = asHtml() - assert.equal( - actual, - '
    \n
  • \n

    foo

    \n
      \n
    • bar
    • \n
    \n
  • \n
' - ) -}) + await t.test('should support ab absolute path', function () { + assert.equal( + asHtml(), + '

' + ) + }) -test('should handle ordered lists', () => { - const input = '1. Ordered\n2. Lists\n3. Are cool\n' - const actual = asHtml() - assert.equal( - actual, - '
    \n
  1. Ordered
  2. \n
  3. Lists
  4. \n
  5. Are cool
  6. \n
' - ) -}) + await t.test('should support an absolute URL', function () { + assert.equal( + asHtml(), + '

' + ) + }) -test('should handle ordered lists with a start index', () => { - const input = '7. Ordered\n8. Lists\n9. Are cool\n' - const actual = asHtml() - assert.equal( - actual, - '
    \n
  1. Ordered
  2. \n
  3. Lists
  4. \n
  5. Are cool
  6. \n
' - ) -}) + await t.test('should support a URL w/ uppercase protocol', function () { + assert.equal( + asHtml(), + '

' + ) + }) + + await t.test('should make a `javascript:` URL safe', function () { + const consoleError = console.error + console.error = noop + assert.equal( + asHtml(), + '

' + ) + console.error = consoleError + }) + + await t.test('should make a `vbscript:` URL safe', function () { + const consoleError = console.error + console.error = noop + assert.equal( + asHtml(), + '

' + ) + console.error = consoleError + }) + + await t.test('should make a `VBSCRIPT:` URL safe', function () { + const consoleError = console.error + console.error = noop + assert.equal( + asHtml(), + '

' + ) + console.error = consoleError + }) + + await t.test('should make a `file:` URL safe', function () { + const consoleError = console.error + console.error = noop + assert.equal( + asHtml(), + '

' + ) + console.error = consoleError + }) -test('should pass `ordered`, `depth`, `checked`, `index` to list/listItem', () => { - const input = '- foo\n\n 2. bar\n 3. baz\n\n- root\n' - const actual = asHtml( - = 0, true) - return React.createElement('li', props) - }, - ol({node, ordered, depth, ...props}) { - assert.equal(ordered, true) - assert.equal(depth >= 0, true) - return React.createElement('ol', props) - }, - ul({node, ordered, depth, ...props}) { - assert.equal(ordered, false) - assert.equal(depth >= 0, true) - return React.createElement('ul', props) - } - }} - /> - ) + await t.test('should allow an empty URL', function () { + assert.equal(asHtml(), '

') + }) - assert.equal( - actual, - '
    \n
  • \n

    foo

    \n
      \n
    1. bar
    2. \n
    3. baz
    4. \n
    \n
  • \n
  • \n

    root

    \n
  • \n
' - ) -}) + await t.test('should support search (`?`) in a URL', function () { + assert.equal( + asHtml(), + '

' + ) + }) -test('should pass `inline: true` to inline code', () => { - const input = '```\na\n```\n\n\tb\n\n`c`' - const actual = asHtml( - - ) - const expected = - '
a\n
\n
b\n
\n

c

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

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

a

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

' + ) + }) -test('should pass `isHeader: boolean` to `tr`s', () => { - const input = '| a |\n| - |\n| b |\n| c |' - const actual = asHtml( - + await t.test( + 'should support turning off `transformLinkUri` (dangerous)', + function () { + assert.equal( + asHtml( + + ), + '

' + ) + } ) - const expected = - '
a
b
c
' - assert.equal(actual, expected) -}) -test('should pass `isHeader: true` to `th`s, `isHeader: false` to `td`s', () => { - const input = '| a |\n| - |\n| b |\n| c |' - const actual = asHtml( - - ) - const expected = - '
a
b
c
' - assert.equal(actual, expected) -}) + await t.test('should support `transformImageUri`', function () { + assert.equal( + asHtml( + + ), + '

a

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

' + ) + }) -test('should pass `index: number`, `ordered: boolean`, `checked: boolean | null` to `li`s', () => { - const input = '* [x] a\n* [ ] b\n* c' - let count = 0 - const actual = asHtml( - + await t.test( + 'should support turning off `transformImageUri` (dangerous)', + function () { + assert.equal( + asHtml( + + ), + '

' + ) + } ) - const expected = - '
    \n
  • a
  • \n
  • b
  • \n
  • c
  • \n
' - assert.equal(actual, expected) -}) -test('should pass `level: number` to `h1`, `h2`, ...', () => { - const input = '#\n##\n###' - - /** - * @param {object} props - * @param {Element} props.node - * @param {number} props.level - */ - function heading({node, level, ...props}) { - return React.createElement(`h${level}`, props) - } - - const actual = asHtml( - - ) - const expected = '

\n

\n

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

abc

') + }) -test('should skip inline html with skipHtml option enabled', () => { - const input = 'I am having so much fun' - const actual = asHtml() - assert.equal(actual, '

I am having so much fun

') -}) + await t.test('should support `sourcePos`', function () { + assert.equal( + asHtml(), + '

a

' + ) + }) -test('should escape html blocks by default', () => { - const input = [ - 'This is a regular paragraph.\n\n\n \n ', - '\n \n
Foo
\n\nThis is another', - ' regular paragraph.' - ].join('') - - const actual = asHtml() - assert.equal( - actual, - '

This is a regular paragraph.

\n<table>\n <tr>\n <td>Foo</td>\n </tr>\n</table>\n

This is another regular paragraph.

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

\n
    \n
  • b
  • \n
' + ) + } ) -}) -test('should skip html blocks if skipHtml prop is set', () => { - const input = [ - 'This is a regular paragraph.\n\n\n \n ', - '\n \n
Foo
\n\nThis is another', - ' regular paragraph.' - ].join('') - - const actual = asHtml() - assert.equal( - actual, - '

This is a regular paragraph.

\n\n

This is another regular paragraph.

' - ) -}) + await t.test('should support `allowedElements` as a function', function () { + assert.equal( + asHtml( + + ), + '

b

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

\n
    \n
  • b
  • \n
' + ) + }) -test('should escape html blocks by default (with HTML parser plugin)', () => { - const input = [ - 'This is a regular paragraph.\n\n\n \n ', - '\n \n
Foo
\n\nThis is another', - ' regular paragraph.' - ].join('') - - const actual = asHtml() - assert.equal( - actual, - '

This is a regular paragraph.

\n<table>\n <tr>\n <td>Foo</td>\n </tr>\n</table>\n

This is another regular paragraph.

' + await t.test( + 'should fail for both `allowedElements` and `disallowedElements`', + function () { + assert.throws(function () { + asHtml( + + ) + }, /only one of/i) + } ) -}) - -test('should handle horizontal rules', () => { - const input = 'Foo\n\n------------\n\nBar' - const actual = asHtml() - assert.equal(actual, '

Foo

\n
\n

Bar

') -}) -test('should set source position attributes if sourcePos option is enabled', () => { - const input = 'Foo\n\n------------\n\nBar' - const actual = asHtml() - assert.equal( - actual, - '

Foo

\n
\n

Bar

' + await t.test( + 'should support `unwrapDisallowed` w/ `allowedElements`', + function () { + assert.equal( + asHtml( + + ), + '

a

' + ) + } ) -}) -test('should pass on raw source position to non-tag components if rawSourcePos option is enabled', () => { - const input = '*Foo*\n\n------------\n\n__Bar__' - const actual = asHtml( - - } - }} - /> + await t.test( + 'should support `unwrapDisallowed` w/ `disallowedElements`', + function () { + assert.equal( + asHtml( + + ), + '

a

' + ) + } ) - assert.equal( - actual, - '

Foo

\n
\n

Bar

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

1

\n

Footnotes

\n
    \n
  1. \n

    a

    \n
  2. \n
\n
' + ) + }) -test('should pass on raw source position to non-tag components if rawSourcePos option is enabled and `rehype-raw` is used', () => { - const input = '*Foo*' - asHtml( - - ) -}) + await t.test('should support `components`', function () { + assert.equal( + asHtml(), + '

a

' + ) + }) + + await t.test('should support `components` as functions', function () { + assert.equal( + asHtml( + + } + }} + /> + ), + '
a
' + ) + }) + + await t.test('should fail on invalid component', function () { + assert.throws(function () { + asHtml( + + ) + }, /Component for name `h1`/) + }) + + await t.test('should support `components` (headings; `level`)', function () { + let calls = 0 + + assert.equal( + asHtml( + + ), + '

a

\n

b

' + ) -test('should skip nodes that are not defined as allowed', () => { - const input = - '# Header\n\nParagraph\n## New header\n1. List item\n2. List item 2' - const actual = asHtml( - - ) - assert.equal( - actual, - '\n

Paragraph

\n\n
    \n
  1. List item
  2. \n
  3. List item 2
  4. \n
' - ) -}) + assert.equal(calls, 2) -test('should skip nodes that are defined as disallowed', () => { - const input = - '# Header\n\nParagraph\n## New header\n1. List item\n2. List item 2\n\nFoo' - const actual = asHtml( - - ) - assert.equal( - actual, - '

Header

\n

Paragraph

\n

New header

\n
    \n\n\n
\n

Foo

' - ) -}) + /** + * @param {HeadingProps} props + */ + function heading(props) { + const {level, node, ...rest} = props + assert.equal(typeof level, 'number') + calls++ + const H = `h${level}` + return + } + }) + + await t.test('should support `components` (code; `inline`)', function () { + let calls = 0 + assert.equal( + asHtml( + + } + }} + /> + ), + '
a\n
\n
b\n
\n

c

' + ) -test('should unwrap child nodes from disallowed nodes, if unwrapDisallowed option is enabled', () => { - const input = - 'Espen *~~initiated~~ had the initial commit*, but has had several **contributors**' - const actual = asHtml( - - ) - assert.equal( - actual, - '

Espen initiated had the initial commit, but has had several contributors

' - ) -}) + assert.equal(calls, 3) + }) -test('should render tables', () => { - const input = [ - 'Languages are fun, right?', - '', - '| ID | English | Norwegian | Italian |', - '| :-- | :-----: | --------: | ------- |', - '| 1 | one | en | uno |', - '| 2 | two | to | due |', - '| 3 | three | tre | tre |', - '' - ].join('\n') - - assert.equal( - asHtml(), - '

Languages are fun, right?

\n
IDEnglishNorwegianItalian
1oneenuno
2twotodue
3threetretre
' - ) -}) + await t.test( + 'should support `components` (li; `checked`, `index`, `ordered`)', + function () { + let calls = 0 -test('should render partial tables', () => { - const input = 'User is writing a table by hand\n\n| Test | Test |\n|-|-|' + assert.equal( + asHtml( + = 0, true) + calls++ + return
  • + } + }} + remarkPlugins={[remarkGfm]} + /> + ), + '
      \n
    • a
    • \n
    \n
      \n
    1. b
    2. \n
    ' + ) - assert.equal( - asHtml(), - '

    User is writing a table by hand

    \n
    TestTest
    ' + assert.equal(calls, 2) + } ) -}) -test('should render link references', () => { - const input = [ - 'Stuff were changed in [1.1.4]. Check out the changelog for reference.', - '', - '[1.1.4]: https://github.com/remarkjs/react-markdown/compare/v1.1.3...v1.1.4' - ].join('\n') - - assert.equal( - asHtml(), - '

    Stuff were changed in 1.1.4. Check out the changelog for reference.

    ' - ) -}) + await t.test( + 'should support `components` (ol; `depth`, `ordered`)', + function () { + let calls = 0 -test('should render empty link references', () => { - const input = - 'Stuff were changed in [][]. Check out the changelog for reference.' + assert.equal( + asHtml( + + } + }} + /> + ), + '
      \n
    1. a
    2. \n
    ' + ) - assert.equal( - asHtml(), - '

    Stuff were changed in [][]. Check out the changelog for reference.

    ' + assert.equal(calls, 1) + } ) -}) -test('should render image references', () => { - const input = [ - 'Checkout out this ninja: ![The Waffle Ninja][ninja]. Pretty neat, eh?', - '', - '[ninja]: /assets/ninja.png' - ].join('\n') + await t.test( + 'should support `components` (ul; `depth`, `ordered`)', + function () { + let calls = 0 - assert.equal( - asHtml(), - '

    Checkout out this ninja: The Waffle Ninja. Pretty neat, eh?

    ' - ) -}) + assert.equal( + asHtml( + + } + }} + /> + ), + '
      \n
    • a
    • \n
    ' + ) -test('should render footnote with custom options', () => { - const input = [ - 'This is a statement[^1] with a citation.', - '', - '[^1]: This is a footnote for the citation.' - ].join('\n') - - assert.equal( - asHtml( - - ), - '

    This is a statement1 with a citation.

    \n

    Footnotes

    \n
      \n
    1. \n

      This is a footnote for the citation.

      \n
    2. \n
    \n
    ' + assert.equal(calls, 1) + } ) -}) - -test('should support definitions with funky keys', () => { - const input = - '[][__proto__] and [][constructor]\n\n[__proto__]: a\n[constructor]: b' - const actual = asHtml() - const expected = '

    and

    ' - assert.equal(actual, expected) -}) -test('should support duplicate definitions', () => { - const input = '[a][]\n\n[a]: b\n[a]: c' - const actual = asHtml() - const expected = '

    a

    ' - assert.equal(actual, expected) -}) + await t.test('should support `components` (tr; `isHeader`)', function () { + let calls = 0 + + assert.equal( + asHtml( + + } + }} + remarkPlugins={[remarkGfm]} + /> + ), + '
    a
    b
    ' + ) -test('should skip nodes that are defined as disallowed', () => { - /** @type {Record} */ - const samples = { - p: {input: 'Paragraphs are cool', shouldNotContain: 'Paragraphs are cool'}, - h1: {input: '# Headers are neat', shouldNotContain: 'Headers are neat'}, - br: {input: 'Text \nHardbreak', shouldNotContain: '
    '}, - a: { - input: "[Espen's blog](http://espen.codes/) yeh?", - shouldNotContain: '' - }, - blockquote: { - input: '> Moo\n> Tools\n> FTW\n', - shouldNotContain: '} */ - const inputs = [] - /** @type {keyof samples} */ - let key - - for (key in samples) { - if (own.call(samples, key)) { - inputs.push(samples[key].input) - } - } + assert.equal(calls, 2) + }) + + await t.test('should support `components` (td, th; `isHeader`)', function () { + let tdCalls = 0 + let thCalls = 0 + + assert.equal( + asHtml( + + }, + th(props) { + const {isHeader, node, ...rest} = props + assert.equal(isHeader, true) + thCalls++ + return + } + }} + remarkPlugins={[remarkGfm]} + /> + ), + '
    a
    b
    ' + ) - const fullInput = inputs.join('\n') + assert.equal(tdCalls, 1) + assert.equal(thCalls, 1) + }) + + await t.test('should pass `node` to components', function () { + let calls = 0 + assert.equal( + asHtml( + + } + }} + /> + ), + '

    a

    ' + ) - for (key in samples) { - if (own.call(samples, key)) { - const sample = samples[key] + assert.equal(calls, 1) + }) - // Just to make sure, let ensure that the opposite is true + await t.test( + 'should support `rawSourcePos` (pass `sourcePosition` to components)', + function () { + let calls = 0 assert.equal( - asHtml().includes( - sample.shouldNotContain + asHtml( + + } + }} + /> ), - true, - 'fixture should contain `' + - sample.shouldNotContain + - '` (`' + - key + - '`)' + '

    a

    ' ) + assert.equal(calls, 1) + } + ) + + await t.test( + 'should support `includeElementIndex` (pass `index` to components)', + function () { + let calls = 0 assert.equal( asHtml( - - ).includes(sample.shouldNotContain), - false, - '`' + - key + - '` should not contain `' + - sample.shouldNotContain + - '` when disallowed' + {rest.children}

    + } + }} + /> + ), + '

    a

    ' ) + assert.equal(calls, 1) } - } -}) - -test('should throw if both allowed and disallowed types is specified', () => { - assert.throws(() => { - asHtml( - - ) - }, /only one of/i) -}) - -test('should be able to use a custom function to determine if the node should be allowed', () => { - const input = [ - '# Header', - '[react-markdown](https://github.com/remarkjs/react-markdown/) is a nice helper', - 'Also check out [my website](https://espen.codes/)' - ].join('\n\n') - - assert.equal( - asHtml( - - element.tagName !== 'a' || - (element.properties && - typeof element.properties.href === 'string' && - element.properties.href.indexOf('https://github.com/') === 0) - } - /> - ), - [ - '

    Header

    ', - '

    react-markdown is a nice helper

    ', - '

    Also check out

    ' - ].join('\n') ) -}) -test('should be able to override components', () => { - const input = - '# Header\n\nParagraph\n## New header\n1. List item\n2. List item 2\n\nFoo' - /** - * @param {number} level - */ - const heading = (level) => { - /** - * @param {object} props - * @param {Array} props.children - */ - const component = (props) => ( - {props.children} + await t.test('should support plugins (`remark-gfm`)', function () { + assert.equal( + asHtml(), + '

    a b c

    ' ) - return component - } - - const actual = asHtml( - - ) - - assert.equal( - actual, - 'Header\n

    Paragraph

    \nNew header\n
      \n
    1. List item
    2. \n
    3. List item 2
    4. \n
    \n

    Foo

    ' - ) -}) - -test('should throw on invalid component', () => { - const input = - '# Header\n\nParagraph\n## New header\n1. List item\n2. List item 2\n\nFoo' - const components = {h1: 123} - assert.throws( - () => - // @ts-expect-error runtime - asHtml(), - /Component for name `h1`/ - ) -}) - -test('can render the whole spectrum of markdown within a single run', async () => { - const inputUrl = new URL('fixtures/runthrough.md', import.meta.url) - const expectedUrl = new URL('fixtures/runthrough.html', import.meta.url) - const input = String(await fs.readFile(inputUrl)) - const expected = String(await fs.readFile(expectedUrl)) - - const actual = asHtml( - - ) - - assert.equal(actual, expected) -}) - -test('sanitizes certain dangerous urls for links by default', () => { - const error = console.error - - console.error = () => {} - - const input = [ - '# [Much fun](javascript:alert("foo"))', - "Can be had with [XSS links](vbscript:foobar('test'))", - '> And [other](VBSCRIPT:bap) nonsense... [files](file:///etc/passwd) for instance', - '## [Entities]( javascript:alert("bazinga")) can be tricky, too', - 'Regular [links](https://foo.bar) must [be]() allowed', - '[Some ref][xss]', - '[xss]: javascript:alert("foo") "Dangerous stuff"', - 'Should allow [mailto](mailto:ex@ample.com) and [tel](tel:13133) links tho', - 'Also, [protocol-agnostic](//google.com) should be allowed', - 'local [paths](/foo/bar) should be [allowed](foo)', - 'allow [weird](?javascript:foo) query strings and [hashes](foo#vbscript:orders)' - ].join('\n\n') - - const actual = asHtml() - assert.equal( - actual, - '

    Much fun

    \n

    Can be had with XSS links

    \n
    \n

    And other nonsense... files for instance

    \n
    \n

    Entities can be tricky, too

    \n

    Regular links must be allowed

    \n

    Some ref

    \n

    Should allow mailto and tel links tho

    \n

    Also, protocol-agnostic should be allowed

    \n

    local paths should be allowed

    \n

    allow weird query strings and hashes

    ' - ) - - console.error = error -}) - -test('allows specifying a custom URI-transformer', () => { - const input = - 'Received a great [pull request](https://github.com/remarkjs/react-markdown/pull/15) today' - const actual = asHtml( - uri.replace(/^https?:\/\/github\.com\//i, '/')} - /> - ) - assert.equal( - actual, - '

    Received a great pull request today

    ' - ) -}) - -test('should support turning off the default URI transform', () => { - const input = '[a](data:text/html,)' - const actual = asHtml() - const expected = - '

    a

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

    a

    +

    Contents

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

    b

    +

    c

    +

    d

    ` + ) + }) -test('can use parser plugins', () => { - const input = 'a ~b~ c' - const actual = asHtml( - - ) - assert.equal(actual, '

    a b c

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

    c

    ' + ) -test('supports checkbox lists', () => { - const input = '- [ ] Foo\n- [x] Bar\n\n---\n\n- Foo\n- Bar' - const actual = asHtml( - - ) - assert.equal( - actual, - '
      \n
    • Foo
    • \n
    • Bar
    • \n
    \n
    \n
      \n
    • Foo
    • \n
    • Bar
    • \n
    ' - ) -}) + 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: [] + }) + } + } + }) -test('should pass index of a node under its parent to components if `includeElementIndex` option is enabled', () => { - const input = 'Foo\n\nBar\n\nBaz' - const actual = asHtml( - {otherProps.children}

    - } - }} - /> - ) - assert.equal(actual, '

    Foo

    \n

    Bar

    \n

    Baz

    ') -}) + await t.test('should support data properties', function () { + assert.equal( + asHtml(), + '

    b

    ' + ) -test('should be able to render components with forwardRef in HOC', () => { - /** - * @typedef {import('react').Ref} Ref - * @typedef {JSX.IntrinsicElements['a'] & import('../lib/ast-to-react.js').ReactMarkdownProps} Props - */ - - /** - * @param {(params: Props) => JSX.Element} Component - */ - const wrapper = (Component) => - React.forwardRef( + function plugin() { /** - * @param {Props} props - * @param {Ref} ref + * @param {Root} tree + * @returns {undefined} */ - (props, ref) => + 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( + asHtml(), + '

    c

    ' ) - /** - * @param {Props} props - */ - // eslint-disable-next-line react/jsx-no-target-blank - const wrapped = (props) => + function plugin() { + /** + * @param {Root} tree + * @returns {undefined} + */ + return function (tree) { + tree.children.unshift({ + type: 'element', + tagName: 'i', + properties: {accept: ['a', 'b']}, + children: [] + }) + } + } + }) - const actual = asHtml( - - [Link](https://example.com/) - - ) - assert.equal( - actual, - '

    Link

    ' - ) -}) + await t.test('should support `style` properties', function () { + assert.equal( + asHtml(), + '

    a

    ' + ) -test('should render table of contents plugin', () => { - const input = [ - '# Header', - '## Table of Contents', - '## First Section', - '## Second Section', - '### Subsection', - '## Third Section' - ].join('\n') - - const actual = asHtml( - - ) + 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: [] + }) + } + } + }) - assert.equal( - actual, - '

    Header

    \n

    Table of Contents

    \n\n

    First Section

    \n

    Second Section

    \n

    Subsection

    \n

    Third Section

    ' - ) -}) + await t.test( + 'should support `style` properties w/ vendor prefixes', + function () { + assert.equal( + asHtml(), + '

    a

    ' + ) -test('should pass `node` as prop to all non-tag/non-fragment components', () => { - const input = "# So, *headers... they're _cool_*\n\n" - const actual = asHtml( - { - text += child.value + 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: [] }) - return text } - }} - /> - ) - assert.equal(actual, 'So, headers... they're cool') -}) - -test('should support formatting at the start of a GFM tasklist (GH-494)', () => { - const input = '- [ ] *a*' - const actual = asHtml( - + } + } ) - const expected = - '
      \n
    • a
    • \n
    ' - assert.equal(actual, expected) -}) - -test('should support aria properties', () => { - const input = 'c' - - /** @type {import('unified').Plugin, Root>} */ - const plugin = () => (root) => { - root.children.unshift({ - type: 'element', - tagName: 'input', - properties: {id: 'a', ariaDescribedBy: 'b', required: true}, - children: [] - }) - } - - const actual = asHtml() - const expected = '

    c

    ' - assert.equal(actual, expected) -}) - -test('should support data properties', () => { - const input = 'b' - - /** @type {import('unified').Plugin, Root>} */ - const plugin = () => (root) => { - root.children.unshift({ - type: 'element', - tagName: 'i', - properties: {dataWhatever: 'a', dataIgnoreThis: undefined}, - children: [] - }) - } - - const actual = asHtml() - const expected = '

    b

    ' - assert.equal(actual, expected) -}) - -test('should support comma separated properties', () => { - const input = 'c' - - /** @type {import('unified').Plugin, Root>} */ - const plugin = () => (root) => { - root.children.unshift({ - type: 'element', - tagName: 'i', - properties: {accept: ['a', 'b']}, - children: [] - }) - } - - const actual = asHtml() - const expected = '

    c

    ' - assert.equal(actual, expected) -}) -test('should support `style` properties', () => { - const input = 'a' - - /** @type {import('unified').Plugin, Root>} */ - const plugin = () => (root) => { - root.children.unshift({ - type: 'element', - tagName: 'i', - properties: {style: 'color: red; font-weight: bold'}, - children: [] - }) - } - - const actual = asHtml() - const expected = '

    a

    ' - assert.equal(actual, expected) -}) - -test('should support `style` properties w/ vendor prefixes', () => { - const input = 'a' - - /** @type {import('unified').Plugin, Root>} */ - const plugin = () => (root) => { - root.children.unshift({ - type: 'element', - tagName: 'i', - properties: {style: '-ms-b: 1; -webkit-c: 2'}, - children: [] - }) - } - - const actual = asHtml() - const expected = '

    a

    ' - assert.equal(actual, expected) -}) - -test('should support broken `style` properties', () => { - const input = 'a' - - /** @type {import('unified').Plugin, Root>} */ - const plugin = () => (root) => { - root.children.unshift({ - type: 'element', - tagName: 'i', - properties: {style: 'broken'}, - children: [] - }) - } - - const actual = asHtml() - const expected = '

    a

    ' - assert.equal(actual, expected) -}) + await t.test('should support broken `style` properties', function () { + assert.equal( + asHtml(), + '

    a

    ' + ) -test('should support SVG elements', () => { - const input = 'a' - - /** @type {import('unified').Plugin, Root>} */ - const plugin = () => (root) => { - root.children.unshift({ - type: 'element', - tagName: 'svg', - properties: {xmlns: 'http://www.w3.org/2000/svg', viewBox: '0 0 500 500'}, - 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. - { + function plugin() { + /** + * @param {Root} tree + * @returns {undefined} + */ + return function (tree) { + tree.children.unshift({ type: 'element', - tagName: 'path', - properties: {strokeMiterLimit: -1}, + tagName: 'i', + properties: {style: 'broken'}, children: [] - } - ] - }) - } - - const actual = asHtml() - const expected = - 'SVG `<circle>` element

    a

    ' - assert.equal(actual, expected) -}) - -test('should support (ignore) comments', () => { - const input = 'a' - - /** @type {import('unified').Plugin, Root>} */ - const plugin = () => (root) => { - root.children.unshift({type: 'comment', value: 'things!'}) - } - - const actual = asHtml() - const expected = '

    a

    ' - assert.equal(actual, expected) -}) - -test('should support table cells w/ style', () => { - const input = '| a |\n| :- |' - - /** @type {import('unified').Plugin, Root>} */ - const plugin = () => (root) => { - visit(root, {type: 'element', tagName: 'th'}, (node) => { - node.properties = {...node.properties, style: 'color: red'} - }) - } - - const actual = asHtml( - - ) - const expected = - '
    a
    ' + }) + } + } + }) - assert.equal(actual, expected) -}) + await t.test('should support SVG elements', function () { + assert.equal( + asHtml(), + 'SVG `<circle>` element

    a

    ' + ) -test('should crash on a plugin replacing `root`', () => { - const input = 'a' - /** @type {import('unified').Plugin, Root>} */ - // @ts-expect-error: runtime. - const plugin = () => () => ({type: 'comment', value: 'things!'}) - assert.throws(() => { - asHtml() - }, /Expected a `root` node/) -}) + 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: [] + } + ] + }) + } + } + }) -test('should support remark plugins with array parameter', async () => { - const error = console.error - /** @type {string} */ - let message = '' + await t.test('should support comments (ignore them)', function () { + const input = 'a' + const actual = asHtml( + + ) + const expected = '

    a

    ' + assert.equal(actual, expected) - console.error = (/** @type {string} */ d) => { - message = d - } + 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( + asHtml( + + ), + '
    a
    ' + ) - const input = 'a' - /** @type {import('unified').Plugin>, Root>} */ - const plugin = () => () => {} + 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'} + } + }) + } + } + }) - const actual = asHtml( - - ) - const expected = '

    a

    ' - assert.equal(actual, expected) + await t.test('should fail on a plugin replacing `root`', function () { + assert.throws(function () { + asHtml() + }, /Expected a `root` node/) - assert.doesNotMatch(message, /Warning: Failed/, 'Prop types should be valid') - console.error = error + function plugin() { + /** + * @returns {Root} + */ + return function () { + // @ts-expect-error: check how non-roots are handled. + return {type: 'comment', value: 'things!'} + } + } + }) }) -test('should support rehype plugins with array parameter', async () => { - const error = console.error - /** @type {string} */ - let message = '' - - console.error = (/** @type {string} */ d) => { - message = d - } - - const input = 'a' - /** @type {import('unified').Plugin>, Root>} */ - const plugin = () => () => {} - - const actual = asHtml( - - ) - const expected = '

    a

    ' - assert.equal(actual, expected) +/** + * @param {ReturnType} input + * @returns {string} + */ +function asHtml(input) { + return renderToStaticMarkup(input) +} - assert.doesNotMatch(message, /Warning: Failed/, 'Prop types should be valid') - console.error = error -}) +/** + * @returns {undefined} + */ +function noop() {} diff --git a/tsconfig.json b/tsconfig.json index 31c68dc0..7baf4f67 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,11 +6,9 @@ "emitDeclarationOnly": true, "exactOptionalPropertyTypes": true, "jsx": "preserve", - "lib": ["dom", "es2020"], + "lib": ["dom", "es2022"], "module": "node16", "strict": true, - // To do: remove after update. - "skipLibCheck": true, "target": "es2020" }, "exclude": ["coverage/", "node_modules/", "**/*.min.js"], From 8b1ff41d1c70bd4f751dbe5b4658e4cb425792d6 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Tue, 26 Sep 2023 18:47:23 +0200 Subject: [PATCH 057/125] Remove unneeded dependency --- lib/ast-to-react.js | 8 ++++---- package.json | 4 +--- test/test.jsx | 16 ++++++++++++++++ 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/lib/ast-to-react.js b/lib/ast-to-react.js index 9076b38e..07b6a3ce 100644 --- a/lib/ast-to-react.js +++ b/lib/ast-to-react.js @@ -75,7 +75,6 @@ */ import React from 'react' -import ReactIs from 'react-is' import {stringify as commas} from 'comma-separated-tokens' import {whitespace} from 'hast-util-whitespace' import {find, hastToReact, svg} from 'property-information' @@ -185,14 +184,15 @@ function toReact(state, node, index, parent) { // Restore parent schema. state.schema = parentSchema + /** @type {ComponentType | string} */ const component = options.components && own.call(options.components, name) - ? options.components[name] + ? options.components[name] || name : name const basic = typeof component === 'string' || component === React.Fragment - if (!ReactIs.isValidElementType(component)) { - throw new TypeError( + if (!basic && typeof component !== 'function') { + throw new Error( `Component for name \`${name}\` not defined or is not renderable` ) } diff --git a/package.json b/package.json index 459523e5..c6df64f6 100644 --- a/package.json +++ b/package.json @@ -84,10 +84,9 @@ "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", - "mdast-util-to-hast": "^13.0.2", + "mdast-util-to-hast": "^13.0.0", "prop-types": "^15.0.0", "property-information": "^6.0.0", - "react-is": "^18.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "space-separated-tokens": "^2.0.0", @@ -105,7 +104,6 @@ "@types/node": "^20.0.0", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", - "@types/react-is": "^18.0.0", "c8": "^8.0.0", "esbuild": "^0.19.0", "eslint-plugin-es": "^4.0.0", diff --git a/test/test.jsx b/test/test.jsx index fbc38f6c..e2c31d30 100644 --- a/test/test.jsx +++ b/test/test.jsx @@ -645,6 +645,22 @@ test('react-markdown', async function (t) { }, /Component for name `h1`/) }) + await t.test('should support `null`, `undefined` in components', function () { + assert.equal( + asHtml( + + ), + '

    a

    ' + ) + }) + await t.test('should support `components` (headings; `level`)', function () { let calls = 0 From 8aabf74e449ae165cabfe5846fe641312b542750 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Tue, 26 Sep 2023 18:56:37 +0200 Subject: [PATCH 058/125] Change to improve error messages --- lib/ast-to-react.js | 6 +++++- lib/react-markdown.js | 10 ++++++---- lib/rehype-filter.js | 2 +- test/test.jsx | 8 ++++---- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/lib/ast-to-react.js b/lib/ast-to-react.js index 07b6a3ce..4e3731b0 100644 --- a/lib/ast-to-react.js +++ b/lib/ast-to-react.js @@ -193,7 +193,11 @@ function toReact(state, node, index, parent) { if (!basic && typeof component !== 'function') { throw new Error( - `Component for name \`${name}\` not defined or is not renderable` + 'Unexpected value `' + + component + + '` for `' + + name + + '`, expected component or tag name' ) } diff --git a/lib/react-markdown.js b/lib/react-markdown.js index 2aeb1372..4d08e366 100644 --- a/lib/react-markdown.js +++ b/lib/react-markdown.js @@ -180,17 +180,19 @@ export function ReactMarkdown(options) { ) } - const hastNode = processor.runSync(processor.parse(file), file) + const hastTree = processor.runSync(processor.parse(file), file) - if (hastNode.type !== 'root') { - throw new TypeError('Expected a `root` node') + if (hastTree.type !== 'root') { + throw new TypeError( + 'Unexpected `' + hastTree.type + '` node, expected `root`' + ) } /** @type {ReactElement} */ let result = React.createElement( React.Fragment, {}, - childrenToReact({options, schema: html, listDepth: 0}, hastNode) + childrenToReact({options, schema: html, listDepth: 0}, hastTree) ) if (options.className) { diff --git a/lib/rehype-filter.js b/lib/rehype-filter.js index 9da34f48..03121bad 100644 --- a/lib/rehype-filter.js +++ b/lib/rehype-filter.js @@ -22,7 +22,7 @@ export default function rehypeFilter(options) { ) { if (options.allowedElements && options.disallowedElements) { throw new TypeError( - 'Only one of `allowedElements` and `disallowedElements` should be defined' + 'Unexpected combined `allowedElements` and `disallowedElements`, expected one or the other' ) } diff --git a/test/test.jsx b/test/test.jsx index e2c31d30..23b3ecad 100644 --- a/test/test.jsx +++ b/test/test.jsx @@ -558,7 +558,7 @@ test('react-markdown', async function (t) { disallowedElements={['a']} /> ) - }, /only one of/i) + }, /Unexpected combined `allowedElements` and `disallowedElements`, expected one or the other/) } ) @@ -631,7 +631,7 @@ test('react-markdown', async function (t) { ) }) - await t.test('should fail on invalid component', function () { + await t.test('should fail on an invalid component', function () { assert.throws(function () { asHtml( ) - }, /Component for name `h1`/) + }, /Unexpected value `123` for `h1`, expected component or tag name/) }) await t.test('should support `null`, `undefined` in components', function () { @@ -1204,7 +1204,7 @@ test('react-markdown', async function (t) { await t.test('should fail on a plugin replacing `root`', function () { assert.throws(function () { asHtml() - }, /Expected a `root` node/) + }, /Unexpected `comment` node, expected `root/) function plugin() { /** From 434627686e21d4bcfb4301417e0da2bb851d4391 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Wed, 27 Sep 2023 13:08:38 +0200 Subject: [PATCH 059/125] Remove support for passing custom props to components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, this project automatically passed different extra props to particular components. Those props are sometimes useful to some people, but not always useful to everyone. 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 When using options, these props are no longer passed: * `includeElementIndex`: `index` (create a plugin) * `rawSourcePos`: `sourcePosition` (use `node.position`) * `sourcePos`: `data-sourcepos` (create a plugin) --- changelog.md | 99 +++++++ index.js | 9 +- lib/ast-to-react.js | 431 ---------------------------- lib/complex-types.d.ts | 36 --- lib/{react-markdown.js => index.js} | 259 +++++++++++++---- lib/rehype-filter.js | 66 ----- lib/uri-transformer.js | 50 ---- package.json | 14 +- readme.md | 74 +---- test/test.jsx | 317 +++++++++----------- 10 files changed, 449 insertions(+), 906 deletions(-) delete mode 100644 lib/ast-to-react.js delete mode 100644 lib/complex-types.d.ts rename lib/{react-markdown.js => index.js} (54%) delete mode 100644 lib/rehype-filter.js delete mode 100644 lib/uri-transformer.js diff --git a/changelog.md b/changelog.md index 481cf7ce..5fd55f18 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,105 @@ All notable changes will be documented in this file. +## 9.0.0 - unreleased + +### Remove `includeElementIndex` option + +The `includeElementIndex` option was removed, so `index` is never passed to +components. +Write a plugin to pass `index`: + +
    +Show example of plugin + +```jsx +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 + } + }) + } +} +``` + +### Remove `rawSourcePos` option + +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` option + +The `sourcePos` option was removed, so `data-sourcepos` is never passed to +elements. +Write a plugin to pass `index`: + +
    +Show example of plugin + +```jsx +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) diff --git a/index.js b/index.js index 6e83f36e..eb6590e0 100644 --- a/index.js +++ b/index.js @@ -1,8 +1,7 @@ /** - * @typedef {import('./lib/react-markdown.js').Options} Options - * @typedef {import('./lib/ast-to-react.js').Components} Components + * @typedef {import('hast-util-to-jsx-runtime').Components} Components + * @typedef {import('hast-util-to-jsx-runtime').ExtraProps} ExtraProps + * @typedef {import('./lib/index.js').Options} Options */ -export {uriTransformer} from './lib/uri-transformer.js' - -export {ReactMarkdown as default} from './lib/react-markdown.js' +export {ReactMarkdown as default, uriTransformer} from './lib/index.js' diff --git a/lib/ast-to-react.js b/lib/ast-to-react.js deleted file mode 100644 index 4e3731b0..00000000 --- a/lib/ast-to-react.js +++ /dev/null @@ -1,431 +0,0 @@ -/// - -/** - * @typedef {import('react').ComponentPropsWithoutRef} ComponentPropsWithoutRef - * @template {import('react').ElementType} T - */ - -/** - * @typedef {import('react').ComponentType} ComponentType - * @template T - */ - -/** - * @typedef {import('hast').Element} Element - * @typedef {import('hast').Parents} Parents - * @typedef {import('hast').Root} Root - * - * @typedef {import('property-information').Schema} Schema - * - * @typedef {import('react').ReactNode} ReactNode - * - * @typedef {import('unist').Position} Position - * - * @typedef {import('./complex-types.js').ReactMarkdownProps} ReactMarkdownProps - * @typedef {import('./complex-types.js').NormalComponents} NormalComponents - * @typedef {import('./react-markdown.js').Options} Options - */ - -/** - * @typedef State - * Info passed around. - * @property {Readonly} options - * Configuration. - * @property {Schema} schema - * Schema. - * @property {number} listDepth - * Depth. - * - * @typedef {ComponentPropsWithoutRef<'code'> & ReactMarkdownProps & {inline?: boolean}} CodeProps - * Props passed to components for `code`. - * to do: always pass `inline`? - * @typedef {ComponentPropsWithoutRef<'h1'> & ReactMarkdownProps & {level: number}} HeadingProps - * Props passed to components for `h1`, `h2`, etc. - * @typedef {ComponentPropsWithoutRef<'li'> & ReactMarkdownProps & {checked: boolean | null, index: number, ordered: boolean}} LiProps - * Props passed to components for `li`. - * to do: use `undefined`. - * @typedef {ComponentPropsWithoutRef<'ol'> & ReactMarkdownProps & {depth: number, ordered: true}} OrderedListProps - * Props passed to components for `ol`. - * @typedef {ComponentPropsWithoutRef<'td'> & ReactMarkdownProps & {isHeader: false}} TableDataCellProps - * Props passed to components for `td`. - * @typedef {ComponentPropsWithoutRef<'th'> & ReactMarkdownProps & {isHeader: true}} TableHeaderCellProps - * Props passed to components for `th`. - * @typedef {ComponentPropsWithoutRef<'tr'> & ReactMarkdownProps & {isHeader: boolean}} TableRowProps - * Props passed to components for `tr`. - * @typedef {ComponentPropsWithoutRef<'ul'> & ReactMarkdownProps & {depth: number, ordered: false}} UnorderedListProps - * Props passed to components for `ul`. - * - * @typedef SpecialComponents - * @property {ComponentType | keyof JSX.IntrinsicElements} code - * @property {ComponentType | keyof JSX.IntrinsicElements} h1 - * @property {ComponentType | keyof JSX.IntrinsicElements} h2 - * @property {ComponentType | keyof JSX.IntrinsicElements} h3 - * @property {ComponentType | keyof JSX.IntrinsicElements} h4 - * @property {ComponentType | keyof JSX.IntrinsicElements} h5 - * @property {ComponentType | keyof JSX.IntrinsicElements} h6 - * @property {ComponentType | keyof JSX.IntrinsicElements} li - * @property {ComponentType | keyof JSX.IntrinsicElements} ol - * @property {ComponentType | keyof JSX.IntrinsicElements} td - * @property {ComponentType | keyof JSX.IntrinsicElements} th - * @property {ComponentType | keyof JSX.IntrinsicElements} tr - * @property {ComponentType | keyof JSX.IntrinsicElements} ul - * - * @typedef {Partial & SpecialComponents>} Components - * Components. - */ - -import React from 'react' -import {stringify as commas} from 'comma-separated-tokens' -import {whitespace} from 'hast-util-whitespace' -import {find, hastToReact, svg} from 'property-information' -import {stringify as spaces} from 'space-separated-tokens' -import style from 'style-to-object' -import {stringifyPosition} from 'unist-util-stringify-position' -import {uriTransformer} from './uri-transformer.js' - -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 {State} state - * Info passed around. - * @param {Readonly} node - * Node to transform. - * @returns {Array} - * Nodes. - */ -export function childrenToReact(state, node) { - /** @type {Array} */ - const children = [] - let childIndex = -1 - - while (++childIndex < node.children.length) { - const child = node.children[childIndex] - - if (child.type === 'element') { - children.push(toReact(state, 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' && !state.options.skipHtml) { - // Default behavior is to show (encoded) HTML. - children.push(child.value) - } - } - - return children -} - -/** - * @param {State} state - * Info passed around. - * @param {Readonly} node - * Node to transform. - * @param {number} index - * Position of `node` in `parent`. - * @param {Readonly} parent - * Parent of `node`. - * @returns {ReactNode} - * Node. - */ -function toReact(state, node, index, parent) { - const options = state.options - const transform = - options.transformLinkUri === undefined - ? uriTransformer - : options.transformLinkUri - const parentSchema = state.schema - // Assume a known HTML/SVG element. - const name = /** @type {keyof JSX.IntrinsicElements} */ (node.tagName) - /** @type {Record} */ - const properties = {} - let schema = parentSchema - /** @type {string} */ - let property - - if (parentSchema.space === 'html' && name === 'svg') { - schema = svg - state.schema = schema - } - - if (node.properties) { - for (property in node.properties) { - if (own.call(node.properties, property)) { - addProperty(state, properties, property, node.properties[property]) - } - } - } - - if (name === 'ol' || name === 'ul') { - state.listDepth++ - } - - const children = childrenToReact(state, node) - - if (name === 'ol' || name === 'ul') { - state.listDepth-- - } - - // Restore parent schema. - state.schema = parentSchema - - /** @type {ComponentType | string} */ - const component = - options.components && own.call(options.components, name) - ? options.components[name] || name - : name - const basic = typeof component === 'string' || component === React.Fragment - - if (!basic && typeof component !== 'function') { - throw new Error( - 'Unexpected value `' + - component + - '` for `' + - name + - '`, expected component or tag name' - ) - } - - properties.key = index - - if (name === 'a' && transform) { - properties.href = transform( - String(properties.href || ''), - node.children, - // To do: pass `undefined`. - 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 || ''), - // To do: pass `undefined`. - typeof properties.title === 'string' ? properties.title : null - ) - } - - if (!basic && name === 'li' && parent.type === 'element') { - const input = getInputElement(node) - properties.checked = - // To do: pass `undefined`. - input ? 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 = state.listDepth - } - - if (name === 'td' || name === 'th') { - if (properties.align) { - let style = /** @type {Record | undefined} */ ( - properties.style - ) - - if (!style) { - style = {} - properties.style = style - } - - style.textAlign = String(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'] = stringifyPosition(node) - } - - 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 {Readonly} node - * Node to check. - * @returns {Element | undefined} - * `input` element, if found. - */ -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 - } - } -} - -/** - * @param {Readonly} parent - * Node. - * @param {Readonly} [node] - * Node in parent (optional). - * @returns {number} - * Siblings before `node`. - */ -function getElementsBeforeCount(parent, node) { - let index = -1 - let count = 0 - - while (++index < parent.children.length) { - const child = parent.children[index] - if (child === node) break - if (child.type === 'element') count++ - } - - return count -} - -/** - * @param {State} state - * Info passed around. - * @param {Record} props - * Properties. - * @param {string} prop - * Property. - * @param {unknown} value - * Value. - * @returns {undefined} - * Nothing. - */ -function addProperty(state, props, prop, value) { - const info = find(state.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 - * Style. - * @returns {Record} - * Style. - */ -function parseStyle(value) { - /** @type {Record} */ - const result = {} - - try { - style(value, iterator) - } catch { - // Silent. - } - - return result - - /** - * @param {string} name - * Name. - * @param {string} v - * Value. - */ - 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} _ - * Whole match. - * @param {string} $1 - * Letter. - * @returns {string} - * Replacement. - */ -function styleReplacer(_, $1) { - return $1.toUpperCase() -} diff --git a/lib/complex-types.d.ts b/lib/complex-types.d.ts deleted file mode 100644 index b460319a..00000000 --- a/lib/complex-types.d.ts +++ /dev/null @@ -1,36 +0,0 @@ -// File for types which are not handled correctly in JSDoc mode. -import type {Element} from 'hast' -import type {ComponentPropsWithoutRef, ComponentType, ReactNode} from 'react' -import type {Position} from 'unist' - -/** - * Props passed to components. - */ -export type ReactMarkdownProps = { - /** - * Passed when `options.sourcePos` is given. - */ - 'data-sourcepos': string | undefined - /** - * Passed when `options.includeElementIndex` is given - */ - index?: number - /** - * Original hast node. - */ - node: Element - /** - * Passed when `options.includeElementIndex` is given - */ - siblingCount?: number - /** - * Passed when `options.rawSourcePos` is given - */ - sourcePosition?: Position | undefined -} - -export type NormalComponents = { - [TagName in keyof JSX.IntrinsicElements]: - | keyof JSX.IntrinsicElements - | ComponentType & ReactMarkdownProps> -} diff --git a/lib/react-markdown.js b/lib/index.js similarity index 54% rename from lib/react-markdown.js rename to lib/index.js index 4d08e366..6c79f953 100644 --- a/lib/react-markdown.js +++ b/lib/index.js @@ -1,11 +1,16 @@ +// Register `Raw` in tree: +/// + /** * @typedef {import('hast').Element} Element * @typedef {import('hast').ElementContent} ElementContent + * @typedef {import('hast').Nodes} Nodes * @typedef {import('hast').Parents} Parents + * @typedef {import('hast').Root} Root + * @typedef {import('hast-util-to-jsx-runtime').Components} Components * @typedef {import('remark-rehype').Options} RemarkRehypeOptions - * @typedef {import('react').ReactElement<{}>} ReactElement + * @typedef {import('unist-util-visit').BuildVisitor} Visitor * @typedef {import('unified').PluggableList} PluggableList - * @typedef {import('./ast-to-react.js').Components} Components */ /** @@ -39,17 +44,11 @@ * Markdown to parse. * @property {string | null | undefined} [className] * Wrap the markdown in a `div` with this class name. - * @property {Components | null | undefined} [components] + * @property {Partial | null | undefined} [components] * Map tag names to React components. * @property {ReadonlyArray | null | undefined} [disallowedElements] * Tag names to disallow (cannot combine w/ `allowedElements`), all tag names * are allowed by default. - * @property {boolean | null | undefined} [includeElementIndex=false] - * Pass the `index` (number of elements before it) and `siblingCount` (number - * of elements in parent) as props to all components (default: `false`). - * @property {boolean | null | undefined} [rawSourcePos=false] - * Pass a `sourcePosition` prop to all components with their position - * (default: `false`). * @property {PluggableList | null | undefined} [rehypePlugins] * List of rehype plugins to use. * @property {PluggableList | null | undefined} [remarkPlugins] @@ -58,9 +57,6 @@ * Options to pass through to `remark-rehype`. * @property {boolean | null | undefined} [skipHtml=false] * Ignore HTML in markdown completely (default: `false`). - * @property {boolean | null | undefined} [sourcePos=false] - * Pass a `data-sourcepos` prop to all components with a serialized position - * (default: `false`). * @property {TransformLink | false | null | undefined} [transformLinkUri] * Change URLs on images (default: `uriTransformer`); * pass `false` to allow all URLs, which is unsafe @@ -97,20 +93,27 @@ * Transformed URL (optional). */ -import React from 'react' +import {toJsxRuntime} from 'hast-util-to-jsx-runtime' import PropTypes from 'prop-types' -import {html} from 'property-information' +// @ts-expect-error: untyped. +import {Fragment, jsx, jsxs} from 'react/jsx-runtime' import remarkParse from 'remark-parse' import remarkRehype from 'remark-rehype' import {unified} from 'unified' +import {visit} from 'unist-util-visit' import {VFile} from 'vfile' -import {childrenToReact} from './ast-to-react.js' -import rehypeFilter from './rehype-filter.js' + +const safeProtocols = ['http', 'https', 'mailto', 'tel'] const own = {}.hasOwnProperty const changelog = 'https://github.com/remarkjs/react-markdown/blob/main/changelog.md' +/** @type {PluggableList} */ +const emptyPlugins = [] +/** @type {Readonly} */ +const emptyRemarkRehypeOptions = {allowDangerousHtml: true} + // Mutable because we `delete` any time it’s used and a message is sent. /** @type {Record>} */ const deprecated = { @@ -129,13 +132,13 @@ const deprecated = { to: 'disallowedElements' }, escapeHtml: {id: 'remove-buggy-html-in-markdown-parser'}, - includeNodeIndex: { - id: 'change-includenodeindex-to-includeelementindex', - to: 'includeElementIndex' - }, + includeElementIndex: {id: '#remove-includeelementindex-option'}, + includeNodeIndex: {id: 'change-includenodeindex-to-includeelementindex'}, plugins: {id: 'change-plugins-to-remarkplugins', to: 'remarkPlugins'}, + rawSourcePos: {id: '#remove-rawsourcepos-option'}, renderers: {id: 'change-renderers-to-components', to: 'components'}, - source: {id: 'change-source-to-children', to: 'children'} + source: {id: 'change-source-to-children', to: 'children'}, + sourcePos: {id: '#remove-sourcepos-option'} } /** @@ -144,11 +147,58 @@ const deprecated = { * @param {Readonly} options * Configuration (required). * Note: React types require that props are passed. - * @returns {ReactElement} + * @returns {JSX.Element} * React element. */ export function ReactMarkdown(options) { + const allowedElements = options.allowedElements + const allowElement = options.allowElement + const children = options.children || '' + const className = options.className + const components = options.components + const disallowedElements = options.disallowedElements + const rehypePlugins = options.rehypePlugins || emptyPlugins + const remarkPlugins = options.remarkPlugins || emptyPlugins + const remarkRehypeOptions = options.remarkRehypeOptions + ? {...options.remarkRehypeOptions, ...emptyRemarkRehypeOptions} + : emptyRemarkRehypeOptions + const skipHtml = options.skipHtml + const transformImageUri = + options.transformImageUri === undefined + ? uriTransformer + : options.transformImageUri + const transformLinkUri = + options.transformLinkUri === undefined + ? uriTransformer + : options.transformLinkUri + const unwrapDisallowed = options.unwrapDisallowed + + const processor = unified() + .use(remarkParse) + .use(remarkPlugins) + .use(remarkRehype, remarkRehypeOptions) + .use(rehypePlugins) + + const file = new VFile() + + if (typeof children === 'string') { + file.value = children + } else { + console.warn( + '[react-markdown] Warning: please pass a string as `children` (not: `' + + children + + '`)' + ) + } + + if (allowedElements && disallowedElements) { + throw new TypeError( + 'Unexpected combined `allowedElements` and `disallowedElements`, expected one or the other' + ) + } + for (const key in deprecated) { + // To do: use `Object.hasOwn`. if (own.call(deprecated, key) && own.call(options, key)) { const deprecation = deprecated[key] console.warn( @@ -160,46 +210,93 @@ export function ReactMarkdown(options) { } } - const processor = unified() - .use(remarkParse) - .use(options.remarkPlugins || []) - .use(remarkRehype, { - ...options.remarkRehypeOptions, - allowDangerousHtml: true - }) - .use(options.rehypePlugins || []) - .use(rehypeFilter, options) - - const file = new VFile() + const mdastTree = processor.parse(file) + /** @type {Nodes} */ + let hastTree = processor.runSync(mdastTree, file) - if (typeof options.children === 'string') { - file.value = options.children - } else if (options.children !== null && options.children !== undefined) { - console.warn( - `[react-markdown] Warning: please pass a string as \`children\` (not: \`${options.children}\`)` - ) + // Wrap in `div` if there’s a class name. + if (className) { + hastTree = { + type: 'element', + tagName: 'div', + properties: {className}, + // Assume no doctypes. + children: /** @type {Array} */ ( + hastTree.type === 'root' ? hastTree.children : [hastTree] + ) + } } - const hastTree = processor.runSync(processor.parse(file), file) + visit(hastTree, transform) - if (hastTree.type !== 'root') { - throw new TypeError( - 'Unexpected `' + hastTree.type + '` node, expected `root`' - ) - } + return toJsxRuntime(hastTree, { + Fragment, + components, + ignoreInvalidStyle: true, + jsx, + jsxs, + passKeys: true, + passNode: true + }) - /** @type {ReactElement} */ - let result = React.createElement( - React.Fragment, - {}, - childrenToReact({options, schema: html, listDepth: 0}, hastTree) - ) + /** @type {Visitor} */ + 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} + } - if (options.className) { - result = React.createElement('div', {className: options.className}, result) - } + return index + } - return result + if (transformLinkUri && node.type === 'element' && node.tagName === 'a') { + node.properties.href = transformLinkUri( + String(node.properties.href || ''), + node.children, + // To do: pass `undefined`. + typeof node.properties.title === 'string' ? node.properties.title : null + ) + } + + if ( + transformImageUri && + node.type === 'element' && + node.tagName === 'img' + ) { + node.properties.src = transformImageUri( + String(node.properties.src || ''), + String(node.properties.alt || ''), + // To do: pass `undefined`. + typeof node.properties.title === 'string' ? node.properties.title : null + ) + } + + if (node.type === 'element') { + let remove = false + + if (allowedElements) { + remove = !allowedElements.includes(node.tagName) + } else if (disallowedElements) { + remove = disallowedElements.includes(node.tagName) + } + + 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 + } + } + } } ReactMarkdown.propTypes = { @@ -252,11 +349,57 @@ ReactMarkdown.propTypes = { ]) ), // Transform options: - sourcePos: PropTypes.bool, - rawSourcePos: PropTypes.bool, skipHtml: PropTypes.bool, - includeElementIndex: PropTypes.bool, transformLinkUri: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]), transformImageUri: PropTypes.func, components: PropTypes.object } + +/** + * Make a URL safe. + * + * @param {string} value + * URL. + * @returns {string} + * Safe URL. + */ +export function uriTransformer(value) { + const url = (value || '').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 < safeProtocols.length) { + const protocol = safeProtocols[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 + } + + // To do: is there an alternative? + // eslint-disable-next-line no-script-url + return 'javascript:void(0)' +} diff --git a/lib/rehype-filter.js b/lib/rehype-filter.js deleted file mode 100644 index 03121bad..00000000 --- a/lib/rehype-filter.js +++ /dev/null @@ -1,66 +0,0 @@ -/** - * @typedef {import('hast').Element} Element - * @typedef {import('hast').Root} Root - * @typedef {import('./react-markdown.js').Options} Options - */ - -import {visit} from 'unist-util-visit' - -/** - * Filter nodes. - * - * @param {Readonly} options - * Configuration (required). - * @returns - * Transform (optional). - */ -export default function rehypeFilter(options) { - if ( - options.allowElement || - options.allowedElements || - options.disallowedElements - ) { - if (options.allowedElements && options.disallowedElements) { - throw new TypeError( - 'Unexpected combined `allowedElements` and `disallowedElements`, expected one or the other' - ) - } - - /** - * Transform. - * - * @param {Root} tree - * Tree. - * @returns {undefined} - * Nothing. - */ - return function (tree) { - visit(tree, 'element', function (node, index, 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 && parent && 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 eca407fb..00000000 --- a/lib/uri-transformer.js +++ /dev/null @@ -1,50 +0,0 @@ -const protocols = ['http', 'https', 'mailto', 'tel'] - -/** - * Make a URL safe. - * - * @param {string} value - * URL. - * @returns {string} - * Safe URL. - */ -export function uriTransformer(value) { - const url = (value || '').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 - } - - // To do: is there an alternative? - // eslint-disable-next-line no-script-url - return 'javascript:void(0)' -} diff --git a/package.json b/package.json index c6df64f6..44e7bb7a 100644 --- a/package.json +++ b/package.json @@ -81,18 +81,12 @@ "dependencies": { "@types/hast": "^3.0.0", "@types/prop-types": "^15.0.0", - "@types/unist": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "hast-util-whitespace": "^3.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", "mdast-util-to-hast": "^13.0.0", "prop-types": "^15.0.0", - "property-information": "^6.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", - "space-separated-tokens": "^2.0.0", - "style-to-object": "^0.4.0", "unified": "^11.0.0", - "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, @@ -161,9 +155,9 @@ "atLeast": 100, "detail": true, "ignoreCatch": true, - "#": "below is ignored because some proptypes will `any`", + "#": "below is ignored because some proptypes will `any`; to do: remove prop-types?", "ignoreFiles": [ - "lib/react-markdown.d.ts" + "lib/index.d.ts" ], "strict": true }, @@ -191,13 +185,13 @@ "test/**/*.jsx" ], "rules": { - "n/file-extension-in-import": "off", "no-unused-vars": "off" } } ], "prettier": true, "rules": { + "n/file-extension-in-import": "off", "unicorn/prefer-string-replace-all": "off" } } diff --git a/readme.md b/readme.md index acb8265d..1b6f5500 100644 --- a/readme.md +++ b/readme.md @@ -182,11 +182,6 @@ The default export is `ReactMarkdown`. * `disallowedElements` (`Array`, optional)\ tag names to disallow (cannot combine w/ `allowedElements`), all tag names are allowed by default -* `includeElementIndex` (`boolean`, default: `false`)\ - pass the `index` (number of elements before it) and `siblingCount` (number - of elements in parent) as props to all components -* `rawSourcePos` (`boolean`, default: `false`)\ - pass a `sourcePosition` prop to all components with their [position][] * `rehypePlugins` (`Array`, optional)\ list of [rehype plugins][rehype-plugins] to use * `remarkPlugins` (`Array`, optional)\ @@ -195,8 +190,6 @@ The default export is `ReactMarkdown`. options to pass through to [`remark-rehype`][remark-rehype] * `skipHtml` (`boolean`, default: `false`)\ ignore HTML in markdown completely -* `sourcePos` (`boolean`, default: `false`)\ - pass a `data-sourcepos` prop to all components with a serialized position * `transformImageUri` (`(src, alt, title) => string`, default: [`uriTransformer`][uri-transformer])\ change URLs on images; @@ -349,9 +342,9 @@ ReactDom.render( children={markdown} components={{ code(props) { - const {children, className, inline, node, ...rest} = props + const {children, className, node, ...rest} = props const match = /language-(\w+)/.exec(className || '') - return !inline && match ? ( + return match ? ( a), '

    a

    ') + assert.equal(asHtml(), '

    a

    ') }) await t.test('should warn w/ `source`', function () { @@ -33,7 +33,7 @@ test('react-markdown', async function (t) { console.warn = capture // @ts-expect-error: check how the runtime handles untyped `source`. - assert.equal(asHtml(b), '

    b

    ') + assert.equal(asHtml(), '') assert.equal( message, '[react-markdown] Warning: please use `children` instead of `source` (see for more info)' @@ -86,10 +86,10 @@ test('react-markdown', async function (t) { console.warn = capture // @ts-expect-error: check how the runtime handles invalid `children`. - assert.equal(asHtml(), '') + assert.equal(asHtml(), '') assert.equal( message, - '[react-markdown] Warning: please pass a string as `children` (not: `false`)' + '[react-markdown] Warning: please pass a string as `children` (not: `true`)' ) console.error = error @@ -119,8 +119,11 @@ test('react-markdown', async function (t) { console.warn = capture - // @ts-expect-error: check how the runtime handles deprecated `allowDangerousHtml`. - assert.equal(asHtml(a), '

    a

    ') + assert.equal( + // @ts-expect-error: check how the runtime handles deprecated `allowDangerousHtml`. + asHtml(), + '

    a

    ' + ) assert.equal( message, '[react-markdown] Warning: please remove `allowDangerousHtml` (see for more info)' @@ -139,11 +142,30 @@ test('react-markdown', async function (t) { await t.test('should support `className`', function () { assert.equal( - asHtml(a), + asHtml(), '

    a

    ' ) }) + await t.test('should support `className` (if w/o root)', function () { + assert.equal( + asHtml( + + ), + '
    ' + ) + + function plugin() { + /** + * @returns {Root} + */ + return function () { + // @ts-expect-error: check how non-roots are handled. + return {type: 'comment', value: 'things!'} + } + } + }) + await t.test('should support a block quote', function () { assert.equal( asHtml(), @@ -505,13 +527,6 @@ test('react-markdown', async function (t) { assert.equal(actual, '

    abc

    ') }) - await t.test('should support `sourcePos`', function () { - assert.equal( - asHtml(), - '

    a

    ' - ) - }) - await t.test( 'should support `allowedElements` (drop unlisted nodes)', function () { @@ -622,6 +637,7 @@ test('react-markdown', async function (t) { components={{ p(props) { const {node, ...rest} = props + assert.deepEqual(rest, {children: 'a'}) return
    } }} @@ -632,6 +648,12 @@ test('react-markdown', async function (t) { }) await t.test('should fail on an invalid component', function () { + const warn = console.warn + /** @type {unknown} */ + let message + + console.error = capture + assert.throws(function () { asHtml( ) - }, /Unexpected value `123` for `h1`, expected component or tag name/) - }) + }, /Element type is invalid/) - await t.test('should support `null`, `undefined` in components', function () { - assert.equal( - asHtml( - - ), - '

    a

    ' - ) + console.error = warn + + assert.match(String(message), /Warning: React.jsx: type is invalid/) + + /** + * @param {unknown} d + * @returns {undefined} + */ + function capture(d) { + message = d + } }) - await t.test('should support `components` (headings; `level`)', function () { + await t.test('should support `components` (headings)', function () { let calls = 0 assert.equal( @@ -677,18 +695,18 @@ test('react-markdown', async function (t) { assert.equal(calls, 2) /** - * @param {HeadingProps} props + * @param {JSX.IntrinsicElements['h1'] & ExtraProps} props */ function heading(props) { - const {level, node, ...rest} = props - assert.equal(typeof level, 'number') + const {node, ...rest} = props + assert(node) + assert(node.tagName === 'h1' || node.tagName === 'h2') calls++ - const H = `h${level}` - return + return } }) - await t.test('should support `components` (code; `inline`)', function () { + await t.test('should support `components` (code)', function () { let calls = 0 assert.equal( asHtml( @@ -696,9 +714,9 @@ test('react-markdown', async function (t) { children={'```\na\n```\n\n\tb\n\n`c`'} components={{ code(props) { - const {inline, node, ...rest} = props - // To do: this should always be boolean on `code`? - assert(inline === undefined || typeof inline === 'boolean') + const {node, ...rest} = props + assert(node) + assert(node.tagName === 'code') calls++ return } @@ -711,90 +729,80 @@ test('react-markdown', async function (t) { assert.equal(calls, 3) }) - await t.test( - 'should support `components` (li; `checked`, `index`, `ordered`)', - function () { - let calls = 0 + await t.test('should support `components` (li)', function () { + let calls = 0 - assert.equal( - asHtml( - = 0, true) - calls++ - return
  • - } - }} - remarkPlugins={[remarkGfm]} - /> - ), - '
      \n
    • a
    • \n
    \n
      \n
    1. b
    2. \n
    ' - ) + assert.equal( + asHtml( + + } + }} + remarkPlugins={[remarkGfm]} + /> + ), + '
      \n
    • a
    • \n
    \n
      \n
    1. b
    2. \n
    ' + ) - assert.equal(calls, 2) - } - ) + assert.equal(calls, 2) + }) - await t.test( - 'should support `components` (ol; `depth`, `ordered`)', - function () { - let calls = 0 + await t.test('should support `components` (ol)', function () { + let calls = 0 - assert.equal( - asHtml( - - } - }} - /> - ), - '
      \n
    1. a
    2. \n
    ' - ) + assert.equal( + asHtml( + + } + }} + /> + ), + '
      \n
    1. a
    2. \n
    ' + ) - assert.equal(calls, 1) - } - ) + assert.equal(calls, 1) + }) - await t.test( - 'should support `components` (ul; `depth`, `ordered`)', - function () { - let calls = 0 + await t.test('should support `components` (ul)', function () { + let calls = 0 - assert.equal( - asHtml( - - } - }} - /> - ), - '
      \n
    • a
    • \n
    ' - ) + assert.equal( + asHtml( + + } + }} + /> + ), + '
      \n
    • a
    • \n
    ' + ) - assert.equal(calls, 1) - } - ) + assert.equal(calls, 1) + }) - await t.test('should support `components` (tr; `isHeader`)', function () { + await t.test('should support `components` (tr)', function () { let calls = 0 assert.equal( @@ -803,8 +811,9 @@ test('react-markdown', async function (t) { children={'|a|\n|-|\n|b|'} components={{ tr(props) { - const {isHeader, node, ...rest} = props - assert.equal(typeof isHeader, 'boolean') + const {node, ...rest} = props + assert(node) + assert(node.tagName === 'tr') calls++ return } @@ -818,7 +827,7 @@ test('react-markdown', async function (t) { assert.equal(calls, 2) }) - await t.test('should support `components` (td, th; `isHeader`)', function () { + await t.test('should support `components` (td, th)', function () { let tdCalls = 0 let thCalls = 0 @@ -828,14 +837,16 @@ test('react-markdown', async function (t) { children={'|a|\n|-|\n|b|'} components={{ td(props) { - const {isHeader, node, ...rest} = props - assert.equal(isHeader, false) + const {node, ...rest} = props + assert(node) + assert(node.tagName === 'td') tdCalls++ return }, th(props) { - const {isHeader, node, ...rest} = props - assert.equal(isHeader, true) + const {node, ...rest} = props + assert(node) + assert(node.tagName === 'th') thCalls++ return } @@ -890,60 +901,6 @@ test('react-markdown', async function (t) { assert.equal(calls, 1) }) - await t.test( - 'should support `rawSourcePos` (pass `sourcePosition` to components)', - function () { - let calls = 0 - assert.equal( - asHtml( - - } - }} - /> - ), - '

    a

    ' - ) - - assert.equal(calls, 1) - } - ) - - await t.test( - 'should support `includeElementIndex` (pass `index` to components)', - function () { - let calls = 0 - assert.equal( - asHtml( - {rest.children}

    - } - }} - /> - ), - '

    a

    ' - ) - assert.equal(calls, 1) - } - ) - await t.test('should support plugins (`remark-gfm`)', function () { assert.equal( asHtml(), @@ -1201,10 +1158,8 @@ test('react-markdown', async function (t) { } }) - await t.test('should fail on a plugin replacing `root`', function () { - assert.throws(function () { - asHtml() - }, /Unexpected `comment` node, expected `root/) + await t.test('should not fail on a plugin replacing `root`', function () { + assert.equal(asHtml(), '') function plugin() { /** From abb9a6944f4788be0b0beadde7cd9db9cc077568 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Wed, 27 Sep 2023 13:11:11 +0200 Subject: [PATCH 060/125] Refactor to use `Markdown` as identifier --- index.js | 2 +- lib/index.js | 4 ++-- package.json | 2 +- readme.md | 38 +++++++++++++++++++------------------- 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/index.js b/index.js index eb6590e0..a2df3084 100644 --- a/index.js +++ b/index.js @@ -4,4 +4,4 @@ * @typedef {import('./lib/index.js').Options} Options */ -export {ReactMarkdown as default, uriTransformer} from './lib/index.js' +export {Markdown as default, uriTransformer} from './lib/index.js' diff --git a/lib/index.js b/lib/index.js index 6c79f953..881ebc7f 100644 --- a/lib/index.js +++ b/lib/index.js @@ -150,7 +150,7 @@ const deprecated = { * @returns {JSX.Element} * React element. */ -export function ReactMarkdown(options) { +export function Markdown(options) { const allowedElements = options.allowedElements const allowElement = options.allowElement const children = options.children || '' @@ -299,7 +299,7 @@ export function ReactMarkdown(options) { } } -ReactMarkdown.propTypes = { +Markdown.propTypes = { // Core options: children: PropTypes.string, // Layout options: diff --git a/package.json b/package.json index 44e7bb7a..c3f0d1d3 100644 --- a/package.json +++ b/package.json @@ -117,7 +117,7 @@ "xo": "^0.56.0" }, "scripts": { - "build": "tsc --build --clean && tsc --build && 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;})));\"", + "build": "tsc --build --clean && tsc --build && type-coverage && esbuild index.js --bundle --minify --target=es2015 --outfile=react-markdown.min.js --global-name=Markdown --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.Markdown = f()); }(this, (function () { 'use strict';\" --footer:js=\"return Markdown;})));\"", "format": "remark . --frail --output --quiet && prettier . --log-level warn --write && xo --fix", "prepack": "npm run build && npm run format", "test": "npm run build && npm run format && npm run test-coverage", diff --git a/readme.md b/readme.md index 1b6f5500..0d375536 100644 --- a/readme.md +++ b/readme.md @@ -96,14 +96,14 @@ npm install react-markdown In Deno with [`esm.sh`][esmsh]: ```js -import ReactMarkdown from 'https://esm.sh/react-markdown@7' +import Markdown from 'https://esm.sh/react-markdown@7' ``` In browsers with [`esm.sh`][esmsh]: ```html ``` @@ -113,10 +113,10 @@ A basic hello world: ```jsx import React from 'react' -import ReactMarkdown from 'react-markdown' +import Markdown from 'react-markdown' import ReactDom from 'react-dom' -ReactDom.render(# Hello, *world*!, document.body) +ReactDom.render(# Hello, *world*!, document.body) ```
    @@ -137,13 +137,13 @@ tables, tasklists and URLs directly): ```jsx import React from 'react' import ReactDom from 'react-dom' -import ReactMarkdown from 'react-markdown' +import Markdown from 'react-markdown' import remarkGfm from 'remark-gfm' const markdown = `Just a link: https://reactjs.com.` ReactDom.render( - , + , document.body ) ``` @@ -163,7 +163,7 @@ ReactDom.render( This package exports the following identifier: [`uriTransformer`][uri-transformer]. -The default export is `ReactMarkdown`. +The default export is `Markdown`. ### `props` @@ -221,7 +221,7 @@ tasklists and URLs directly: ```jsx import React from 'react' -import ReactMarkdown from 'react-markdown' +import Markdown from 'react-markdown' import ReactDom from 'react-dom' import remarkGfm from 'remark-gfm' @@ -240,7 +240,7 @@ A table: ` ReactDom.render( - , + , document.body ) ``` @@ -291,14 +291,14 @@ second. ```jsx import React from 'react' -import ReactMarkdown from 'react-markdown' +import Markdown from 'react-markdown' import ReactDom from 'react-dom' import remarkGfm from 'remark-gfm' ReactDom.render( - + This ~is not~ strikethrough, but ~~this is~~! - , + , document.body ) ``` @@ -325,7 +325,7 @@ In this case, we apply syntax highlighting with the seriously super amazing ```jsx import React from 'react' import ReactDom from 'react-dom' -import ReactMarkdown from 'react-markdown' +import Markdown from 'react-markdown' import {Prism as SyntaxHighlighter} from 'react-syntax-highlighter' import {dark} from 'react-syntax-highlighter/dist/esm/styles/prism' @@ -338,7 +338,7 @@ console.log('It works!') ` ReactDom.render( - @@ -516,7 +516,7 @@ Some *emphasis* and strong!
  • ` ReactDom.render( - , + , document.body ) ``` @@ -542,7 +542,7 @@ markdown! You can also change the things that come from markdown: ```jsx - Date: Wed, 27 Sep 2023 13:15:33 +0200 Subject: [PATCH 061/125] Refactor to move files around --- .gitignore | 1 - .remarkignore | 1 - package.json | 6 +++--- test/loader.js => script/load-jsx.js | 0 test/test.jsx => test.jsx | 6 +++--- 5 files changed, 6 insertions(+), 8 deletions(-) delete mode 100644 .remarkignore rename test/loader.js => script/load-jsx.js (100%) rename test/test.jsx => test.jsx (99%) diff --git a/.gitignore b/.gitignore index a78a71ba..fdbc06a8 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,3 @@ node_modules/ .DS_Store react-markdown.min.js yarn.lock -!/lib/complex-types.d.ts diff --git a/.remarkignore b/.remarkignore deleted file mode 100644 index ebb6519a..00000000 --- a/.remarkignore +++ /dev/null @@ -1 +0,0 @@ -/test/fixtures/ diff --git a/package.json b/package.json index c3f0d1d3..0a6786e1 100644 --- a/package.json +++ b/package.json @@ -121,8 +121,8 @@ "format": "remark . --frail --output --quiet && prettier . --log-level warn --write && xo --fix", "prepack": "npm run build && npm run format", "test": "npm run build && npm run format && npm run test-coverage", - "test-api": "node --conditions development --experimental-loader=./test/loader.js --no-warnings test/test.jsx", - "test-coverage": "c8 --100 --reporter lcov npm run test-api" + "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" }, "prettier": { "bracketSpacing": false, @@ -182,7 +182,7 @@ }, { "files": [ - "test/**/*.jsx" + "**/*.jsx" ], "rules": { "no-unused-vars": "off" diff --git a/test/loader.js b/script/load-jsx.js similarity index 100% rename from test/loader.js rename to script/load-jsx.js diff --git a/test/test.jsx b/test.jsx similarity index 99% rename from test/test.jsx rename to test.jsx index 35580e9e..ae6bca61 100644 --- a/test/test.jsx +++ b/test.jsx @@ -1,7 +1,7 @@ /* @jsxRuntime automatic @jsxImportSource react */ /** * @typedef {import('hast').Root} Root - * @typedef {import('../index.js').ExtraProps} ExtraProps + * @typedef {import('./index.js').ExtraProps} ExtraProps */ import assert from 'node:assert/strict' @@ -11,11 +11,11 @@ import rehypeRaw from 'rehype-raw' import remarkGfm from 'remark-gfm' import remarkToc from 'remark-toc' import {visit} from 'unist-util-visit' -import Markdown from '../index.js' +import Markdown from './index.js' test('react-markdown', async function (t) { await t.test('should expose the public api', async function () { - assert.deepEqual(Object.keys(await import('../index.js')).sort(), [ + assert.deepEqual(Object.keys(await import('./index.js')).sort(), [ 'default', 'uriTransformer' ]) From e12b5e9f6d4cda3e3b885020627316c1a37e0f1e Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Wed, 27 Sep 2023 13:20:08 +0200 Subject: [PATCH 062/125] Remove `prop-types` --- lib/index.js | 57 ---------------------------------------------------- test.jsx | 4 ---- 2 files changed, 61 deletions(-) diff --git a/lib/index.js b/lib/index.js index 881ebc7f..cef02788 100644 --- a/lib/index.js +++ b/lib/index.js @@ -94,7 +94,6 @@ */ import {toJsxRuntime} from 'hast-util-to-jsx-runtime' -import PropTypes from 'prop-types' // @ts-expect-error: untyped. import {Fragment, jsx, jsxs} from 'react/jsx-runtime' import remarkParse from 'remark-parse' @@ -299,62 +298,6 @@ export function Markdown(options) { } } -Markdown.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.bool, - PropTypes.string, - PropTypes.object, - PropTypes.func, - PropTypes.arrayOf( - // prettier-ignore - // type-coverage:ignore-next-line - PropTypes.any - ) - ]) - ) - ]) - ), - rehypePlugins: PropTypes.arrayOf( - PropTypes.oneOfType([ - PropTypes.object, - PropTypes.func, - PropTypes.arrayOf( - PropTypes.oneOfType([ - PropTypes.bool, - PropTypes.string, - PropTypes.object, - PropTypes.func, - PropTypes.arrayOf( - // prettier-ignore - // type-coverage:ignore-next-line - PropTypes.any - ) - ]) - ) - ]) - ), - // Transform options: - skipHtml: PropTypes.bool, - transformLinkUri: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]), - transformImageUri: PropTypes.func, - components: PropTypes.object -} - /** * Make a URL safe. * diff --git a/test.jsx b/test.jsx index ae6bca61..9489c5ef 100644 --- a/test.jsx +++ b/test.jsx @@ -55,7 +55,6 @@ test('react-markdown', async function (t) { /** @type {unknown} */ let message - console.error = function () {} console.warn = capture // @ts-expect-error: check how the runtime handles invalid `children`. @@ -65,7 +64,6 @@ test('react-markdown', async function (t) { '[react-markdown] Warning: please pass a string as `children` (not: `1`)' ) - console.error = error console.warn = warn /** @@ -82,7 +80,6 @@ test('react-markdown', async function (t) { /** @type {unknown} */ let message - console.error = function () {} console.warn = capture // @ts-expect-error: check how the runtime handles invalid `children`. @@ -92,7 +89,6 @@ test('react-markdown', async function (t) { '[react-markdown] Warning: please pass a string as `children` (not: `true`)' ) - console.error = error console.warn = warn /** From 4eb7aa0fc4b966381b2203154b812783663fd2ec Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Wed, 27 Sep 2023 15:10:37 +0200 Subject: [PATCH 063/125] Change to throw errors for removed props --- lib/index.js | 75 ++++++++++++++++++++-------------- package.json | 1 + test.jsx | 111 +++++++++------------------------------------------ 3 files changed, 64 insertions(+), 123 deletions(-) diff --git a/lib/index.js b/lib/index.js index cef02788..869526b0 100644 --- a/lib/index.js +++ b/lib/index.js @@ -27,10 +27,12 @@ * * @typedef Deprecation * Deprecation. + * @property {string} from + * Old field. * @property {string} id * ID in readme. - * @property {string} [to] - * Field to use instead (optional). + * @property {keyof Options} [to] + * New field. * * @typedef Options * Configuration. @@ -93,6 +95,7 @@ * Transformed URL (optional). */ +import {unreachable} from 'devlop' import {toJsxRuntime} from 'hast-util-to-jsx-runtime' // @ts-expect-error: untyped. import {Fragment, jsx, jsxs} from 'react/jsx-runtime' @@ -114,31 +117,37 @@ const emptyPlugins = [] const emptyRemarkRehypeOptions = {allowDangerousHtml: true} // Mutable because we `delete` any time it’s used and a message is sent. -/** @type {Record>} */ -const deprecated = { - astPlugins: {id: 'remove-buggy-html-in-markdown-parser'}, - allowDangerousHtml: {id: 'remove-buggy-html-in-markdown-parser'}, - allowNode: { +/** @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' }, - allowedTypes: { + { + from: 'allowedTypes', id: 'replace-allownode-allowedtypes-and-disallowedtypes', to: 'allowedElements' }, - disallowedTypes: { + { + from: 'disallowedTypes', id: 'replace-allownode-allowedtypes-and-disallowedtypes', to: 'disallowedElements' }, - escapeHtml: {id: 'remove-buggy-html-in-markdown-parser'}, - includeElementIndex: {id: '#remove-includeelementindex-option'}, - includeNodeIndex: {id: 'change-includenodeindex-to-includeelementindex'}, - plugins: {id: 'change-plugins-to-remarkplugins', to: 'remarkPlugins'}, - rawSourcePos: {id: '#remove-rawsourcepos-option'}, - renderers: {id: 'change-renderers-to-components', to: 'components'}, - source: {id: 'change-source-to-children', to: 'children'}, - sourcePos: {id: '#remove-sourcepos-option'} -} + {from: 'escapeHtml', id: 'remove-buggy-html-in-markdown-parser'}, + {from: 'includeElementIndex', id: '#remove-includeelementindex-option'}, + { + from: 'includeNodeIndex', + id: 'change-includenodeindex-to-includeelementindex' + }, + {from: 'plugins', id: 'change-plugins-to-remarkplugins', to: 'remarkPlugins'}, + {from: 'rawSourcePos', id: '#remove-rawsourcepos-option'}, + {from: 'renderers', id: 'change-renderers-to-components', to: 'components'}, + {from: 'source', id: 'change-source-to-children', to: 'children'}, + {from: 'sourcePos', id: '#remove-sourcepos-option'} +] /** * Component to render markdown. @@ -183,29 +192,35 @@ export function Markdown(options) { if (typeof children === 'string') { file.value = children } else { - console.warn( - '[react-markdown] Warning: please pass a string as `children` (not: `' + + unreachable( + 'Unexpected value `' + children + - '`)' + '` for `children` prop, expected `string`' ) } if (allowedElements && disallowedElements) { - throw new TypeError( + unreachable( 'Unexpected combined `allowedElements` and `disallowedElements`, expected one or the other' ) } - for (const key in deprecated) { + for (const deprecation of deprecations) { // To do: use `Object.hasOwn`. - 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)` + if (own.call(options, deprecation.from)) { + unreachable( + 'Unexpected `' + + deprecation.from + + '` prop, ' + + (deprecation.to + ? 'use `' + deprecation.to + '` instead' + : 'remove it') + + ' (see <' + + changelog + + '#' + + deprecation.id + + '> for more info)' ) - delete deprecated[key] } } diff --git a/package.json b/package.json index 0a6786e1..e488f501 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "dependencies": { "@types/hast": "^3.0.0", "@types/prop-types": "^15.0.0", + "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "mdast-util-to-hast": "^13.0.0", "prop-types": "^15.0.0", diff --git a/test.jsx b/test.jsx index 9489c5ef..83fe93f7 100644 --- a/test.jsx +++ b/test.jsx @@ -25,79 +25,25 @@ test('react-markdown', async function (t) { assert.equal(asHtml(), '

    a

    ') }) - await t.test('should warn w/ `source`', function () { - const warn = console.warn - /** @type {unknown} */ - let message - - console.warn = capture - - // @ts-expect-error: check how the runtime handles untyped `source`. - assert.equal(asHtml(), '') - assert.equal( - message, - '[react-markdown] Warning: please use `children` instead of `source` (see for more info)' - ) - - console.warn = warn - - /** - * @param {unknown} d - * @returns {undefined} - */ - function capture(d) { - message = d - } + await t.test('should throw w/ `source`', function () { + assert.throws(function () { + // @ts-expect-error: check how the runtime handles untyped `source`. + asHtml() + }, /Unexpected `source` prop, use `children` instead/) }) - await t.test('should warn w/ non-string children (number)', function () { - const {error, warn} = console - /** @type {unknown} */ - let message - - console.warn = capture - - // @ts-expect-error: check how the runtime handles invalid `children`. - assert.equal(asHtml(), '') - assert.equal( - message, - '[react-markdown] Warning: please pass a string as `children` (not: `1`)' - ) - - console.warn = warn - - /** - * @param {unknown} d - * @returns {undefined} - */ - function capture(d) { - message = d - } + await t.test('should throw w/ non-string children (number)', function () { + assert.throws(function () { + // @ts-expect-error: check how the runtime handles invalid `children`. + asHtml() + }, /Unexpected value `1` for `children` prop, expected `string`/) }) - await t.test('should warn w/ non-string children (boolean)', function () { - const {error, warn} = console - /** @type {unknown} */ - let message - - console.warn = capture - - // @ts-expect-error: check how the runtime handles invalid `children`. - assert.equal(asHtml(), '') - assert.equal( - message, - '[react-markdown] Warning: please pass a string as `children` (not: `true`)' - ) - - console.warn = warn - - /** - * @param {unknown} d - * @returns {undefined} - */ - function capture(d) { - message = d - } + await t.test('should throw w/ non-string children (boolean)', function () { + assert.throws(function () { + // @ts-expect-error: check how the runtime handles invalid `children`. + asHtml() + }, /Unexpected value `true` for `children` prop, expected `string`/) }) await t.test('should support `null` as children', function () { @@ -109,31 +55,10 @@ test('react-markdown', async function (t) { }) await t.test('should warn w/ `allowDangerousHtml`', function () { - const warn = console.warn - /** @type {unknown} */ - let message - - console.warn = capture - - assert.equal( + assert.throws(function () { // @ts-expect-error: check how the runtime handles deprecated `allowDangerousHtml`. - asHtml(), - '

    a

    ' - ) - assert.equal( - message, - '[react-markdown] Warning: please remove `allowDangerousHtml` (see for more info)' - ) - - console.warn = warn - - /** - * @param {unknown} d - * @returns {undefined} - */ - function capture(d) { - message = d - } + asHtml() + }, /Unexpected `allowDangerousHtml` prop, remove it/) }) await t.test('should support `className`', function () { From c0dfbd6e09509d9c6e296b8d3d9a8692d3ae1927 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Wed, 27 Sep 2023 15:19:22 +0200 Subject: [PATCH 064/125] Remove UMD bundle from package --- .eslintignore | 2 -- .prettierignore | 2 -- package.json | 6 ++---- tsconfig.json | 2 +- 4 files changed, 3 insertions(+), 9 deletions(-) delete mode 100644 .eslintignore 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/.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/package.json b/package.json index e488f501..82497e1c 100644 --- a/package.json +++ b/package.json @@ -71,12 +71,10 @@ "type": "module", "main": "index.js", "types": "index.d.ts", - "unpkg": "react-markdown.min.js", "files": [ "lib/", "index.d.ts", - "index.js", - "react-markdown.min.js" + "index.js" ], "dependencies": { "@types/hast": "^3.0.0", @@ -118,7 +116,7 @@ "xo": "^0.56.0" }, "scripts": { - "build": "tsc --build --clean && tsc --build && type-coverage && esbuild index.js --bundle --minify --target=es2015 --outfile=react-markdown.min.js --global-name=Markdown --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.Markdown = f()); }(this, (function () { 'use strict';\" --footer:js=\"return Markdown;})));\"", + "build": "tsc --build --clean && tsc --build && type-coverage", "format": "remark . --frail --output --quiet && prettier . --log-level warn --write && xo --fix", "prepack": "npm run build && npm run format", "test": "npm run build && npm run format && npm run test-coverage", diff --git a/tsconfig.json b/tsconfig.json index 7baf4f67..06e5468a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,6 +11,6 @@ "strict": true, "target": "es2020" }, - "exclude": ["coverage/", "node_modules/", "**/*.min.js"], + "exclude": ["coverage/", "node_modules/"], "include": ["**/*.js", "**/*.jsx", "lib/complex-types.d.ts"] } From ec2b13463788527bf2279743fb6909c61d51f4ec Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Wed, 27 Sep 2023 15:22:17 +0200 Subject: [PATCH 065/125] Change to require React 18 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit React 16 *may* currently still work depending on your bundler. But it doesn’t work in Node, because the React team uses an incorrect export map to expose their JSX runtime in 16 (and 17). --- package.json | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 82497e1c..42b8afde 100644 --- a/package.json +++ b/package.json @@ -78,11 +78,9 @@ ], "dependencies": { "@types/hast": "^3.0.0", - "@types/prop-types": "^15.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "mdast-util-to-hast": "^13.0.0", - "prop-types": "^15.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", @@ -90,8 +88,8 @@ "vfile": "^6.0.0" }, "peerDependencies": { - "@types/react": ">=16", - "react": ">=16" + "@types/react": ">=18", + "react": ">=18" }, "devDependencies": { "@types/node": "^20.0.0", From eca5e6b9d1ab5f4bb7722f49e7532aaaade94f5a Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Wed, 27 Sep 2023 15:49:27 +0200 Subject: [PATCH 066/125] Replace `transformImageUri`, `transformLinkUri` w/ `urlTransform` --- changelog.md | 8 +++++ index.js | 2 +- lib/index.js | 87 ++++++++++++++++++---------------------------------- package.json | 1 + test.jsx | 79 ++++++++++------------------------------------- 5 files changed, 56 insertions(+), 121 deletions(-) diff --git a/changelog.md b/changelog.md index 5fd55f18..ffa6ca75 100644 --- a/changelog.md +++ b/changelog.md @@ -4,6 +4,14 @@ All notable changes will be documented in this file. ## 9.0.0 - unreleased +### 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 `includeElementIndex` option The `includeElementIndex` option was removed, so `index` is never passed to diff --git a/index.js b/index.js index a2df3084..1c36187e 100644 --- a/index.js +++ b/index.js @@ -4,4 +4,4 @@ * @typedef {import('./lib/index.js').Options} Options */ -export {Markdown as default, uriTransformer} from './lib/index.js' +export {Markdown as default, defaultUrlTransform} from './lib/index.js' diff --git a/lib/index.js b/lib/index.js index 869526b0..b8edf44f 100644 --- a/lib/index.js +++ b/lib/index.js @@ -59,44 +59,28 @@ * Options to pass through to `remark-rehype`. * @property {boolean | null | undefined} [skipHtml=false] * Ignore HTML in markdown completely (default: `false`). - * @property {TransformLink | false | null | undefined} [transformLinkUri] - * Change URLs on images (default: `uriTransformer`); - * pass `false` to allow all URLs, which is unsafe - * @property {TransformImage | false | null | undefined} [transformImageUri] - * Change URLs on links (default: `uriTransformer`); - * pass `false` to allow all URLs, which is unsafe * @property {boolean | null | undefined} [unwrapDisallowed=false] * Extract (unwrap) the children of not allowed elements (default: `false`); * normally when say `strong` is disallowed, 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`) * - * @callback TransformImage - * Transform URLs on images. - * @param {string} src + * @callback UrlTransform + * Transform URLs. + * @param {string} url * URL to transform. - * @param {string} alt - * Alt text. - * @param {string | null} title - * Title. - * To do: pass `undefined`. + * @param {string} key + * Property name (example: `'href'`). + * @param {Readonly} node + * Node. * @returns {string | null | undefined} * Transformed URL (optional). - * - * @callback TransformLink - * Transform URLs on links. - * @param {string} href - * URL to transform. - * @param {ReadonlyArray} children - * Content. - * @param {string | null} title - * Title. - * To do: pass `undefined`. - * @returns {string} - * Transformed URL (optional). */ import {unreachable} from 'devlop' import {toJsxRuntime} from 'hast-util-to-jsx-runtime' +import {urlAttributes} from 'html-url-attributes' // @ts-expect-error: untyped. import {Fragment, jsx, jsxs} from 'react/jsx-runtime' import remarkParse from 'remark-parse' @@ -146,7 +130,9 @@ const deprecations = [ {from: 'rawSourcePos', id: '#remove-rawsourcepos-option'}, {from: 'renderers', id: 'change-renderers-to-components', to: 'components'}, {from: 'source', id: 'change-source-to-children', to: 'children'}, - {from: 'sourcePos', id: '#remove-sourcepos-option'} + {from: 'sourcePos', id: '#remove-sourcepos-option'}, + {from: 'transformImageUri', id: '#add-urltransform', to: 'urlTransform'}, + {from: 'transformLinkUri', id: '#add-urltransform', to: 'urlTransform'} ] /** @@ -171,15 +157,8 @@ export function Markdown(options) { ? {...options.remarkRehypeOptions, ...emptyRemarkRehypeOptions} : emptyRemarkRehypeOptions const skipHtml = options.skipHtml - const transformImageUri = - options.transformImageUri === undefined - ? uriTransformer - : options.transformImageUri - const transformLinkUri = - options.transformLinkUri === undefined - ? uriTransformer - : options.transformLinkUri const unwrapDisallowed = options.unwrapDisallowed + const urlTransform = options.urlTransform || defaultUrlTransform const processor = unified() .use(remarkParse) @@ -265,26 +244,19 @@ export function Markdown(options) { return index } - if (transformLinkUri && node.type === 'element' && node.tagName === 'a') { - node.properties.href = transformLinkUri( - String(node.properties.href || ''), - node.children, - // To do: pass `undefined`. - typeof node.properties.title === 'string' ? node.properties.title : null - ) - } - - if ( - transformImageUri && - node.type === 'element' && - node.tagName === 'img' - ) { - node.properties.src = transformImageUri( - String(node.properties.src || ''), - String(node.properties.alt || ''), - // To do: pass `undefined`. - typeof node.properties.title === 'string' ? node.properties.title : null - ) + if (node.type === 'element') { + /** @type {string} */ + let key + + for (key in urlAttributes) { + if (own.call(urlAttributes, key) && own.call(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') { @@ -316,13 +288,14 @@ export function Markdown(options) { /** * Make a URL safe. * + * @satisfies {UrlTransform} * @param {string} value * URL. * @returns {string} * Safe URL. */ -export function uriTransformer(value) { - const url = (value || '').trim() +export function defaultUrlTransform(value) { + const url = value.trim() const first = url.charAt(0) if (first === '#' || first === '/') { diff --git a/package.json b/package.json index 42b8afde..804d2e94 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "@types/hast": "^3.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", diff --git a/test.jsx b/test.jsx index 83fe93f7..120bf26a 100644 --- a/test.jsx +++ b/test.jsx @@ -17,7 +17,7 @@ test('react-markdown', async function (t) { await t.test('should expose the public api', async function () { assert.deepEqual(Object.keys(await import('./index.js')).sort(), [ 'default', - 'uriTransformer' + 'defaultUrlTransform' ]) }) @@ -345,15 +345,15 @@ test('react-markdown', async function (t) { ) }) - await t.test('should support `transformLinkUri`', function () { + await t.test('should support `urlTransform` (`href` on `a`)', function () { assert.equal( asHtml( @@ -362,15 +362,15 @@ test('react-markdown', async function (t) { ) }) - await t.test('should support `transformLinkUri` w/ empty URLs', function () { + await t.test('should support `urlTransform` w/ empty URLs', function () { assert.equal( asHtml( @@ -379,30 +379,15 @@ test('react-markdown', async function (t) { ) }) - await t.test( - 'should support turning off `transformLinkUri` (dangerous)', - function () { - assert.equal( - asHtml( - - ), - '

    ' - ) - } - ) - - await t.test('should support `transformImageUri`', function () { + await t.test('should support `urlTransform` (`src` on `img`)', function () { assert.equal( asHtml( @@ -411,38 +396,6 @@ test('react-markdown', async function (t) { ) }) - await t.test('should support `transformImageUri` w/ empty URLs', function () { - assert.equal( - asHtml( - - ), - '

    ' - ) - }) - - await t.test( - 'should support turning off `transformImageUri` (dangerous)', - function () { - assert.equal( - asHtml( - - ), - '

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

    abc

    ') From a1fc6d9c541360cdb4ba902e1583f4f247f4d34b Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Wed, 27 Sep 2023 16:11:08 +0200 Subject: [PATCH 067/125] Refactor `package.json` some more --- lib/index.js | 20 +++++++------------- package.json | 24 ++---------------------- 2 files changed, 9 insertions(+), 35 deletions(-) diff --git a/lib/index.js b/lib/index.js index b8edf44f..3bbe47bd 100644 --- a/lib/index.js +++ b/lib/index.js @@ -260,13 +260,11 @@ export function Markdown(options) { } if (node.type === 'element') { - let remove = false - - if (allowedElements) { - remove = !allowedElements.includes(node.tagName) - } else if (disallowedElements) { - remove = disallowedElements.includes(node.tagName) - } + let remove = allowedElements + ? !allowedElements.includes(node.tagName) + : disallowedElements + ? disallowedElements.includes(node.tagName) + : false if (!remove && allowElement && typeof index === 'number') { remove = !allowElement(node, index, parent) @@ -307,11 +305,7 @@ export function defaultUrlTransform(value) { return url } - let index = -1 - - while (++index < safeProtocols.length) { - const protocol = safeProtocols[index] - + for (const protocol of safeProtocols) { if ( colon === protocol.length && url.slice(0, protocol.length).toLowerCase() === protocol @@ -320,7 +314,7 @@ export function defaultUrlTransform(value) { } } - index = url.indexOf('?') + let index = url.indexOf('?') if (index !== -1 && colon > index) { return url } diff --git a/package.json b/package.json index 804d2e94..0953edcf 100644 --- a/package.json +++ b/package.json @@ -98,10 +98,7 @@ "@types/react-dom": "^18.0.0", "c8": "^8.0.0", "esbuild": "^0.19.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": "^3.0.0", "react": "^18.0.0", "react-dom": "^18.0.0", @@ -153,10 +150,6 @@ "atLeast": 100, "detail": true, "ignoreCatch": true, - "#": "below is ignored because some proptypes will `any`; to do: remove prop-types?", - "ignoreFiles": [ - "lib/index.d.ts" - ], "strict": true }, "xo": { @@ -165,19 +158,6 @@ ], "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": [ "**/*.jsx" @@ -189,8 +169,8 @@ ], "prettier": true, "rules": { - "n/file-extension-in-import": "off", - "unicorn/prefer-string-replace-all": "off" + "complexity": "off", + "n/file-extension-in-import": "off" } } } From 08ead9ef38765d7b273065edb8d72ab2aa5ab512 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Wed, 27 Sep 2023 17:34:43 +0200 Subject: [PATCH 068/125] Refactor to improve safe URL detection --- lib/index.js | 39 +++------------------------------------ test.jsx | 25 ++++--------------------- 2 files changed, 7 insertions(+), 57 deletions(-) diff --git a/lib/index.js b/lib/index.js index 3bbe47bd..2ebf91f7 100644 --- a/lib/index.js +++ b/lib/index.js @@ -81,6 +81,7 @@ import {unreachable} from 'devlop' import {toJsxRuntime} from 'hast-util-to-jsx-runtime' import {urlAttributes} from 'html-url-attributes' +import {sanitizeUri} from 'micromark-util-sanitize-uri' // @ts-expect-error: untyped. import {Fragment, jsx, jsxs} from 'react/jsx-runtime' import remarkParse from 'remark-parse' @@ -89,8 +90,6 @@ import {unified} from 'unified' import {visit} from 'unist-util-visit' import {VFile} from 'vfile' -const safeProtocols = ['http', 'https', 'mailto', 'tel'] - const own = {}.hasOwnProperty const changelog = 'https://github.com/remarkjs/react-markdown/blob/main/changelog.md' @@ -99,6 +98,7 @@ const changelog = 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>} */ @@ -293,38 +293,5 @@ export function Markdown(options) { * Safe URL. */ export function defaultUrlTransform(value) { - const url = value.trim() - const first = url.charAt(0) - - if (first === '#' || first === '/') { - return url - } - - const colon = url.indexOf(':') - if (colon === -1) { - return url - } - - for (const protocol of safeProtocols) { - if ( - colon === protocol.length && - url.slice(0, protocol.length).toLowerCase() === protocol - ) { - return url - } - } - - let index = url.indexOf('?') - if (index !== -1 && colon > index) { - return url - } - - index = url.indexOf('#') - if (index !== -1 && colon > index) { - return url - } - - // To do: is there an alternative? - // eslint-disable-next-line no-script-url - return 'javascript:void(0)' + return sanitizeUri(value, safeProtocol) } diff --git a/test.jsx b/test.jsx index 120bf26a..1a56847c 100644 --- a/test.jsx +++ b/test.jsx @@ -288,43 +288,31 @@ test('react-markdown', async function (t) { }) await t.test('should make a `javascript:` URL safe', function () { - const consoleError = console.error - console.error = noop assert.equal( asHtml(), - '

    ' + '

    ' ) - console.error = consoleError }) await t.test('should make a `vbscript:` URL safe', function () { - const consoleError = console.error - console.error = noop assert.equal( asHtml(), - '

    ' + '

    ' ) - console.error = consoleError }) await t.test('should make a `VBSCRIPT:` URL safe', function () { - const consoleError = console.error - console.error = noop assert.equal( asHtml(), - '

    ' + '

    ' ) - console.error = consoleError }) await t.test('should make a `file:` URL safe', function () { - const consoleError = console.error - console.error = noop assert.equal( asHtml(), - '

    ' + '

    ' ) - console.error = consoleError }) await t.test('should allow an empty URL', function () { @@ -1054,8 +1042,3 @@ test('react-markdown', async function (t) { function asHtml(input) { return renderToStaticMarkup(input) } - -/** - * @returns {undefined} - */ -function noop() {} From d056940648449e6103b777edbeea57cfe545deea Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Wed, 27 Sep 2023 17:35:13 +0200 Subject: [PATCH 069/125] Refactor docs --- index.js | 4 +- lib/index.js | 38 ++--- package.json | 1 + readme.md | 433 ++++++++++++++++++++++++++++++++------------------- 4 files changed, 296 insertions(+), 180 deletions(-) diff --git a/index.js b/index.js index 1c36187e..ff903ce0 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,9 @@ /** - * @typedef {import('hast-util-to-jsx-runtime').Components} Components * @typedef {import('hast-util-to-jsx-runtime').ExtraProps} ExtraProps + * @typedef {import('./lib/index.js').AllowElement} AllowElement + * @typedef {import('./lib/index.js').Components} Components * @typedef {import('./lib/index.js').Options} Options + * @typedef {import('./lib/index.js').UrlTransform} UrlTransform */ export {Markdown as default, defaultUrlTransform} from './lib/index.js' diff --git a/lib/index.js b/lib/index.js index 2ebf91f7..335d8493 100644 --- a/lib/index.js +++ b/lib/index.js @@ -7,7 +7,7 @@ * @typedef {import('hast').Nodes} Nodes * @typedef {import('hast').Parents} Parents * @typedef {import('hast').Root} Root - * @typedef {import('hast-util-to-jsx-runtime').Components} Components + * @typedef {import('hast-util-to-jsx-runtime').Components} JsxRuntimeComponents * @typedef {import('remark-rehype').Options} RemarkRehypeOptions * @typedef {import('unist-util-visit').BuildVisitor} Visitor * @typedef {import('unified').PluggableList} PluggableList @@ -15,7 +15,7 @@ /** * @callback AllowElement - * Decide if `element` should be allowed. + * Filter elements. * @param {Readonly} element * Element to check. * @param {number} index @@ -25,6 +25,9 @@ * @returns {boolean | null | undefined} * Whether to allow `element` (default: `false`). * + * @typedef {Partial} Components + * Map tag names to components. + * * @typedef Deprecation * Deprecation. * @property {string} from @@ -37,20 +40,20 @@ * @typedef Options * Configuration. * @property {AllowElement | null | undefined} [allowElement] - * Function called to check if an element is allowed (when truthy) or not, - * `allowedElements` or `disallowedElements` is used first! + * Filter elements (optional); + * `allowedElements` / `disallowedElements` is used first. * @property {ReadonlyArray | null | undefined} [allowedElements] - * Tag names to allow (cannot combine w/ `disallowedElements`), all tag names - * are allowed by default. + * Tag names to allow (default: all tag names); + * cannot combine w/ `disallowedElements`. * @property {string | null | undefined} [children] - * Markdown to parse. + * Markdown. * @property {string | null | undefined} [className] - * Wrap the markdown in a `div` with this class name. - * @property {Partial | null | undefined} [components] - * Map tag names to React components. + * Wrap in a `div` with this class name. + * @property {Components | null | undefined} [components] + * Map tag names to components. * @property {ReadonlyArray | null | undefined} [disallowedElements] - * Tag names to disallow (cannot combine w/ `allowedElements`), all tag names - * are allowed by default. + * 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] @@ -60,16 +63,16 @@ * @property {boolean | null | undefined} [skipHtml=false] * Ignore HTML in markdown completely (default: `false`). * @property {boolean | null | undefined} [unwrapDisallowed=false] - * Extract (unwrap) the children of not allowed elements (default: `false`); - * normally when say `strong` is disallowed, it and it’s children are dropped, + * 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`) * * @callback UrlTransform - * Transform URLs. + * Transform all URLs. * @param {string} url - * URL to transform. + * URL. * @param {string} key * Property name (example: `'href'`). * @param {Readonly} node @@ -139,8 +142,7 @@ const deprecations = [ * Component to render markdown. * * @param {Readonly} options - * Configuration (required). - * Note: React types require that props are passed. + * Props. * @returns {JSX.Element} * React element. */ diff --git a/package.json b/package.json index 0953edcf..188d85ba 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,7 @@ "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", + "micromark-util-sanitize-uri": "^2.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", diff --git a/readme.md b/readme.md index 0d375536..7df6a928 100644 --- a/readme.md +++ b/readme.md @@ -18,13 +18,13 @@ React component to render markdown. ## Feature highlights -* [x] **[safe][security] by default** +* [x] **[safe][section-security] by default** (no `dangerouslySetInnerHTML` or XSS attacks) -* [x] **[components][]** +* [x] **[components][section-components]** (pass your own component to use instead of `

    ` for `## hi`) -* [x] **[plugins][]** +* [x] **[plugins][section-plugins]** (many plugins you can pick and choose from) -* [x] **[compliant][syntax]** +* [x] **[compliant][section-syntax]** (100% to CommonMark, 100% to GFM with a plugin) ## Contents @@ -34,8 +34,13 @@ React component to render markdown. * [Install](#install) * [Use](#use) * [API](#api) - * [`props`](#props) - * [`uriTransformer`](#uritransformer) + * [`Markdown`](#markdown) + * [`defaultUrlTransform(url)`](#defaulturltransformurl) + * [`AllowElement`](#allowelement) + * [`Components`](#components) + * [`ExtraProps`](#extraprops) + * [`Options`](#options) + * [`UrlTransform`](#urltransform) * [Examples](#examples) * [Use a plugin](#use-a-plugin) * [Use a plugin with options](#use-a-plugin-with-options) @@ -57,25 +62,23 @@ React component to render markdown. 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 learn markdown, see this [cheatsheet and tutorial][commonmark-help] * to try out `react-markdown`, see [our demo][demo] ## 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][], specifically [remark][] for +markdown and [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. @@ -87,7 +90,7 @@ If you instead want to use JavaScript and JSX *inside* markdown files, use ## 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][]: ```sh npm install react-markdown @@ -96,14 +99,14 @@ npm install react-markdown In Deno with [`esm.sh`][esmsh]: ```js -import Markdown from 'https://esm.sh/react-markdown@7' +import Markdown from 'https://esm.sh/react-markdown@8' ``` In browsers with [`esm.sh`][esmsh]: ```html ``` @@ -113,10 +116,12 @@ A basic hello world: ```jsx import React from 'react' -import Markdown from 'react-markdown' import ReactDom from 'react-dom' +import Markdown from 'react-markdown' -ReactDom.render(# Hello, *world*!, document.body) +const markdown = '# Hi, *Pluto*!' + +ReactDom.render({markdown}, document.body) ```
    @@ -124,15 +129,15 @@ ReactDom.render(# Hello, *world*!, document.body) ```jsx

    - 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): +to use a plugin ([`remark-gfm`][remark-gfm], which adds support for +footnotes, strikethrough, tables, tasklists and URLs directly): ```jsx import React from 'react' @@ -140,10 +145,10 @@ import ReactDom from 'react-dom' 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( - , + {markdown}, document.body ) ``` @@ -153,7 +158,7 @@ ReactDom.render( ```jsx

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

    ``` @@ -162,67 +167,148 @@ ReactDom.render( ## API This package exports the following identifier: -[`uriTransformer`][uri-transformer]. -The default export is `Markdown`. - -### `props` - -* `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! -* `allowedElements` (`Array`, optional)\ - tag names to allow (cannot combine w/ `disallowedElements`), all tag names - are allowed by default -* `children` (`string`, optional)\ - markdown to parse -* `className` (`string?`)\ - wrap the markdown in a `div` with this class name -* `components` (`Record`, optional)\ - map tag names to React components -* `disallowedElements` (`Array`, optional)\ - tag names to disallow (cannot combine w/ `allowedElements`), all tag names - are allowed by default -* `rehypePlugins` (`Array`, optional)\ - list of [rehype plugins][rehype-plugins] to use -* `remarkPlugins` (`Array`, optional)\ - list of [remark plugins][remark-plugins] to use -* `remarkRehypeOptions` (`Object?`, optional)\ - options to pass through to [`remark-rehype`][remark-rehype] -* `skipHtml` (`boolean`, default: `false`)\ - ignore HTML in markdown completely -* `transformImageUri` (`(src, alt, title) => string`, default: - [`uriTransformer`][uri-transformer])\ - change URLs on images; - pass `false` to allow all URLs, which is unsafe (see [security][]) -* `transformLinkUri` (`(href, children, title) => string`, default: - [`uriTransformer`][uri-transformer])\ - change URLs on links; - pass `false` to allow all URLs, which is unsafe (see [security][]) -* `unwrapDisallowed` (`boolean`, default: `false`)\ - extract (unwrap) the children of not allowed elements; - normally when say `strong` is disallowed, it and it’s children are dropped, +[`defaultUrlTransform`][api-default-url-transform]. +The default export is [`Markdown`][api-markdown]. + +### `Markdown` + +Component to render markdown. + +###### Parameters + +* `options` ([`Options`][api-options]) + — props + +###### Returns + +React element (`JSX.Element`). + +### `defaultUrlTransform(url)` + +Make a URL safe. + +###### Parameters + +* `url` (`string`) + — URL + +###### Returns + +Safe URL (`string`). + +### `AllowElement` + +Filter elements (TypeScript type). + +###### Fields + +* `node` ([`Element` from `hast`][hast-element]) + — element to check +* `index` (`number | undefined`) + — index of `element` in `parent` +* `parent` ([`Node` from `hast`][hast-node]) + — parent of `element` + +###### Returns + +Whether to allow `element` (`boolean`, optional). + +### `Components` + +Map tag names to components (TypeScript type). + +###### Type + +```ts +import type {Element} from 'hast' + +type Components = Partial<{ + [TagName in keyof JSX.IntrinsicElements]: + // Class component: + | (new (props: JSX.IntrinsicElements[TagName] & ExtraProps) => JSX.ElementClass) + // Function component: + | ((props: JSX.IntrinsicElements[TagName] & ExtraProps) => JSX.Element | string | null | undefined) + // Tag name: + | keyof JSX.IntrinsicElements +}> +``` + +### `ExtraProps` + +Extra fields we pass to components (TypeScript type). + +###### Fields + +* `node` ([`Element` from `hast`][hast-element], optional) + — original node + +### `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 +* `className` (`string`, optional) + — wrap in a `div` with this class name +* `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][rehype-plugins] to use +* `remarkPlugins` (`Array`, optional) + — list of [remark plugins][remark-plugins] to use +* `remarkRehypeOptions` ([`Options` from + `remark-rehype`][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` -### `uriTransformer` +Transform URLs (TypeScript type). -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`). +###### Fields -See the [source code here][uri]. +* `url` (`string`) + — URL +* `key` (`string`, example: `'href'`) + — property name +* `node` ([`Element` from `hast`][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`][remark-gfm], which adds support for strikethrough, +tables, tasklists and URLs directly: ```jsx import React from 'react' -import Markdown from 'react-markdown' import ReactDom from 'react-dom' +import Markdown from 'react-markdown' import remarkGfm from 'remark-gfm' const markdown = `A paragraph with *emphasis* and **strong importance**. @@ -240,7 +326,7 @@ A table: ` ReactDom.render( - , + {markdown}, document.body ) ``` @@ -259,21 +345,21 @@ ReactDom.render( https://reactjs.org.

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

      A table:

      - - + +
      abab
      @@ -287,17 +373,20 @@ 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`][remark-gfm] has an option to allow only double tildes for +strikethrough: ```jsx import React from 'react' -import Markdown from 'react-markdown' import ReactDom from 'react-dom' +import Markdown from 'react-markdown' import remarkGfm from 'remark-gfm' +const markdown = 'This ~is not~ strikethrough, but ~~this is~~!' + ReactDom.render( - This ~is not~ strikethrough, but ~~this is~~! + {markdown} , document.body ) @@ -322,6 +411,8 @@ In this case, we apply syntax highlighting with the seriously super amazing [`react-syntax-highlighter`][react-syntax-highlighter] by [**@conorhastings**][conor]: + + ```jsx import React from 'react' import ReactDom from 'react-dom' @@ -380,25 +471,24 @@ 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`][remark-math]) is used to support math in markdown, and a transform plugin -([`rehype-katex`][katex]) to render that math. +([`rehype-katex`][rehype-katex]) to render that math. ```jsx import React from 'react' import ReactDom from 'react-dom' import Markdown from 'react-markdown' -import remarkMath from 'remark-math' 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 +const markdown = `The lift coefficient ($C_L$) is a dimensionless coefficient.` + ReactDom.render( - , + + {markdown} + , document.body ) ``` @@ -409,14 +499,12 @@ ReactDom.render( ```jsx

      The lift coefficient ( - - - - {/* … */} - - + + + {/* … */} + + ) is a dimensionless coefficient. @@ -452,15 +540,23 @@ 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. +It exports the additional types +[`AllowElement`][api-allow-element], +[`ExtraProps`][api-extra-props], +[`Components`][api-components], +[`Options`][api-options], and +[`UrlTransform`][api-url-transform]. ## 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@^8`, +compatible with Node.js 12. + 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 @@ -501,7 +597,7 @@ 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`][rehype-raw]: ```jsx import React from 'react' @@ -509,14 +605,14 @@ import ReactDom from 'react-dom' import Markdown from 'react-markdown' import rehypeRaw from 'rehype-raw' -const input = `

      +const markdown = `
      Some *emphasis* and strong!
      ` ReactDom.render( - , + {markdown}, document.body ) ``` @@ -525,15 +621,17 @@ ReactDom.render( Show equivalent JSX ```jsx -
      -

      Some emphasis and strong!

      +
      +

      + 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! @@ -543,7 +641,6 @@ You can also change the things that come from markdown: ```jsx Date: Wed, 27 Sep 2023 17:36:51 +0200 Subject: [PATCH 070/125] Change to use `exports` --- package.json | 3 +-- readme.md | 4 ++-- test.jsx | 6 +++--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 188d85ba..18a4c63e 100644 --- a/package.json +++ b/package.json @@ -69,8 +69,7 @@ ], "sideEffects": false, "type": "module", - "main": "index.js", - "types": "index.d.ts", + "exports": "./index.js", "files": [ "lib/", "index.d.ts", diff --git a/readme.md b/readme.md index 7df6a928..42485968 100644 --- a/readme.md +++ b/readme.md @@ -685,9 +685,9 @@ It lets you define your own schema of what is and isn’t allowed. ## Related -* [`MDX`](https://github.com/mdx-js/mdx) +* [`MDX`][mdx] — JSX *in* markdown -* [`remark-gfm`](https://github.com/remarkjs/remark-gfm) +* [`remark-gfm`][remark-gfm] — add support for GitHub flavored markdown support * [`react-remark`][react-remark] — modern hook based alternative diff --git a/test.jsx b/test.jsx index 1a56847c..4d1ba14a 100644 --- a/test.jsx +++ b/test.jsx @@ -1,21 +1,21 @@ /* @jsxRuntime automatic @jsxImportSource react */ /** * @typedef {import('hast').Root} Root - * @typedef {import('./index.js').ExtraProps} ExtraProps + * @typedef {import('react-markdown').ExtraProps} ExtraProps */ import assert from 'node:assert/strict' import test from 'node:test' import {renderToStaticMarkup} from 'react-dom/server' +import Markdown from 'react-markdown' import rehypeRaw from 'rehype-raw' import remarkGfm from 'remark-gfm' import remarkToc from 'remark-toc' import {visit} from 'unist-util-visit' -import Markdown from './index.js' test('react-markdown', async function (t) { await t.test('should expose the public api', async function () { - assert.deepEqual(Object.keys(await import('./index.js')).sort(), [ + assert.deepEqual(Object.keys(await import('react-markdown')).sort(), [ 'default', 'defaultUrlTransform' ]) From b67d7149fe3e450ba2c3d190ab689a9067abeed2 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Wed, 27 Sep 2023 17:38:47 +0200 Subject: [PATCH 071/125] Change to require Node.js 16 --- lib/index.js | 3 +-- readme.md | 10 +++++----- tsconfig.json | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/lib/index.js b/lib/index.js index 335d8493..0bf98706 100644 --- a/lib/index.js +++ b/lib/index.js @@ -187,8 +187,7 @@ export function Markdown(options) { } for (const deprecation of deprecations) { - // To do: use `Object.hasOwn`. - if (own.call(options, deprecation.from)) { + if (Object.hasOwn(options, deprecation.from)) { unreachable( 'Unexpected `' + deprecation.from + diff --git a/readme.md b/readme.md index 42485968..6b5b2470 100644 --- a/readme.md +++ b/readme.md @@ -200,7 +200,7 @@ Safe URL (`string`). Filter elements (TypeScript type). -###### Fields +###### Parameters * `node` ([`Element` from `hast`][hast-element]) — element to check @@ -284,7 +284,7 @@ Configuration (TypeScript type). Transform URLs (TypeScript type). -###### Fields +###### Parameters * `url` (`string`) — URL @@ -554,8 +554,8 @@ versions of Node.js. 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@^8`, -compatible with Node.js 12. +This means we try to keep the current release line, `react-markdown@^9`, +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 @@ -690,7 +690,7 @@ It lets you define your own schema of what is and isn’t allowed. * [`remark-gfm`][remark-gfm] — add support for GitHub flavored markdown support * [`react-remark`][react-remark] - — modern hook based alternative + — hook based alternative * [`rehype-react`][rehype-react] — turn HTML into React elements diff --git a/tsconfig.json b/tsconfig.json index 06e5468a..0fe0d02d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,7 @@ "lib": ["dom", "es2022"], "module": "node16", "strict": true, - "target": "es2020" + "target": "es2022" }, "exclude": ["coverage/", "node_modules/"], "include": ["**/*.js", "**/*.jsx", "lib/complex-types.d.ts"] From 72e68d28f6295a7a4fc58c9a4a4e00f0c002f21f Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Wed, 27 Sep 2023 17:51:57 +0200 Subject: [PATCH 072/125] Add docs on line endings Closes Gh-749. --- readme.md | 60 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/readme.md b/readme.md index 6b5b2470..0de921d7 100644 --- a/readme.md +++ b/readme.md @@ -53,6 +53,7 @@ React component to render markdown. * [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) @@ -671,6 +672,65 @@ Every component will receive a `node`. This is the original [`Element` from `hast`][hast-element] element being turned into a React element. +## 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: + +```jsx +// 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 expresion as an only child: +{markdown} +``` + +👆 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: + +```jsx + + # 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: + +```jsx + # Hi This is **not** a paragraph. +``` + +Instead, to pass markdown to `Markdown`, you can use an expression: +with a template literal: + +```jsx +{` +# 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: + +```jsx +{` + # This is **not** a heading, it’s an indented code block +`} +``` + ## Security Use of `react-markdown` is secure by default. From 6360bc2379eae9e3d3cfb5ff697f2a87ed2ac34d Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Wed, 27 Sep 2023 18:06:54 +0200 Subject: [PATCH 073/125] 9.0.0 --- changelog.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++---- lib/index.js | 7 ++++--- package.json | 2 +- readme.md | 2 +- 4 files changed, 53 insertions(+), 9 deletions(-) diff --git a/changelog.md b/changelog.md index ffa6ca75..a596f364 100644 --- a/changelog.md +++ b/changelog.md @@ -2,7 +2,44 @@ All notable changes will be documented in this file. -## 9.0.0 - unreleased +## 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` @@ -12,7 +49,13 @@ 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 `includeElementIndex` option +### 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. @@ -39,13 +82,13 @@ function rehypePluginAddingIndex() { } ``` -### Remove `rawSourcePos` option +### 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` option +### Remove `sourcePos` The `sourcePos` option was removed, so `data-sourcepos` is never passed to elements. diff --git a/lib/index.js b/lib/index.js index 0bf98706..fa2336de 100644 --- a/lib/index.js +++ b/lib/index.js @@ -124,16 +124,17 @@ const deprecations = [ to: 'disallowedElements' }, {from: 'escapeHtml', id: 'remove-buggy-html-in-markdown-parser'}, - {from: 'includeElementIndex', id: '#remove-includeelementindex-option'}, + {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-option'}, + {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-option'}, + {from: 'sourcePos', id: '#remove-sourcepos'}, {from: 'transformImageUri', id: '#add-urltransform', to: 'urlTransform'}, {from: 'transformLinkUri', id: '#add-urltransform', to: 'urlTransform'} ] diff --git a/package.json b/package.json index 18a4c63e..1a21d10e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-markdown", - "version": "8.0.7", + "version": "9.0.0", "description": "React component to render markdown", "license": "MIT", "keywords": [ diff --git a/readme.md b/readme.md index 0de921d7..11036dae 100644 --- a/readme.md +++ b/readme.md @@ -100,7 +100,7 @@ npm install react-markdown In Deno with [`esm.sh`][esmsh]: ```js -import Markdown from 'https://esm.sh/react-markdown@8' +import Markdown from 'https://esm.sh/react-markdown@9' ``` In browsers with [`esm.sh`][esmsh]: From 5445cbbd41d86e53464be13a342a4c85f2d290af Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Wed, 27 Sep 2023 18:09:54 +0200 Subject: [PATCH 074/125] Fix to close details --- changelog.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/changelog.md b/changelog.md index a596f364..0f91e902 100644 --- a/changelog.md +++ b/changelog.md @@ -82,6 +82,8 @@ function rehypePluginAddingIndex() { } ``` +
    + ### Remove `rawSourcePos` The `rawSourcePos` option was removed, so `sourcePos` is never passed to @@ -116,6 +118,8 @@ function rehypePluginAddingIndex() { } ``` + + ### Remove extra props passed to certain components When overwriting components, these props are no longer passed: From 1d5cbf583f59cf6c48e971e12dccdb44c479754b Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Thu, 28 Sep 2023 09:23:50 +0200 Subject: [PATCH 075/125] Refactor some more --- lib/index.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/index.js b/lib/index.js index fa2336de..abe0ec21 100644 --- a/lib/index.js +++ b/lib/index.js @@ -93,7 +93,6 @@ import {unified} from 'unified' import {visit} from 'unist-util-visit' import {VFile} from 'vfile' -const own = {}.hasOwnProperty const changelog = 'https://github.com/remarkjs/react-markdown/blob/main/changelog.md' @@ -251,7 +250,10 @@ export function Markdown(options) { let key for (key in urlAttributes) { - if (own.call(urlAttributes, key) && own.call(node.properties, key)) { + 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)) { From bc0936443f7589c38219147231f2cef2b1076db5 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Thu, 28 Sep 2023 09:25:33 +0200 Subject: [PATCH 076/125] Fix typos --- changelog.md | 2 +- readme.md | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/changelog.md b/changelog.md index 0f91e902..02e13fe3 100644 --- a/changelog.md +++ b/changelog.md @@ -124,7 +124,7 @@ function rehypePluginAddingIndex() { When overwriting components, these props are no longer passed: -* `inline` on `code`: +* `inline` on `code` — create a plugin or use `pre` for the block * `level` on `h1`, `h2`, `h3`, `h4`, `h5`, `h6` — check `node.tagName` instead diff --git a/readme.md b/readme.md index 11036dae..5e4751f4 100644 --- a/readme.md +++ b/readme.md @@ -136,9 +136,9 @@ ReactDom.render({markdown}, document.body) -Here is an example that shows passing the markdown as a string and how -to use a plugin ([`remark-gfm`][remark-gfm], which adds support for -footnotes, strikethrough, tables, tasklists and URLs directly): +Here is an example that shows how to use a plugin ([`remark-gfm`][remark-gfm], +which adds support for footnotes, strikethrough, tables, tasklists and URLs +directly): ```jsx import React from 'react' @@ -685,7 +685,7 @@ const markdown = ` ` // Pass the value as an expresion as an only child: -{markdown} +const result = {markdown} ``` 👆 That works. From 2245c6409c37baccc0442a2ff37ac9777530120d Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Sun, 15 Oct 2023 18:37:12 +0200 Subject: [PATCH 077/125] Fix typo Closes GH-782. --- changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 02e13fe3..aadd70d1 100644 --- a/changelog.md +++ b/changelog.md @@ -75,7 +75,7 @@ function rehypePluginAddingIndex() { return function (tree) { visit(tree, function (node, index) { if (node.type === 'element' && typeof index === 'number') { - node.properties === index + node.properties.index = index } }) } From 55d8d831b7d41c1c36b04dd8b25df46732870ff8 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Mon, 6 Nov 2023 14:51:25 +0100 Subject: [PATCH 078/125] Refactor docs to use `createRoot` Closes GH-779. Co-authored-by: tris203 --- readme.md | 50 ++++++++++++++++++++++---------------------------- 1 file changed, 22 insertions(+), 28 deletions(-) diff --git a/readme.md b/readme.md index 5e4751f4..65e99838 100644 --- a/readme.md +++ b/readme.md @@ -117,12 +117,12 @@ A basic hello world: ```jsx import React from 'react' -import ReactDom from 'react-dom' +import {createRoot} from 'react-dom/client' import Markdown from 'react-markdown' const markdown = '# Hi, *Pluto*!' -ReactDom.render({markdown}, document.body) +createRoot(document.body).render({markdown}) ```
    @@ -142,15 +142,14 @@ directly): ```jsx import React from 'react' -import ReactDom from 'react-dom' +import {createRoot} from 'react-dom/client' import Markdown from 'react-markdown' import remarkGfm from 'remark-gfm' const markdown = `Just a link: www.nasa.gov.` -ReactDom.render( - {markdown}, - document.body +createRoot(document.body).render( + {markdown} ) ``` @@ -308,7 +307,7 @@ tables, tasklists and URLs directly: ```jsx import React from 'react' -import ReactDom from 'react-dom' +import {createRoot} from 'react-dom/client' import Markdown from 'react-markdown' import remarkGfm from 'remark-gfm' @@ -326,9 +325,8 @@ A table: | - | - | ` -ReactDom.render( - {markdown}, - document.body +createRoot(document.body).render( + {markdown} ) ``` @@ -379,17 +377,16 @@ strikethrough: ```jsx import React from 'react' -import ReactDom from 'react-dom' +import {createRoot} from 'react-dom/client' import Markdown from 'react-markdown' import remarkGfm from 'remark-gfm' const markdown = 'This ~is not~ strikethrough, but ~~this is~~!' -ReactDom.render( +createRoot(document.body).render( {markdown} - , - document.body + ) ``` @@ -416,7 +413,7 @@ In this case, we apply syntax highlighting with the seriously super amazing ```jsx import React from 'react' -import ReactDom from 'react-dom' +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' @@ -429,7 +426,7 @@ console.log('It works!') ~~~ ` -ReactDom.render( +createRoot(document.body).render( ) : ( @@ -451,8 +448,7 @@ ReactDom.render( ) } }} - />, - document.body + /> ) ``` @@ -478,7 +474,7 @@ is used to support math in markdown, and a transform plugin ```jsx import React from 'react' -import ReactDom from 'react-dom' +import {createRoot} from 'react-dom/client' import Markdown from 'react-markdown' import rehypeKatex from 'rehype-katex' import remarkMath from 'remark-math' @@ -486,11 +482,10 @@ import 'katex/dist/katex.min.css' // `rehype-katex` does not import the CSS for const markdown = `The lift coefficient ($C_L$) is a dimensionless coefficient.` -ReactDom.render( +createRoot(document.body).render( {markdown} - , - document.body + ) ``` @@ -602,7 +597,7 @@ can spare the bundle size (±60kb minzipped), then you can use ```jsx import React from 'react' -import ReactDom from 'react-dom' +import {createRoot} from 'react-dom/client' import Markdown from 'react-markdown' import rehypeRaw from 'rehype-raw' @@ -612,9 +607,8 @@ Some *emphasis* and strong! ` -ReactDom.render( - {markdown}, - document.body +createRoot(document.body).render( + {markdown} ) ``` From d8e37874915b79a2ee20ec2944793ef5ca7a2379 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Mon, 13 Nov 2023 14:55:00 +0100 Subject: [PATCH 079/125] Fix double encoding in new url transform Closes GH-797. --- lib/index.js | 24 ++++++++++++++++++++++-- package.json | 1 - test.jsx | 7 +++++++ 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/lib/index.js b/lib/index.js index abe0ec21..414a534d 100644 --- a/lib/index.js +++ b/lib/index.js @@ -84,7 +84,6 @@ import {unreachable} from 'devlop' import {toJsxRuntime} from 'hast-util-to-jsx-runtime' import {urlAttributes} from 'html-url-attributes' -import {sanitizeUri} from 'micromark-util-sanitize-uri' // @ts-expect-error: untyped. import {Fragment, jsx, jsxs} from 'react/jsx-runtime' import remarkParse from 'remark-parse' @@ -297,5 +296,26 @@ export function Markdown(options) { * Safe URL. */ export function defaultUrlTransform(value) { - return sanitizeUri(value, safeProtocol) + // 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 < 0 || + // 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/package.json b/package.json index 1a21d10e..cfdea22e 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,6 @@ "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", - "micromark-util-sanitize-uri": "^2.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", diff --git a/test.jsx b/test.jsx index 4d1ba14a..f803df98 100644 --- a/test.jsx +++ b/test.jsx @@ -326,6 +326,13 @@ test('react-markdown', async function (t) { ) }) + await t.test('should support hash (`&`) in a URL', function () { + assert.equal( + asHtml(), + '

    ' + ) + }) + await t.test('should support hash (`#`) in a URL', function () { assert.equal( asHtml(), From a27d335fc5419db4a2811e7f589d6467218346de Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Mon, 13 Nov 2023 14:58:58 +0100 Subject: [PATCH 080/125] 9.0.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cfdea22e..8edcf1c7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-markdown", - "version": "9.0.0", + "version": "9.0.1", "description": "React component to render markdown", "license": "MIT", "keywords": [ From 78160f5a0877675c1c18417a220f9948de143720 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Mon, 25 Mar 2024 11:34:00 +0100 Subject: [PATCH 081/125] Update dev-dependencies --- lib/index.js | 7 ++++--- package.json | 9 +++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/index.js b/lib/index.js index 414a534d..eb502165 100644 --- a/lib/index.js +++ b/lib/index.js @@ -84,7 +84,6 @@ import {unreachable} from 'devlop' import {toJsxRuntime} from 'hast-util-to-jsx-runtime' import {urlAttributes} from 'html-url-attributes' -// @ts-expect-error: untyped. import {Fragment, jsx, jsxs} from 'react/jsx-runtime' import remarkParse from 'remark-parse' import remarkRehype from 'remark-rehype' @@ -226,7 +225,9 @@ export function Markdown(options) { Fragment, components, ignoreInvalidStyle: true, + // @ts-expect-error: to do: types. jsx, + // @ts-expect-error: to do: types. jsxs, passKeys: true, passNode: true @@ -266,8 +267,8 @@ export function Markdown(options) { let remove = allowedElements ? !allowedElements.includes(node.tagName) : disallowedElements - ? disallowedElements.includes(node.tagName) - : false + ? disallowedElements.includes(node.tagName) + : false if (!remove && allowElement && typeof index === 'number') { remove = !allowElement(node, index, parent) diff --git a/package.json b/package.json index 8edcf1c7..636e5605 100644 --- a/package.json +++ b/package.json @@ -95,8 +95,8 @@ "@types/node": "^20.0.0", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", - "c8": "^8.0.0", - "esbuild": "^0.19.0", + "c8": "^9.0.0", + "esbuild": "^0.20.0", "eslint-plugin-react": "^7.0.0", "prettier": "^3.0.0", "react": "^18.0.0", @@ -108,7 +108,7 @@ "remark-toc": "^9.0.0", "type-coverage": "^2.0.0", "typescript": "^5.0.0", - "xo": "^0.56.0" + "xo": "^0.58.0" }, "scripts": { "build": "tsc --build --clean && tsc --build && type-coverage", @@ -169,7 +169,8 @@ "prettier": true, "rules": { "complexity": "off", - "n/file-extension-in-import": "off" + "n/file-extension-in-import": "off", + "unicorn/prevent-abbreviations": "off" } } } From 7f32314d5bdab70f8551661ca36ef0deef29d02a Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Sun, 16 Jun 2024 17:16:00 +0200 Subject: [PATCH 082/125] Update dev-dependencies --- changelog.md | 780 +++++++++++++++++++++++++-------------------------- package.json | 21 +- readme.md | 216 +++++++------- 3 files changed, 503 insertions(+), 514 deletions(-) diff --git a/changelog.md b/changelog.md index aadd70d1..7fc79cec 100644 --- a/changelog.md +++ b/changelog.md @@ -4,42 +4,42 @@ All notable changes will be documented in this file. ## 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 +* [`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` @@ -124,138 +124,138 @@ function rehypePluginAddingIndex() { 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 +* `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) +* [`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) +* [`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) +* [`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) +* [`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) +* [`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) +* [`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) +* [`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**) +* [`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) +* [`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) +* [`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 +* [`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 @@ -264,51 +264,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 @@ -358,46 +358,46 @@ Now (**fixed**):
    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
    @@ -602,22 +602,22 @@ with React 15 and older. ## 5.0.3 - 2020-10-23 -* [`bb0bdde`](https://github.com/remarkjs/react-markdown/commit/bb0bdde) - Unlock peer dependency on React to allow v17 -* [`24e42bd`](https://github.com/remarkjs/react-markdown/commit/24e42bd) - Fix exception on missing element from `html-to-react` -* [`3d363e9`](https://github.com/remarkjs/react-markdown/commit/3d363e9) - Fix umd browser build +* [`bb0bdde`](https://github.com/remarkjs/react-markdown/commit/bb0bdde) + Unlock peer dependency on React to allow v17 +* [`24e42bd`](https://github.com/remarkjs/react-markdown/commit/24e42bd) + Fix exception on missing element from `html-to-react` +* [`3d363e9`](https://github.com/remarkjs/react-markdown/commit/3d363e9) + Fix umd browser build ## 5.0.2 - 2020-10-23 -* [`4dadaba`](https://github.com/remarkjs/react-markdown/commit/4dadaba) - Fix to allow combining `allowedTypes`, `unwrapDisallowed` in types +* [`4dadaba`](https://github.com/remarkjs/react-markdown/commit/4dadaba) + Fix to allow combining `allowedTypes`, `unwrapDisallowed` in types ## 5.0.1 - 2020-10-21 -* [`c3dc5ee`](https://github.com/remarkjs/react-markdown/commit/c3dc5ee) - Fix to not crash on empty text nodes +* [`c3dc5ee`](https://github.com/remarkjs/react-markdown/commit/c3dc5ee) + Fix to not crash on empty text nodes ## 5.0.0 - 2020-10-19 @@ -640,10 +640,10 @@ places, such as Discourse, Reddit, Stack Overflow, and GitHub. Note that GitHub does extend CommonMark: to match how Markdown works on GitHub, use the [`remark-gfm`](https://github.com/remarkjs/remark-gfm) plugin. -* [`remark-parse@9.0.0`](https://github.com/remarkjs/remark/releases/tag/remark-parse%409.0.0) -* [`remark-parse@8.0.0`](https://github.com/remarkjs/remark/releases/tag/remark-parse%408.0.0) -* [`remark-parse@7.0.0`](https://github.com/remarkjs/remark/releases/tag/remark-parse%407.0.0) -* [`remark-parse@6.0.0`](https://github.com/remarkjs/remark/releases/tag/remark-parse%406.0.0) +* [`remark-parse@9.0.0`](https://github.com/remarkjs/remark/releases/tag/remark-parse%409.0.0) +* [`remark-parse@8.0.0`](https://github.com/remarkjs/remark/releases/tag/remark-parse%408.0.0) +* [`remark-parse@7.0.0`](https://github.com/remarkjs/remark/releases/tag/remark-parse%407.0.0) +* [`remark-parse@6.0.0`](https://github.com/remarkjs/remark/releases/tag/remark-parse%406.0.0) #### New serializer property: `node` @@ -676,497 +676,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 defintions (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/package.json b/package.json index 636e5605..dc1b8b9f 100644 --- a/package.json +++ b/package.json @@ -95,19 +95,18 @@ "@types/node": "^20.0.0", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", - "c8": "^9.0.0", - "esbuild": "^0.20.0", + "c8": "^10.0.0", + "esbuild": "^0.21.0", "eslint-plugin-react": "^7.0.0", "prettier": "^3.0.0", "react": "^18.0.0", "react-dom": "^18.0.0", "rehype-raw": "^7.0.0", - "remark-cli": "^11.0.0", - "remark-gfm": "^4.0.0", - "remark-preset-wooorm": "^9.0.0", + "remark-cli": "^12.0.0", + "remark-preset-wooorm": "^10.0.0", "remark-toc": "^9.0.0", "type-coverage": "^2.0.0", - "typescript": "^5.0.0", + "typescript": "^5.5.1-rc", "xo": "^0.58.0" }, "scripts": { @@ -129,16 +128,6 @@ "remarkConfig": { "plugins": [ "remark-preset-wooorm", - [ - "./node_modules/remark-preset-wooorm/node_modules/remark-gfm/index.js", - { - "tablePipeAlign": false - } - ], - [ - "remark-lint-table-pipe-alignment", - false - ], [ "remark-lint-no-html", false diff --git a/readme.md b/readme.md index 65e99838..b0706e1a 100644 --- a/readme.md +++ b/readme.md @@ -18,46 +18,46 @@ React component to render markdown. ## Feature highlights -* [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) +* [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) - * [`Markdown`](#markdown) - * [`defaultUrlTransform(url)`](#defaulturltransformurl) - * [`AllowElement`](#allowelement) - * [`Components`](#components) - * [`ExtraProps`](#extraprops) - * [`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) -* [Types](#types) -* [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?](#what-is-this) +* [When should I use this?](#when-should-i-use-this) +* [Install](#install) +* [Use](#use) +* [API](#api) + * [`Markdown`](#markdown) + * [`defaultUrlTransform(url)`](#defaulturltransformurl) + * [`AllowElement`](#allowelement) + * [`Components`](#components) + * [`ExtraProps`](#extraprops) + * [`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) +* [Types](#types) +* [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? @@ -66,8 +66,8 @@ that it’ll safely render to React 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][commonmark-help] -* 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][demo] ## When should I use this? @@ -176,8 +176,8 @@ Component to render markdown. ###### Parameters -* `options` ([`Options`][api-options]) - — props +* `options` ([`Options`][api-options]) + — props ###### Returns @@ -189,8 +189,8 @@ Make a URL safe. ###### Parameters -* `url` (`string`) - — URL +* `url` (`string`) + — URL ###### Returns @@ -202,12 +202,12 @@ Filter elements (TypeScript type). ###### Parameters -* `node` ([`Element` from `hast`][hast-element]) - — element to check -* `index` (`number | undefined`) - — index of `element` in `parent` -* `parent` ([`Node` from `hast`][hast-node]) - — parent of `element` +* `node` ([`Element` from `hast`][hast-element]) + — element to check +* `index` (`number | undefined`) + — index of `element` in `parent` +* `parent` ([`Node` from `hast`][hast-node]) + — parent of `element` ###### Returns @@ -239,8 +239,8 @@ Extra fields we pass to components (TypeScript type). ###### Fields -* `node` ([`Element` from `hast`][hast-element], optional) - — original node +* `node` ([`Element` from `hast`][hast-element], optional) + — original node ### `Options` @@ -248,37 +248,37 @@ 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 -* `className` (`string`, optional) - — wrap in a `div` with this class name -* `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][rehype-plugins] to use -* `remarkPlugins` (`Array`, optional) - — list of [remark plugins][remark-plugins] to use -* `remarkRehypeOptions` ([`Options` from - `remark-rehype`][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 +* `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 +* `className` (`string`, optional) + — wrap in a `div` with this class name +* `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][rehype-plugins] to use +* `remarkPlugins` (`Array`, optional) + — list of [remark plugins][remark-plugins] to use +* `remarkRehypeOptions` ([`Options` from + `remark-rehype`][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` @@ -286,12 +286,12 @@ Transform URLs (TypeScript type). ###### Parameters -* `url` (`string`) - — URL -* `key` (`string`, example: `'href'`) - — property name -* `node` ([`Element` from `hast`][hast-element]) - — element to check +* `url` (`string`) + — URL +* `key` (`string`, example: `'href'`) + — property name +* `node` ([`Element` from `hast`][hast-element]) + — element to check ###### Returns @@ -515,13 +515,13 @@ We use [unified][], specifically [remark][] for markdown and [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`][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 ## Syntax @@ -580,11 +580,11 @@ part until you hit the API section is required reading). 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 @@ -739,14 +739,14 @@ It lets you define your own schema of what is and isn’t allowed. ## Related -* [`MDX`][mdx] - — JSX *in* markdown -* [`remark-gfm`][remark-gfm] - — add support for GitHub flavored markdown support -* [`react-remark`][react-remark] - — hook based alternative -* [`rehype-react`][rehype-react] - — turn HTML into React elements +* [`MDX`][mdx] + — JSX *in* markdown +* [`remark-gfm`][remark-gfm] + — add support for GitHub flavored markdown support +* [`react-remark`][react-remark] + — hook based alternative +* [`rehype-react`][rehype-react] + — turn HTML into React elements ## Contribute From aa5933b98a196ce6c78b0676f4de42d679b0e285 Mon Sep 17 00:00:00 2001 From: Remco Haszing Date: Fri, 28 Jun 2024 12:08:02 +0200 Subject: [PATCH 083/125] Refactor to use `@import` to import types Closes GH-836. Reviewed-by: Titus Wormer --- lib/index.js | 19 ++++++------------- test.jsx | 4 ++-- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/lib/index.js b/lib/index.js index eb502165..a0f3e173 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,16 +1,9 @@ -// Register `Raw` in tree: -/// - /** - * @typedef {import('hast').Element} Element - * @typedef {import('hast').ElementContent} ElementContent - * @typedef {import('hast').Nodes} Nodes - * @typedef {import('hast').Parents} Parents - * @typedef {import('hast').Root} Root - * @typedef {import('hast-util-to-jsx-runtime').Components} JsxRuntimeComponents - * @typedef {import('remark-rehype').Options} RemarkRehypeOptions - * @typedef {import('unist-util-visit').BuildVisitor} Visitor - * @typedef {import('unified').PluggableList} PluggableList + * @import {Element, ElementContent, Nodes, Parents, Root} from 'hast' + * @import {Components as JsxRuntimeComponents} from 'hast-util-to-jsx-runtime' + * @import {Options as RemarkRehypeOptions} from 'remark-rehype' + * @import {BuildVisitor} from 'unist-util-visit' + * @import {PluggableList} from 'unified' */ /** @@ -233,7 +226,7 @@ export function Markdown(options) { passNode: true }) - /** @type {Visitor} */ + /** @type {BuildVisitor} */ function transform(node, index, parent) { if (node.type === 'raw' && parent && typeof index === 'number') { if (skipHtml) { diff --git a/test.jsx b/test.jsx index f803df98..91c0c787 100644 --- a/test.jsx +++ b/test.jsx @@ -1,7 +1,7 @@ /* @jsxRuntime automatic @jsxImportSource react */ /** - * @typedef {import('hast').Root} Root - * @typedef {import('react-markdown').ExtraProps} ExtraProps + * @import {Root} from 'hast' + * @import {ExtraProps} from 'react-markdown' */ import assert from 'node:assert/strict' From 7bf4c0404b2854ced2904c1c5831c9a70cdd898c Mon Sep 17 00:00:00 2001 From: Remco Haszing Date: Fri, 28 Jun 2024 13:22:10 +0200 Subject: [PATCH 084/125] Replace `JSX` global with explicit `ReactElement` Closes GH-837. Reviewed-by: Titus Wormer --- lib/index.js | 3 +- test.jsx | 204 +++++++++++++++++++++++++++++---------------------- 2 files changed, 117 insertions(+), 90 deletions(-) diff --git a/lib/index.js b/lib/index.js index a0f3e173..98a62e29 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,6 +1,7 @@ /** * @import {Element, ElementContent, Nodes, Parents, Root} from 'hast' * @import {Components as JsxRuntimeComponents} from 'hast-util-to-jsx-runtime' + * @import {ReactElement} from 'react' * @import {Options as RemarkRehypeOptions} from 'remark-rehype' * @import {BuildVisitor} from 'unist-util-visit' * @import {PluggableList} from 'unified' @@ -134,7 +135,7 @@ const deprecations = [ * * @param {Readonly} options * Props. - * @returns {JSX.Element} + * @returns {ReactElement} * React element. */ export function Markdown(options) { diff --git a/test.jsx b/test.jsx index 91c0c787..82239185 100644 --- a/test.jsx +++ b/test.jsx @@ -1,6 +1,7 @@ /* @jsxRuntime automatic @jsxImportSource react */ /** * @import {Root} from 'hast' + * @import {ComponentProps, ReactElement} from 'react' * @import {ExtraProps} from 'react-markdown' */ @@ -22,55 +23,55 @@ test('react-markdown', async function (t) { }) await t.test('should work', function () { - assert.equal(asHtml(), '

    a

    ') + 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`. - asHtml() + 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`. - asHtml() + 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`. - asHtml() + renderToStaticMarkup() }, /Unexpected value `true` for `children` prop, expected `string`/) }) await t.test('should support `null` as children', function () { - assert.equal(asHtml(), '') + assert.equal(renderToStaticMarkup(), '') }) await t.test('should support `undefined` as children', function () { - assert.equal(asHtml(), '') + assert.equal(renderToStaticMarkup(), '') }) await t.test('should warn w/ `allowDangerousHtml`', function () { assert.throws(function () { // @ts-expect-error: check how the runtime handles deprecated `allowDangerousHtml`. - asHtml() + renderToStaticMarkup() }, /Unexpected `allowDangerousHtml` prop, remove it/) }) await t.test('should support `className`', function () { assert.equal( - asHtml(), + renderToStaticMarkup(), '

    a

    ' ) }) await t.test('should support `className` (if w/o root)', function () { assert.equal( - asHtml( + renderToStaticMarkup( ), '
    ' @@ -89,43 +90,51 @@ test('react-markdown', async function (t) { await t.test('should support a block quote', function () { assert.equal( - asHtml(), + renderToStaticMarkup(), '
    \n

    a

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

    a
    \nb

    ') + assert.equal( + renderToStaticMarkup(), + '

    a
    \nb

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

    a

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

    a

    ') + assert.equal( + renderToStaticMarkup(), + '

    a

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

    a1

    \n

    Footnotes

    \n
      \n
    1. \n

      y

      \n
    2. \n
    \n
    ' @@ -133,72 +142,80 @@ test('react-markdown', async function (t) { }) await t.test('should support a heading', function () { - assert.equal(asHtml(), '

    a

    ') + assert.equal( + renderToStaticMarkup(), + '

    a

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

    <i>a</i>

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

    a

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

    a

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

    a

    ' ) }) await t.test('should support an image reference / definition', function () { assert.equal( - asHtml(), + renderToStaticMarkup(), '

    a

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

    a

    ') + assert.equal( + renderToStaticMarkup(), + '

    a

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

    a

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

    a

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

    a

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

    a

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

    a

    ') + assert.equal(renderToStaticMarkup(), '

    a

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

    a

    ' ) }) await t.test('should support a table (GFM)', function () { assert.equal( - asHtml( + renderToStaticMarkup( ), '
    ') + assert.equal(renderToStaticMarkup(), '
    ') }) await t.test('should support ab absolute path', function () { assert.equal( - asHtml(), + renderToStaticMarkup(), '

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

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

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

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

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

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

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

    ') + assert.equal( + renderToStaticMarkup(), + '

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

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

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

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

    abc

    ') }) @@ -400,7 +425,7 @@ test('react-markdown', async function (t) { 'should support `allowedElements` (drop unlisted nodes)', function () { assert.equal( - asHtml( + renderToStaticMarkup( ), + renderToStaticMarkup( + + ), '

    \n
      \n
    • b
    • \n
    ' ) }) @@ -435,7 +462,7 @@ test('react-markdown', async function (t) { 'should fail for both `allowedElements` and `disallowedElements`', function () { assert.throws(function () { - asHtml( + renderToStaticMarkup( ), + renderToStaticMarkup(), '

    a

    ' ) }) await t.test('should support `components` as functions', function () { assert.equal( - asHtml( + renderToStaticMarkup( & ExtraProps} props */ function heading(props) { const {node, ...rest} = props @@ -578,7 +605,7 @@ test('react-markdown', async function (t) { await t.test('should support `components` (code)', function () { let calls = 0 assert.equal( - asHtml( + renderToStaticMarkup( ), + renderToStaticMarkup( + + ), '

    a b c

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

    c

    ' ) @@ -825,7 +854,7 @@ test('react-markdown', async function (t) { await t.test('should support data properties', function () { assert.equal( - asHtml(), + renderToStaticMarkup(), '

    b

    ' ) @@ -847,7 +876,7 @@ test('react-markdown', async function (t) { await t.test('should support comma separated properties', function () { assert.equal( - asHtml(), + renderToStaticMarkup(), '

    c

    ' ) @@ -869,7 +898,7 @@ test('react-markdown', async function (t) { await t.test('should support `style` properties', function () { assert.equal( - asHtml(), + renderToStaticMarkup(), '

    a

    ' ) @@ -893,7 +922,9 @@ test('react-markdown', async function (t) { 'should support `style` properties w/ vendor prefixes', function () { assert.equal( - asHtml(), + renderToStaticMarkup( + + ), '

    a

    ' ) @@ -916,7 +947,7 @@ test('react-markdown', async function (t) { await t.test('should support broken `style` properties', function () { assert.equal( - asHtml(), + renderToStaticMarkup(), '

    a

    ' ) @@ -938,7 +969,7 @@ test('react-markdown', async function (t) { await t.test('should support SVG elements', function () { assert.equal( - asHtml(), + renderToStaticMarkup(), 'SVG `<circle>` element

    a

    ' ) @@ -983,7 +1014,7 @@ test('react-markdown', async function (t) { await t.test('should support comments (ignore them)', function () { const input = 'a' - const actual = asHtml( + const actual = renderToStaticMarkup( ) const expected = '

    a

    ' @@ -1002,7 +1033,7 @@ test('react-markdown', async function (t) { await t.test('should support table cells w/ style', function () { assert.equal( - asHtml( + renderToStaticMarkup( ), '') + assert.equal( + renderToStaticMarkup(), + '' + ) function plugin() { /** @@ -1041,11 +1075,3 @@ test('react-markdown', async function (t) { } }) }) - -/** - * @param {ReturnType} input - * @returns {string} - */ -function asHtml(input) { - return renderToStaticMarkup(input) -} From fd337ff9e84a5e7ec8f0f225a98494a67abf6804 Mon Sep 17 00:00:00 2001 From: Mayank Date: Mon, 15 Jul 2024 13:18:33 +0530 Subject: [PATCH 085/125] Add missing dev-dependency Closes GH-846. Reviewed-by: Remco Haszing Reviewed-by: Titus Wormer --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index dc1b8b9f..0c384601 100644 --- a/package.json +++ b/package.json @@ -103,6 +103,7 @@ "react-dom": "^18.0.0", "rehype-raw": "^7.0.0", "remark-cli": "^12.0.0", + "remark-gfm": "^4.0.0", "remark-preset-wooorm": "^10.0.0", "remark-toc": "^9.0.0", "type-coverage": "^2.0.0", From ab6703a4234107c72729e01e6a0554c4e0b891a2 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Mon, 19 Aug 2024 13:32:37 +0200 Subject: [PATCH 086/125] Update dev-dependencies --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 0c384601..10a2191f 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,7 @@ "react": ">=18" }, "devDependencies": { - "@types/node": "^20.0.0", + "@types/node": "^22.0.0", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "c8": "^10.0.0", @@ -107,8 +107,8 @@ "remark-preset-wooorm": "^10.0.0", "remark-toc": "^9.0.0", "type-coverage": "^2.0.0", - "typescript": "^5.5.1-rc", - "xo": "^0.58.0" + "typescript": "^5.0.0", + "xo": "^0.59.0" }, "scripts": { "build": "tsc --build --clean && tsc --build && type-coverage", From 70fca29c44951896212d898c72db0dce5fdcf4b5 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Mon, 19 Aug 2024 14:06:41 +0200 Subject: [PATCH 087/125] Update Actions --- .github/workflows/main.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 626f97d8..2ce794f1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -3,17 +3,17 @@ jobs: name: ${{matrix.node}} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: ${{matrix.node}} - run: npm install - run: npm test - - uses: codecov/codecov-action@v3 + - uses: codecov/codecov-action@v4 strategy: matrix: node: - - lts/gallium + - lts/hydrogen - node name: main on: From ac51b50f6930bc7ed0c527fbae7a59c9da538c5e Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Thu, 19 Sep 2024 15:09:11 +0200 Subject: [PATCH 088/125] Update dev-dependencies --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 10a2191f..35a8e9fa 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,7 @@ "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "c8": "^10.0.0", - "esbuild": "^0.21.0", + "esbuild": "^0.23.0", "eslint-plugin-react": "^7.0.0", "prettier": "^3.0.0", "react": "^18.0.0", From dace1076f6cdc8ebe8fe19270633f4cb195ecca7 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Thu, 19 Sep 2024 15:10:26 +0200 Subject: [PATCH 089/125] Add `.tsbuildinfo` to `.gitignore` --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index fdbc06a8..08861b34 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ coverage/ node_modules/ *.d.ts *.log +*.tsbuildinfo .DS_Store react-markdown.min.js yarn.lock From 6962af7135561a91a843a6af10054087248fdec6 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Thu, 19 Sep 2024 15:10:54 +0200 Subject: [PATCH 090/125] Add declaration maps --- .gitignore | 1 + package.json | 1 + tsconfig.json | 1 + 3 files changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 08861b34..f19ff243 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ coverage/ node_modules/ +*.d.ts.map *.d.ts *.log *.tsbuildinfo diff --git a/package.json b/package.json index 35a8e9fa..6c053327 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "exports": "./index.js", "files": [ "lib/", + "index.d.ts.map", "index.d.ts", "index.js" ], diff --git a/tsconfig.json b/tsconfig.json index 0fe0d02d..d13dc27b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,7 @@ "compilerOptions": { "checkJs": true, "customConditions": ["development"], + "declarationMap": true, "declaration": true, "emitDeclarationOnly": true, "exactOptionalPropertyTypes": true, From baad6c53764e34c4ead41e2eaba176acfc87538a Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Thu, 19 Sep 2024 15:11:12 +0200 Subject: [PATCH 091/125] Remove license year --- license | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 277048c850600f54e9556ca8c9c8466812765f62 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Tue, 15 Oct 2024 13:33:36 +0200 Subject: [PATCH 092/125] Update dev-dependencies --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6c053327..c051f5e1 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "c8": "^10.0.0", - "esbuild": "^0.23.0", + "esbuild": "^0.24.0", "eslint-plugin-react": "^7.0.0", "prettier": "^3.0.0", "react": "^18.0.0", From 492b53e0b5af1a8d025da81b68bf7d760673099c Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Tue, 15 Oct 2024 13:33:43 +0200 Subject: [PATCH 093/125] Remove unneeded `ts-expect-error`s --- lib/index.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/index.js b/lib/index.js index 98a62e29..6470cb8f 100644 --- a/lib/index.js +++ b/lib/index.js @@ -219,9 +219,7 @@ export function Markdown(options) { Fragment, components, ignoreInvalidStyle: true, - // @ts-expect-error: to do: types. jsx, - // @ts-expect-error: to do: types. jsxs, passKeys: true, passNode: true From bb6c8e870c2b50e1445d988970f3cfa71b271366 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Tue, 15 Oct 2024 13:34:16 +0200 Subject: [PATCH 094/125] Refactor Actions --- .github/workflows/bb.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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] From a7ca8edfd698d61ebf0ad83bf95cba1a4106f672 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Tue, 15 Oct 2024 13:34:47 +0200 Subject: [PATCH 095/125] Refactor `.editorconfig` --- .editorconfig | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 From 515bf190a06e2510aa4d09d4c186cfa558b75452 Mon Sep 17 00:00:00 2001 From: Deep Pancholi Date: Fri, 25 Oct 2024 01:26:17 -0700 Subject: [PATCH 096/125] Fix typo Closes GH-868. Reviewed-by: Remco Haszing Reviewed-by: Titus Wormer --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index b0706e1a..8c5038b2 100644 --- a/readme.md +++ b/readme.md @@ -678,7 +678,7 @@ const markdown = ` # This is perfect! ` -// Pass the value as an expresion as an only child: +// Pass the value as an expression as an only child: const result = {markdown} ``` From 9eb589e828445916dfb521117040d8d5420a5e9d Mon Sep 17 00:00:00 2001 From: Bob Conan Date: Thu, 21 Nov 2024 02:49:49 -0600 Subject: [PATCH 097/125] Fix typo in changelog Closes GH-874. Reviewed-by: Remco Haszing Reviewed-by: Titus Wormer --- changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 7fc79cec..8e4e90d3 100644 --- a/changelog.md +++ b/changelog.md @@ -704,7 +704,7 @@ slightly differently. ### Fixes -* (Typings) Add `parserOptions` to type defintions (Ted Piotrowski) +* (Typings) Add `parserOptions` to type definitions (Ted Piotrowski) * Allow renderer to be any React element type (Nathan Bierema) ## 4.1.0 - 2019-06-24 From 27d3949b31beb7aa7a6c0d3d4d34e6fd0965a7d3 Mon Sep 17 00:00:00 2001 From: Remco Haszing Date: Thu, 2 Jan 2025 12:42:11 +0100 Subject: [PATCH 098/125] Separate all typedefs into their own JSDoc blocks (#878) Having them in the same block, can be problematic sometimes. For example when they contain `@template` tags. --- lib/index.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/index.js b/lib/index.js index 6470cb8f..b97d6ede 100644 --- a/lib/index.js +++ b/lib/index.js @@ -18,10 +18,14 @@ * Parent of `element`. * @returns {boolean | null | undefined} * Whether to allow `element` (default: `false`). - * + */ + +/** * @typedef {Partial} Components * Map tag names to components. - * + */ + +/** * @typedef Deprecation * Deprecation. * @property {string} from @@ -30,7 +34,9 @@ * ID in readme. * @property {keyof Options} [to] * New field. - * + */ + +/** * @typedef Options * Configuration. * @property {AllowElement | null | undefined} [allowElement] @@ -62,7 +68,9 @@ * with `unwrapDisallowed` the element itself is replaced by its children. * @property {UrlTransform | null | undefined} [urlTransform] * Change URLs (default: `defaultUrlTransform`) - * + */ + +/** * @callback UrlTransform * Transform all URLs. * @param {string} url From b151a9028f2ca14d8982de47e70a1db7b7c79a2c Mon Sep 17 00:00:00 2001 From: Remco Haszing Date: Thu, 2 Jan 2025 15:59:37 +0100 Subject: [PATCH 099/125] Fix types for React 19 Closes GH-879. Reviewed-by: Christian Murphy Reviewed-by: Titus Wormer --- index.js | 2 +- lib/index.js | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/index.js b/index.js index ff903ce0..174bffe7 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,7 @@ /** - * @typedef {import('hast-util-to-jsx-runtime').ExtraProps} ExtraProps * @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').Options} Options * @typedef {import('./lib/index.js').UrlTransform} UrlTransform */ diff --git a/lib/index.js b/lib/index.js index b97d6ede..d3d201f8 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,7 +1,6 @@ /** * @import {Element, ElementContent, Nodes, Parents, Root} from 'hast' - * @import {Components as JsxRuntimeComponents} from 'hast-util-to-jsx-runtime' - * @import {ReactElement} from 'react' + * @import {ComponentProps, ElementType, ReactElement} from 'react' * @import {Options as RemarkRehypeOptions} from 'remark-rehype' * @import {BuildVisitor} from 'unist-util-visit' * @import {PluggableList} from 'unified' @@ -21,7 +20,16 @@ */ /** - * @typedef {Partial} Components + * @typedef ExtraProps + * Extra fields we pass. + * @property {Element | undefined} [node] + * passed when `passNode` is on. + */ + +/** + * @typedef {{ + * [Key in Extract]?: ElementType & ExtraProps> + * }} Components * Map tag names to components. */ @@ -225,6 +233,9 @@ export function Markdown(options) { return toJsxRuntime(hastTree, { Fragment, + // @ts-expect-error + // React components are allowed to return numbers, + // but not according to the types in hast-util-to-jsx-runtime components, ignoreInvalidStyle: true, jsx, From e68655127bb09402e1d12507e1b2db8fa3c64ff8 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Thu, 2 Jan 2025 16:01:50 +0100 Subject: [PATCH 100/125] Update dev-dependencies --- .gitignore | 1 + lib/index.js | 8 ++++---- package.json | 10 +++++----- test-types.d.ts | 9 +++++++++ test-types.js | 2 ++ test.jsx | 13 ++++++++----- tsconfig.json | 2 +- 7 files changed, 30 insertions(+), 15 deletions(-) create mode 100644 test-types.d.ts create mode 100644 test-types.js diff --git a/.gitignore b/.gitignore index f19ff243..ce84e07a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ node_modules/ .DS_Store react-markdown.min.js yarn.lock +!/test-types.d.ts diff --git a/lib/index.js b/lib/index.js index d3d201f8..529639c5 100644 --- a/lib/index.js +++ b/lib/index.js @@ -318,11 +318,11 @@ export function defaultUrlTransform(value) { if ( // If there is no protocol, it’s relative. - colon < 0 || + 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) || + (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)) ) { diff --git a/package.json b/package.json index c051f5e1..b1073835 100644 --- a/package.json +++ b/package.json @@ -94,14 +94,14 @@ }, "devDependencies": { "@types/node": "^22.0.0", - "@types/react": "^18.0.0", - "@types/react-dom": "^18.0.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", "c8": "^10.0.0", "esbuild": "^0.24.0", "eslint-plugin-react": "^7.0.0", "prettier": "^3.0.0", - "react": "^18.0.0", - "react-dom": "^18.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", "rehype-raw": "^7.0.0", "remark-cli": "^12.0.0", "remark-gfm": "^4.0.0", @@ -109,7 +109,7 @@ "remark-toc": "^9.0.0", "type-coverage": "^2.0.0", "typescript": "^5.0.0", - "xo": "^0.59.0" + "xo": "^0.60.0" }, "scripts": { "build": "tsc --build --clean && tsc --build && type-coverage", diff --git a/test-types.d.ts b/test-types.d.ts new file mode 100644 index 00000000..4dd67e3c --- /dev/null +++ b/test-types.d.ts @@ -0,0 +1,9 @@ +import type {JSX as Jsx} from 'react/jsx-runtime' + +declare global { + namespace JSX { + type ElementClass = Jsx.ElementClass + type Element = Jsx.Element + type IntrinsicElements = Jsx.IntrinsicElements + } +} diff --git a/test-types.js b/test-types.js new file mode 100644 index 00000000..f3ea4d4a --- /dev/null +++ b/test-types.js @@ -0,0 +1,2 @@ +// See `test-types.d.ts`. +export {} diff --git a/test.jsx b/test.jsx index 82239185..f127de10 100644 --- a/test.jsx +++ b/test.jsx @@ -167,21 +167,24 @@ test('react-markdown', async function (t) { await t.test('should support an image', function () { assert.equal( renderToStaticMarkup(), - '

    a

    ' + // Note: React weirdly adds `rel="preload"`. + '

    a

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

    a

    ' + // Note: React weirdly adds `rel="preload"`. + '

    a

    ' ) }) await t.test('should support an image reference / definition', function () { assert.equal( renderToStaticMarkup(), - '

    a

    ' + // Note: React weirdly adds `rel="preload"`. + '

    a

    ' ) }) @@ -410,7 +413,7 @@ test('react-markdown', async function (t) { }} /> ), - '

    a

    ' + '

    a

    ' ) }) @@ -564,7 +567,7 @@ test('react-markdown', async function (t) { console.error = warn - assert.match(String(message), /Warning: React.jsx: type is invalid/) + assert.match(String(message), /type is invalid/) /** * @param {unknown} d diff --git a/tsconfig.json b/tsconfig.json index d13dc27b..5317638d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,5 +13,5 @@ "target": "es2022" }, "exclude": ["coverage/", "node_modules/"], - "include": ["**/*.js", "**/*.jsx", "lib/complex-types.d.ts"] + "include": ["**/*.js", "**/*.jsx", "test-types.d.ts"] } From b664ac4459ed5fe2834665976b8864da03d263e9 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Mon, 6 Jan 2025 11:29:38 +0100 Subject: [PATCH 101/125] Update Actions --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2ce794f1..ade39213 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -9,7 +9,7 @@ jobs: node-version: ${{matrix.node}} - run: npm install - run: npm test - - uses: codecov/codecov-action@v4 + - uses: codecov/codecov-action@v5 strategy: matrix: node: From 2c6ffe8f93871ea8e17d12ec0b6f6e5b0aa49ae2 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Mon, 6 Jan 2025 11:30:19 +0100 Subject: [PATCH 102/125] Refactor `.gitignore` --- .gitignore | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index ce84e07a..94496d35 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,10 @@ -coverage/ -node_modules/ -*.d.ts.map *.d.ts *.log +*.map *.tsbuildinfo .DS_Store +coverage/ +node_modules/ react-markdown.min.js yarn.lock !/test-types.d.ts From 40c097eb6f4b89209bd90cc3338fcaaa957bebaf Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Mon, 6 Jan 2025 11:33:23 +0100 Subject: [PATCH 103/125] 9.0.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b1073835..78d0a92a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-markdown", - "version": "9.0.1", + "version": "9.0.2", "description": "React component to render markdown", "license": "MIT", "keywords": [ From aed001070aae99bc6d1f3bdd8e71974f5c0d5f10 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Mon, 6 Jan 2025 12:15:23 +0100 Subject: [PATCH 104/125] 9.0.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 78d0a92a..9cb4ce3a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-markdown", - "version": "9.0.2", + "version": "9.0.3", "description": "React component to render markdown", "license": "MIT", "keywords": [ From c44e246bbb0cbad1110b22b6fb239db2669de0d0 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Thu, 13 Feb 2025 12:13:49 +0100 Subject: [PATCH 105/125] Update dev-dependencies --- changelog.md | 38 +++---- package.json | 4 +- readme.md | 279 +++++++++++++++++++++++++-------------------------- 3 files changed, 160 insertions(+), 161 deletions(-) diff --git a/changelog.md b/changelog.md index 8e4e90d3..bfbf8d4d 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,5 @@ + + # Changelog All notable changes will be documented in this file. @@ -64,7 +66,7 @@ Write a plugin to pass `index`:
    Show example of plugin -```jsx +```js import {visit} from 'unist-util-visit' function rehypePluginAddingIndex() { @@ -99,7 +101,7 @@ Write a plugin to pass `index`:
    Show example of plugin -```jsx +```js import {stringifyPosition} from 'unist-util-stringify-position' import {visit} from 'unist-util-visit' @@ -333,7 +335,7 @@ for more on components. Before (**broken**): -```jsx +```js Show example of feature -```jsx +```js import rehypeHighlight from 'rehype-highlight' {`~~~js @@ -454,7 +456,7 @@ too. Before (**broken**): -```jsx +```js import MarkdownWithHtml from 'react-markdown/with-html' {`# Hello, world!`} @@ -462,7 +464,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' @@ -481,20 +483,20 @@ Instead of passing a `source` pass `children` instead: Before (**broken**): -```jsx +```js ``` Now (**fixed**): -```jsx +```js {`some markdown`} ``` Or (**also fixed**): -```jsx +```js ``` @@ -513,7 +515,7 @@ names: `allowNode` to `allowElement`, `allowedTypes` to `allowedElements`, and Before (**broken**): -```jsx +```js node.type !== 'heading' || node.depth !== 1} @@ -542,7 +544,7 @@ Before (**broken**): Now (**fixed**): -```jsx +```js element.tagName !== 'h1'} @@ -561,7 +563,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 }} … /> ``` diff --git a/package.json b/package.json index 9cb4ce3a..b9c7cced 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "c8": "^10.0.0", - "esbuild": "^0.24.0", + "esbuild": "^0.25.0", "eslint-plugin-react": "^7.0.0", "prettier": "^3.0.0", "react": "^19.0.0", @@ -105,7 +105,7 @@ "rehype-raw": "^7.0.0", "remark-cli": "^12.0.0", "remark-gfm": "^4.0.0", - "remark-preset-wooorm": "^10.0.0", + "remark-preset-wooorm": "^11.0.0", "remark-toc": "^9.0.0", "type-coverage": "^2.0.0", "typescript": "^5.0.0", diff --git a/readme.md b/readme.md index 8c5038b2..05080a9f 100644 --- a/readme.md +++ b/readme.md @@ -1,18 +1,15 @@ # 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. @@ -67,7 +64,7 @@ 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][commonmark-help] -* to try out `react-markdown`, see [our demo][demo] +* to try out `react-markdown`, see [our demo][github-io-react-markdown] ## When should I use this? @@ -77,21 +74,23 @@ 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][], specifically [remark][] for -markdown and [rehype][] for HTML, which are popular tools to transform content -with plugins. +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 16+), install with [npm][]: +In Node.js (version 16+), install with [npm][npm-install]: ```sh npm install react-markdown @@ -115,7 +114,7 @@ In browsers with [`esm.sh`][esmsh]: A basic hello world: -```jsx +```js import React from 'react' import {createRoot} from 'react-dom/client' import Markdown from 'react-markdown' @@ -128,7 +127,7 @@ createRoot(document.body).render({markdown})
    Show equivalent JSX -```jsx +```js

    Hi, Pluto!

    @@ -136,11 +135,12 @@ createRoot(document.body).render({markdown})
    -Here is an example that shows how to use a plugin ([`remark-gfm`][remark-gfm], -which adds support for footnotes, 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 {createRoot} from 'react-dom/client' import Markdown from 'react-markdown' @@ -156,7 +156,7 @@ createRoot(document.body).render(
    Show equivalent JSX -```jsx +```js

    Just a link: www.nasa.gov.

    @@ -202,11 +202,11 @@ Filter elements (TypeScript type). ###### Parameters -* `node` ([`Element` from `hast`][hast-element]) +* `node` ([`Element` from `hast`][github-hast-element]) — element to check * `index` (`number | undefined`) — index of `element` in `parent` -* `parent` ([`Node` from `hast`][hast-node]) +* `parent` ([`Node` from `hast`][github-hast-nodes]) — parent of `element` ###### Returns @@ -239,7 +239,7 @@ Extra fields we pass to components (TypeScript type). ###### Fields -* `node` ([`Element` from `hast`][hast-element], optional) +* `node` ([`Element` from `hast`][github-hast-element], optional) — original node ### `Options` @@ -264,11 +264,12 @@ Configuration (TypeScript type). — tag names to disallow; cannot combine w/ `allowedElements` * `rehypePlugins` (`Array`, optional) - — list of [rehype plugins][rehype-plugins] to use + — list of [rehype plugins][github-rehype-plugins] to use * `remarkPlugins` (`Array`, optional) - — list of [remark plugins][remark-plugins] to use -* `remarkRehypeOptions` ([`Options` from - `remark-rehype`][remark-rehype-options], 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 @@ -290,7 +291,7 @@ Transform URLs (TypeScript type). — URL * `key` (`string`, example: `'href'`) — property name -* `node` ([`Element` from `hast`][hast-element]) +* `node` ([`Element` from `hast`][github-hast-element]) — element to check ###### Returns @@ -302,10 +303,10 @@ Transformed URL (`string`, optional). ### Use a plugin This example shows how to use a remark plugin. -In this case, [`remark-gfm`][remark-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 {createRoot} from 'react-dom/client' import Markdown from 'react-markdown' @@ -333,7 +334,7 @@ createRoot(document.body).render(
    Show equivalent JSX -```jsx +```js <>

    A paragraph with emphasis and strong importance. @@ -372,10 +373,10 @@ createRoot(document.body).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`][remark-gfm] has an option to allow only double tildes for +[`remark-gfm`][github-remark-gfm] has an option to allow only double tildes for strikethrough: -```jsx +```js import React from 'react' import {createRoot} from 'react-dom/client' import Markdown from 'react-markdown' @@ -393,7 +394,7 @@ createRoot(document.body).render(

    Show equivalent JSX -```jsx +```js

    This ~is not~ strikethrough, but this is!

    @@ -406,12 +407,12 @@ createRoot(document.body).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 {createRoot} from 'react-dom/client' import Markdown from 'react-markdown' @@ -455,7 +456,7 @@ createRoot(document.body).render(
    Show equivalent JSX -```jsx +```js <>

    Here is some JavaScript code:

    @@ -468,11 +469,12 @@ createRoot(document.body).render(
     
     ### Use remark and rehype plugins (math)
     
    -This example shows how a syntax extension (through [`remark-math`][remark-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`][rehype-katex]) to render that math.
    +([`rehype-katex`][github-rehype-katex]) to render that math.
     
    -```jsx
    +```js
     import React from 'react'
     import {createRoot} from 'react-dom/client'
     import Markdown from 'react-markdown'
    @@ -492,7 +494,7 @@ createRoot(document.body).render(
     
    Show equivalent JSX -```jsx +```js

    The lift coefficient ( @@ -511,16 +513,20 @@ createRoot(document.body).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] +* [`awesome-remark`][github-awesome-remark] and + [`awesome-rehype`][github-awesome-rehype] — selection of the most awesome projects -* [List of remark plugins][remark-plugins] and - [list of rehype plugins][rehype-plugins] +* [List of remark plugins][github-remark-plugins] and + [list of rehype plugins][github-rehype-plugins] — list of all plugins -* [`remark-plugin`][remark-plugin] and [`rehype-plugin`][rehype-plugin] topics +* [`remark-plugin`][github-topic-remark-plugin] and + [`rehype-plugin`][github-topic-rehype-plugin] topics — any tagged repo on GitHub ## Syntax @@ -529,7 +535,7 @@ 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. @@ -573,8 +579,9 @@ 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. @@ -593,9 +600,9 @@ 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`][rehype-raw]: +[`rehype-raw`][github-rehype-raw]: -```jsx +```js import React from 'react' import {createRoot} from 'react-dom/client' import Markdown from 'react-markdown' @@ -615,7 +622,7 @@ createRoot(document.body).render(
    Show equivalent JSX -```jsx +```js

    Some emphasis and strong! @@ -634,7 +641,7 @@ markdown! You can also change the things that come from markdown: -```jsx +```js # Hi @@ -700,14 +707,14 @@ The is because in JSX the whitespace (including line endings) is collapsed to a single space. So the above example is equivalent to: -```jsx +```js # Hi This is **not** a paragraph. ``` Instead, to pass markdown to `Markdown`, you can use an expression: with a template literal: -```jsx +```js {` # Hi @@ -719,7 +726,7 @@ 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: -```jsx +```js {` # This is **not** a heading, it’s an indented code block `} @@ -734,133 +741,135 @@ 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`][rehype-sanitize]. +use [`rehype-sanitize`][github-rehype-sanitize]. It lets you define your own schema of what is and isn’t allowed. ## Related -* [`MDX`][mdx] +* [`MDX`][github-mdx] — JSX *in* markdown -* [`remark-gfm`][remark-gfm] +* [`remark-gfm`][github-remark-gfm] — add support for GitHub flavored markdown support -* [`react-remark`][react-remark] +* [`react-remark`][github-react-remark] — hook based alternative -* [`rehype-react`][rehype-react] +* [`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-markdown]: #markdown -[downloads]: https://www.npmjs.com/package/react-markdown +[api-options]: #options -[size-badge]: https://img.shields.io/bundlejs/size/react-markdown +[api-url-transform]: #urltransform -[size]: https://bundlejs.com/?q=react-markdown +[author]: https://espen.codes/ -[sponsors-badge]: https://opencollective.com/unified/sponsors/badge.svg +[badge-build-image]: https://github.com/remarkjs/react-markdown/workflows/main/badge.svg -[backers-badge]: https://opencollective.com/unified/backers/badge.svg +[badge-build-url]: https://github.com/remarkjs/react-markdown/actions -[collective]: https://opencollective.com/unified +[badge-coverage-image]: https://img.shields.io/codecov/c/github/remarkjs/react-markdown.svg -[chat-badge]: https://img.shields.io/badge/chat-discussions-success.svg +[badge-coverage-url]: https://codecov.io/github/remarkjs/react-markdown -[chat]: https://github.com/remarkjs/remark/discussions +[badge-downloads-image]: https://img.shields.io/npm/dm/react-markdown.svg -[npm]: https://docs.npmjs.com/cli/install +[badge-downloads-url]: https://www.npmjs.com/package/react-markdown -[esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c +[badge-size-image]: https://img.shields.io/bundlejs/size/react-markdown -[esmsh]: https://esm.sh +[badge-size-url]: https://bundlejs.com/?q=react-markdown -[health]: https://github.com/remarkjs/.github +[commonmark-help]: https://commonmark.org/help/ -[coc]: https://github.com/remarkjs/.github/blob/main/code-of-conduct.md +[commonmark-html]: https://spec.commonmark.org/0.31.2/#html-blocks -[contributing]: https://github.com/remarkjs/.github/blob/main/contributing.md +[esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c -[support]: https://github.com/remarkjs/.github/blob/main/support.md +[esmsh]: https://esm.sh -[license]: license +[file-license]: license -[author]: https://espen.codes/ +[github-awesome-rehype]: https://github.com/rehypejs/awesome-rehype -[awesome-remark]: https://github.com/remarkjs/awesome-remark +[github-awesome-remark]: https://github.com/remarkjs/awesome-remark -[awesome-rehype]: https://github.com/rehypejs/awesome-rehype +[github-conorhastings]: https://github.com/conorhastings -[commonmark-help]: https://commonmark.org/help/ +[github-hast-element]: https://github.com/syntax-tree/hast#element -[commonmark-html]: https://spec.commonmark.org/0.30/#html-blocks +[github-hast-nodes]: https://github.com/syntax-tree/hast#nodes -[hast-element]: https://github.com/syntax-tree/hast#element +[github-io-react-markdown]: https://remarkjs.github.io/react-markdown/ -[hast-node]: https://github.com/syntax-tree/hast#nodes +[github-mdx]: https://github.com/mdx-js/mdx/ -[mdx]: https://github.com/mdx-js/mdx/ +[github-micromark]: https://github.com/micromark/micromark -[micromark]: https://github.com/micromark/micromark +[github-react-remark]: https://github.com/remarkjs/react-remark -[react]: http://reactjs.org +[github-react-syntax-highlighter]: https://github.com/react-syntax-highlighter/react-syntax-highlighter -[react-remark]: https://github.com/remarkjs/react-remark +[github-rehype]: https://github.com/rehypejs/rehype -[react-syntax-highlighter]: https://github.com/react-syntax-highlighter/react-syntax-highlighter +[github-rehype-katex]: https://github.com/remarkjs/remark-math/tree/main/packages/rehype-katex -[rehype]: https://github.com/rehypejs/rehype +[github-rehype-plugins]: https://github.com/rehypejs/rehype/blob/main/doc/plugins.md#list-of-plugins -[rehype-katex]: https://github.com/remarkjs/remark-math/tree/main/packages/rehype-katex +[github-rehype-raw]: https://github.com/rehypejs/rehype-raw -[rehype-plugin]: https://github.com/topics/rehype-plugin +[github-rehype-react]: https://github.com/rehypejs/rehype-react -[rehype-plugins]: https://github.com/rehypejs/rehype/blob/main/doc/plugins.md#list-of-plugins +[github-rehype-sanitize]: https://github.com/rehypejs/rehype-sanitize -[rehype-react]: https://github.com/rehypejs/rehype-react +[github-remark]: https://github.com/remarkjs/remark -[rehype-raw]: https://github.com/rehypejs/rehype-raw +[github-remark-gfm]: https://github.com/remarkjs/remark-gfm -[rehype-sanitize]: https://github.com/rehypejs/rehype-sanitize +[github-remark-math]: https://github.com/remarkjs/remark-math -[remark]: https://github.com/remarkjs/remark +[github-remark-plugins]: https://github.com/remarkjs/remark/blob/main/doc/plugins.md#list-of-plugins -[remark-gfm]: https://github.com/remarkjs/remark-gfm +[github-remark-rehype-options]: https://github.com/remarkjs/remark-rehype#options -[remark-math]: https://github.com/remarkjs/remark-math +[github-topic-rehype-plugin]: https://github.com/topics/rehype-plugin -[remark-plugin]: https://github.com/topics/remark-plugin +[github-topic-remark-plugin]: https://github.com/topics/remark-plugin -[remark-plugins]: https://github.com/remarkjs/remark/blob/main/doc/plugins.md#list-of-plugins +[github-unified]: https://github.com/unifiedjs/unified -[remark-rehype-options]: https://github.com/remarkjs/remark-rehype#options +[health]: https://github.com/remarkjs/.github -[unified]: https://github.com/unifiedjs/unified +[health-coc]: https://github.com/remarkjs/.github/blob/main/code-of-conduct.md -[typescript]: https://www.typescriptlang.org +[health-contributing]: https://github.com/remarkjs/.github/blob/main/contributing.md -[conor]: https://github.com/conorhastings +[health-support]: https://github.com/remarkjs/.github/blob/main/support.md -[demo]: https://remarkjs.github.io/react-markdown/ +[npm-install]: https://docs.npmjs.com/cli/install + +[react]: http://reactjs.org [section-components]: #appendix-b-components @@ -870,16 +879,4 @@ abide by its terms. [section-syntax]: #syntax -[api-allow-element]: #allowelement - -[api-components]: #components - -[api-default-url-transform]: #defaulturltransformurl - -[api-extra-props]: #extraprops - -[api-markdown]: #markdown - -[api-options]: #options - -[api-url-transform]: #urltransform +[typescript]: https://www.typescriptlang.org From bcdc5b3b4f45be2b662ae11c4b42e74727e3ba2f Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Thu, 13 Feb 2025 12:19:17 +0100 Subject: [PATCH 106/125] Refactor `package.json` --- package.json | 123 +++++++++++++++++++++++++-------------------------- 1 file changed, 60 insertions(+), 63 deletions(-) diff --git a/package.json b/package.json index b9c7cced..ccf0e058 100644 --- a/package.json +++ b/package.json @@ -1,80 +1,51 @@ { - "name": "react-markdown", - "version": "9.0.3", - "description": "React component to render markdown", - "license": "MIT", - "keywords": [ - "ast", - "commonmark", - "component", - "gfm", - "markdown", - "react", - "react-component", - "remark", - "unified" - ], - "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 " - ], - "sideEffects": false, - "type": "module", - "exports": "./index.js", - "files": [ - "lib/", - "index.d.ts.map", - "index.d.ts", - "index.js" + "mudrz ", + "vanchagreen " ], "dependencies": { "@types/hast": "^3.0.0", @@ -88,10 +59,7 @@ "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, - "peerDependencies": { - "@types/react": ">=18", - "react": ">=18" - }, + "description": "React component to render markdown", "devDependencies": { "@types/node": "^22.0.0", "@types/react": "^19.0.0", @@ -111,13 +79,33 @@ "typescript": "^5.0.0", "xo": "^0.60.0" }, - "scripts": { - "build": "tsc --build --clean && tsc --build && type-coverage", - "format": "remark . --frail --output --quiet && prettier . --log-level warn --write && xo --fix", - "prepack": "npm run build && npm run format", - "test": "npm run build && npm run format && npm run test-coverage", - "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" + "exports": "./index.js", + "files": [ + "index.d.ts.map", + "index.d.ts", + "index.js", + "lib/" + ], + "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": ">=18", + "react": ">=18" }, "prettier": { "bracketSpacing": false, @@ -136,12 +124,21 @@ ] ] }, + "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, - "ignoreCatch": true, "strict": true }, + "type": "module", + "version": "9.0.3", "xo": { "envs": [ "shared-node-browser" From 78d08de906536b6913695883f215807992fc034d Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Thu, 13 Feb 2025 12:22:16 +0100 Subject: [PATCH 107/125] Refactor to remove warning in tests --- test.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test.jsx b/test.jsx index f127de10..ccc0b7ba 100644 --- a/test.jsx +++ b/test.jsx @@ -1,7 +1,7 @@ /* @jsxRuntime automatic @jsxImportSource react */ /** * @import {Root} from 'hast' - * @import {ComponentProps, ReactElement} from 'react' + * @import {ComponentProps} from 'react' * @import {ExtraProps} from 'react-markdown' */ @@ -409,7 +409,7 @@ test('react-markdown', async function (t) { assert.equal(url, 'https://b.com/') assert.equal(key, 'src') assert.equal(node.tagName, 'img') - return '' + return null }} /> ), From 6ce120e70630a062b42e225ce917cf71339fa024 Mon Sep 17 00:00:00 2001 From: Titus Date: Thu, 20 Feb 2025 12:52:46 +0100 Subject: [PATCH 108/125] Add support for async plugins This commit adds 2 new components that support turning markdown into react nodes, asynchronously. There are different ways to support async things in React. Component with hooks only run on the client. Components yielding promises are not supported on the client. To support different scenarios and the different ways the future could develop, these choices are made explicit to users. Users can choose whether `MarkdownAsync` or `MarkdownHooks` fits their use case. Closes GH-680. Closes GH-682. Closes GH-890. Closes GH-891. Reviewed-by: Christian Murphy Reviewed-by: Remco Haszing --- index.js | 7 ++- lib/index.js | 154 +++++++++++++++++++++++++++++++++++++++++++-------- package.json | 3 + readme.md | 52 ++++++++++++++++- test.jsx | 96 +++++++++++++++++++++++++++++++- 5 files changed, 283 insertions(+), 29 deletions(-) diff --git a/index.js b/index.js index 174bffe7..629aec01 100644 --- a/index.js +++ b/index.js @@ -6,4 +6,9 @@ * @typedef {import('./lib/index.js').UrlTransform} UrlTransform */ -export {Markdown as default, defaultUrlTransform} from './lib/index.js' +export { + MarkdownAsync, + MarkdownHooks, + Markdown as default, + defaultUrlTransform +} from './lib/index.js' diff --git a/lib/index.js b/lib/index.js index 529639c5..c88a5a07 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,9 +1,10 @@ /** * @import {Element, ElementContent, Nodes, Parents, Root} from 'hast' + * @import {Root as MdastRoot} from 'mdast' * @import {ComponentProps, ElementType, ReactElement} from 'react' * @import {Options as RemarkRehypeOptions} from 'remark-rehype' * @import {BuildVisitor} from 'unist-util-visit' - * @import {PluggableList} from 'unified' + * @import {PluggableList, Processor} from 'unified' */ /** @@ -95,6 +96,7 @@ 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 {createElement, useEffect, useState} from 'react' import remarkParse from 'remark-parse' import remarkRehype from 'remark-rehype' import {unified} from 'unified' @@ -149,26 +151,99 @@ const deprecations = [ /** * 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 allowedElements = options.allowedElements - const allowElement = options.allowElement - const children = options.children || '' - const className = options.className - const components = options.components - const disallowedElements = options.disallowedElements + 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 {ReactElement} + * React element. + */ +export function MarkdownHooks(options) { + const processor = createProcessor(options) + const [error, setError] = useState( + /** @type {Error | undefined} */ (undefined) + ) + const [tree, setTree] = useState(/** @type {Root | undefined} */ (undefined)) + + useEffect( + /* c8 ignore next 7 -- hooks are client-only. */ + function () { + const file = createFile(options) + processor.run(processor.parse(file), file, function (error, tree) { + setError(error) + setTree(tree) + }) + }, + [ + options.children, + options.rehypePlugins, + options.remarkPlugins, + options.remarkRehypeOptions + ] + ) + + /* c8 ignore next -- hooks are client-only. */ + if (error) throw error + + /* c8 ignore next -- hooks are client-only. */ + return tree ? post(tree, options) : createElement(Fragment) +} + +/** + * 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 skipHtml = options.skipHtml - const unwrapDisallowed = options.unwrapDisallowed - const urlTransform = options.urlTransform || defaultUrlTransform const processor = unified() .use(remarkParse) @@ -176,6 +251,19 @@ export function Markdown(options) { .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') { @@ -188,11 +276,27 @@ export function Markdown(options) { ) } - if (allowedElements && disallowedElements) { - unreachable( - 'Unexpected combined `allowedElements` and `disallowedElements`, expected one or the other' - ) - } + 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)) { @@ -212,26 +316,28 @@ export function Markdown(options) { } } - const mdastTree = processor.parse(file) - /** @type {Nodes} */ - let hastTree = processor.runSync(mdastTree, file) + if (allowedElements && disallowedElements) { + unreachable( + 'Unexpected combined `allowedElements` and `disallowedElements`, expected one or the other' + ) + } // Wrap in `div` if there’s a class name. - if (className) { - hastTree = { + if (options.className) { + tree = { type: 'element', tagName: 'div', - properties: {className}, + properties: {className: options.className}, // Assume no doctypes. children: /** @type {Array} */ ( - hastTree.type === 'root' ? hastTree.children : [hastTree] + tree.type === 'root' ? tree.children : [tree] ) } } - visit(hastTree, transform) + visit(tree, transform) - return toJsxRuntime(hastTree, { + return toJsxRuntime(tree, { Fragment, // @ts-expect-error // React components are allowed to return numbers, diff --git a/package.json b/package.json index ccf0e058..4ab537f5 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ ], "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", @@ -65,12 +66,14 @@ "@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", "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", diff --git a/readme.md b/readme.md index 05080a9f..d1ff26ca 100644 --- a/readme.md +++ b/readme.md @@ -32,6 +32,8 @@ React component to render markdown. * [Use](#use) * [API](#api) * [`Markdown`](#markdown) + * [`MarkdownAsync`](#markdownasync) + * [`MarkdownHooks`](#markdownhooks) * [`defaultUrlTransform(url)`](#defaulturltransformurl) * [`AllowElement`](#allowelement) * [`Components`](#components) @@ -166,7 +168,10 @@ createRoot(document.body).render( ## API -This package exports the following identifier: +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]. @@ -174,6 +179,47 @@ The default export is [`Markdown`][api-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 (`JSX.Element`). + +### `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]) @@ -779,6 +825,10 @@ abide by its terms. [api-markdown]: #markdown +[api-markdown-async]: #markdownasync + +[api-markdown-hooks]: #markdownhooks + [api-options]: #options [api-url-transform]: #urltransform diff --git a/test.jsx b/test.jsx index ccc0b7ba..a8bc3288 100644 --- a/test.jsx +++ b/test.jsx @@ -7,21 +7,29 @@ import assert from 'node:assert/strict' import test from 'node:test' -import {renderToStaticMarkup} from 'react-dom/server' -import Markdown from 'react-markdown' +import concatStream from 'concat-stream' +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' -test('react-markdown', async function (t) { +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

    ') }) @@ -1078,3 +1086,85 @@ test('react-markdown', async function (t) { } }) }) + +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` (1)', async function () { + assert.equal(renderToStaticMarkup(), '') + }) + + await t.test('should support `MarkdownHooks` (2)', async function () { + return new Promise(function (resolve, reject) { + renderToPipeableStream() + .pipe( + concatStream({encoding: 'u8'}, function (data) { + assert.equal(decoder.decode(data), '') + resolve() + }) + ) + .on('error', reject) + }) + }) + + await t.test( + 'should support async plugins w/ `MarkdownHooks` (`rehype-starry-night`)', + async function () { + return new Promise(function (resolve) { + renderToPipeableStream( + + ).pipe( + concatStream({encoding: 'u8'}, function (data) { + assert.equal(decoder.decode(data), '') + resolve() + }) + ) + }) + } + ) +}) From 747e505c9a465ee77addba626f288dfbda8bcad4 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Thu, 20 Feb 2025 12:56:53 +0100 Subject: [PATCH 109/125] 9.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4ab537f5..8b5dce3c 100644 --- a/package.json +++ b/package.json @@ -141,7 +141,7 @@ "strict": true }, "type": "module", - "version": "9.0.3", + "version": "9.1.0", "xo": { "envs": [ "shared-node-browser" From aaaa40b4f823e4210ee5f3e3357624ab487d467a Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Thu, 20 Feb 2025 13:42:45 +0100 Subject: [PATCH 110/125] Remove support for `className` prop Closes GH-781. Closes GH-799. Reviewed-by: Christian Murphy Co-authored-by: Remco Haszing --- lib/index.js | 16 ++-------------- readme.md | 2 -- test.jsx | 26 -------------------------- 3 files changed, 2 insertions(+), 42 deletions(-) diff --git a/lib/index.js b/lib/index.js index c88a5a07..6d777cf9 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,5 +1,5 @@ /** - * @import {Element, ElementContent, Nodes, Parents, Root} from 'hast' + * @import {Element, Nodes, Parents, Root} from 'hast' * @import {Root as MdastRoot} from 'mdast' * @import {ComponentProps, ElementType, ReactElement} from 'react' * @import {Options as RemarkRehypeOptions} from 'remark-rehype' @@ -127,6 +127,7 @@ const deprecations = [ id: 'replace-allownode-allowedtypes-and-disallowedtypes', to: 'allowedElements' }, + {from: 'className', id: 'deprecate-classname'}, { from: 'disallowedTypes', id: 'replace-allownode-allowedtypes-and-disallowedtypes', @@ -322,19 +323,6 @@ function post(tree, options) { ) } - // Wrap in `div` if there’s a class name. - if (options.className) { - tree = { - type: 'element', - tagName: 'div', - properties: {className: options.className}, - // Assume no doctypes. - children: /** @type {Array} */ ( - tree.type === 'root' ? tree.children : [tree] - ) - } - } - visit(tree, transform) return toJsxRuntime(tree, { diff --git a/readme.md b/readme.md index d1ff26ca..836153c3 100644 --- a/readme.md +++ b/readme.md @@ -302,8 +302,6 @@ Configuration (TypeScript type). cannot combine w/ `disallowedElements` * `children` (`string`, optional) — markdown -* `className` (`string`, optional) - — wrap in a `div` with this class name * `components` ([`Components`][api-components], optional) — map tag names to components * `disallowedElements` (`Array`, default: `[]`) diff --git a/test.jsx b/test.jsx index a8bc3288..b59702f5 100644 --- a/test.jsx +++ b/test.jsx @@ -70,32 +70,6 @@ test('Markdown', async function (t) { }, /Unexpected `allowDangerousHtml` prop, remove it/) }) - await t.test('should support `className`', function () { - assert.equal( - renderToStaticMarkup(), - '

    a

    ' - ) - }) - - await t.test('should support `className` (if w/o 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!'} - } - } - }) - await t.test('should support a block quote', function () { assert.equal( renderToStaticMarkup(), From 5768374e29eca4aa1976e8b3663608fcf2dc6055 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Thu, 20 Feb 2025 13:53:02 +0100 Subject: [PATCH 111/125] Update changelog --- changelog.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ lib/index.js | 4 +--- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/changelog.md b/changelog.md index bfbf8d4d..54cd5698 100644 --- a/changelog.md +++ b/changelog.md @@ -4,6 +4,57 @@ 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) diff --git a/lib/index.js b/lib/index.js index 6d777cf9..0c0ea4cb 100644 --- a/lib/index.js +++ b/lib/index.js @@ -56,8 +56,6 @@ * cannot combine w/ `disallowedElements`. * @property {string | null | undefined} [children] * Markdown. - * @property {string | null | undefined} [className] - * Wrap in a `div` with this class name. * @property {Components | null | undefined} [components] * Map tag names to components. * @property {ReadonlyArray | null | undefined} [disallowedElements] @@ -127,7 +125,7 @@ const deprecations = [ id: 'replace-allownode-allowedtypes-and-disallowedtypes', to: 'allowedElements' }, - {from: 'className', id: 'deprecate-classname'}, + {from: 'className', id: 'remove-classname'}, { from: 'disallowedTypes', id: 'replace-allownode-allowedtypes-and-disallowedtypes', From 33c31e7e238ede6cac0cfaaea31eff7425a3e904 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Thu, 20 Feb 2025 14:01:10 +0100 Subject: [PATCH 112/125] 10.0.0 --- package.json | 2 +- readme.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 8b5dce3c..4229029a 100644 --- a/package.json +++ b/package.json @@ -141,7 +141,7 @@ "strict": true }, "type": "module", - "version": "9.1.0", + "version": "10.0.0", "xo": { "envs": [ "shared-node-browser" diff --git a/readme.md b/readme.md index 836153c3..62d4dff7 100644 --- a/readme.md +++ b/readme.md @@ -101,14 +101,14 @@ npm install react-markdown In Deno with [`esm.sh`][esmsh]: ```js -import Markdown from 'https://esm.sh/react-markdown@9' +import Markdown from 'https://esm.sh/react-markdown@10' ``` In browsers with [`esm.sh`][esmsh]: ```html ``` @@ -600,7 +600,7 @@ versions of Node.js. 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@^9`, +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). From 21b47b9e7f916602987e1b85e7df7a688b9957ee Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Tue, 25 Feb 2025 10:31:09 +0100 Subject: [PATCH 113/125] Remove local use of `JSX` --- .gitignore | 1 - lib/index.js | 3 --- readme.md | 23 +++++++++-------------- test-types.d.ts | 9 --------- test-types.js | 2 -- tsconfig.json | 2 +- 6 files changed, 10 insertions(+), 30 deletions(-) delete mode 100644 test-types.d.ts delete mode 100644 test-types.js diff --git a/.gitignore b/.gitignore index 94496d35..fceff11a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,3 @@ coverage/ node_modules/ react-markdown.min.js yarn.lock -!/test-types.d.ts diff --git a/lib/index.js b/lib/index.js index 0c0ea4cb..76960477 100644 --- a/lib/index.js +++ b/lib/index.js @@ -325,9 +325,6 @@ function post(tree, options) { return toJsxRuntime(tree, { Fragment, - // @ts-expect-error - // React components are allowed to return numbers, - // but not according to the types in hast-util-to-jsx-runtime components, ignoreInvalidStyle: true, jsx, diff --git a/readme.md b/readme.md index 62d4dff7..69bf5bde 100644 --- a/readme.md +++ b/readme.md @@ -191,7 +191,7 @@ see [`MarkdownAsync`][api-markdown-async] or ###### Returns -React element (`JSX.Element`). +React element (`ReactElement`). ### `MarkdownAsync` @@ -209,7 +209,7 @@ see [`MarkdownHooks`][api-markdown-hooks]. ###### Returns -Promise to a React element (`Promise`). +Promise to a React element (`Promise`). ### `MarkdownHooks` @@ -227,7 +227,7 @@ see [`MarkdownAsync`][api-markdown-async]. ###### Returns -React element (`JSX.Element`). +React element (`ReactElement`). ### `defaultUrlTransform(url)` @@ -266,17 +266,12 @@ Map tag names to components (TypeScript type). ###### Type ```ts -import type {Element} from 'hast' - -type Components = Partial<{ - [TagName in keyof JSX.IntrinsicElements]: - // Class component: - | (new (props: JSX.IntrinsicElements[TagName] & ExtraProps) => JSX.ElementClass) - // Function component: - | ((props: JSX.IntrinsicElements[TagName] & ExtraProps) => JSX.Element | string | null | undefined) - // Tag name: - | keyof JSX.IntrinsicElements -}> +import type {ExtraProps} from 'react-markdown' +import type {ComponentProps, ElementType} from 'react' + +type Components = { + [Key in Extract]?: ElementType & ExtraProps> +} ``` ### `ExtraProps` diff --git a/test-types.d.ts b/test-types.d.ts deleted file mode 100644 index 4dd67e3c..00000000 --- a/test-types.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type {JSX as Jsx} from 'react/jsx-runtime' - -declare global { - namespace JSX { - type ElementClass = Jsx.ElementClass - type Element = Jsx.Element - type IntrinsicElements = Jsx.IntrinsicElements - } -} diff --git a/test-types.js b/test-types.js deleted file mode 100644 index f3ea4d4a..00000000 --- a/test-types.js +++ /dev/null @@ -1,2 +0,0 @@ -// See `test-types.d.ts`. -export {} diff --git a/tsconfig.json b/tsconfig.json index 5317638d..95ae95ae 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,5 +13,5 @@ "target": "es2022" }, "exclude": ["coverage/", "node_modules/"], - "include": ["**/*.js", "**/*.jsx", "test-types.d.ts"] + "include": ["**/*.js", "**/*.jsx"] } From 7c17ede8e47f57785d0b82a7b42fffd8287bf3a3 Mon Sep 17 00:00:00 2001 From: Remco Haszing Date: Mon, 3 Mar 2025 11:48:42 +0100 Subject: [PATCH 114/125] Fix performance around components Related-to: GH-895. Related-to: GH-883. Closes GH-893. Reviewed-by: Christian Murphy Reviewed-by: Titus Wormer --- lib/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/index.js b/lib/index.js index 76960477..1e2a4a12 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,7 +1,7 @@ /** * @import {Element, Nodes, Parents, Root} from 'hast' * @import {Root as MdastRoot} from 'mdast' - * @import {ComponentProps, ElementType, ReactElement} from 'react' + * @import {ComponentType, JSX, ReactElement} from 'react' * @import {Options as RemarkRehypeOptions} from 'remark-rehype' * @import {BuildVisitor} from 'unist-util-visit' * @import {PluggableList, Processor} from 'unified' @@ -29,7 +29,7 @@ /** * @typedef {{ - * [Key in Extract]?: ElementType & ExtraProps> + * [Key in keyof JSX.IntrinsicElements]?: ComponentType | keyof JSX.IntrinsicElements * }} Components * Map tag names to components. */ From 2792c32cdd2e7fd38e5d79fe5761da521d3ca0ae Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Mon, 3 Mar 2025 11:50:54 +0100 Subject: [PATCH 115/125] 10.0.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4229029a..1b7cf8b5 100644 --- a/package.json +++ b/package.json @@ -141,7 +141,7 @@ "strict": true }, "type": "module", - "version": "10.0.0", + "version": "10.0.1", "xo": { "envs": [ "shared-node-browser" From ad7f37f0b407ed90663e0ff85dda246f7987b5a9 Mon Sep 17 00:00:00 2001 From: Remco Haszing Date: Mon, 3 Mar 2025 14:45:03 +0100 Subject: [PATCH 116/125] Add lifecycle tests for `MarkdownHooks` Closes GH-894. Reviewed-by: Christian Murphy Reviewed-by: Titus Wormer --- lib/index.js | 3 -- package.json | 2 + test.jsx | 136 +++++++++++++++++++++++++++++++++++++++++---------- 3 files changed, 112 insertions(+), 29 deletions(-) diff --git a/lib/index.js b/lib/index.js index 1e2a4a12..c6067477 100644 --- a/lib/index.js +++ b/lib/index.js @@ -206,7 +206,6 @@ export function MarkdownHooks(options) { const [tree, setTree] = useState(/** @type {Root | undefined} */ (undefined)) useEffect( - /* c8 ignore next 7 -- hooks are client-only. */ function () { const file = createFile(options) processor.run(processor.parse(file), file, function (error, tree) { @@ -222,10 +221,8 @@ export function MarkdownHooks(options) { ] ) - /* c8 ignore next -- hooks are client-only. */ if (error) throw error - /* c8 ignore next -- hooks are client-only. */ return tree ? post(tree, options) : createElement(Fragment) } diff --git a/package.json b/package.json index 1b7cf8b5..d0689a33 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ }, "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", @@ -69,6 +70,7 @@ "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", diff --git a/test.jsx b/test.jsx index b59702f5..8bbf4f7b 100644 --- a/test.jsx +++ b/test.jsx @@ -1,13 +1,17 @@ /* @jsxRuntime automatic @jsxImportSource react */ /** * @import {Root} from 'hast' - * @import {ComponentProps} from 'react' + * @import {ComponentProps, ReactNode} from 'react' * @import {ExtraProps} from 'react-markdown' + * @import {Plugin} from 'unified' */ 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' @@ -1106,39 +1110,119 @@ test('MarkdownAsync', async function (t) { // Note: hooks are not supported on the “server”. test('MarkdownHooks', async function (t) { - await t.test('should support `MarkdownHooks` (1)', async function () { - assert.equal(renderToStaticMarkup(), '') - }) + await t.test('should support `MarkdownHooks`', async function () { + const plugin = deferPlugin() - await t.test('should support `MarkdownHooks` (2)', async function () { - return new Promise(function (resolve, reject) { - renderToPipeableStream() - .pipe( - concatStream({encoding: 'u8'}, function (data) { - assert.equal(decoder.decode(data), '') - resolve() - }) - ) - .on('error', reject) + const {container} = render( + + ) + + assert.equal(container.innerHTML, '') + plugin.resolve() + await waitFor(() => { + assert.notEqual(container.innerHTML, '') }) + assert.equal(container.innerHTML, '

    a

    ') }) await t.test( 'should support async plugins w/ `MarkdownHooks` (`rehype-starry-night`)', async function () { - return new Promise(function (resolve) { - renderToPipeableStream( - - ).pipe( - concatStream({encoding: 'u8'}, function (data) { - assert.equal(decoder.decode(data), '') - resolve() - }) - ) + const plugin = deferPlugin() + + const {container} = render( + + ) + + assert.equal(container.innerHTML, '') + plugin.resolve() + await waitFor(() => { + assert.notEqual(container.innerHTML, '') }) + assert.equal( + container.innerHTML, + '
    console.log(3.14)\n
    ' + ) } ) + + await t.test('should support `MarkdownHooks` that error', async function () { + const plugin = deferPlugin() + + const {container} = render( + + + + ) + + assert.equal(container.innerHTML, '') + plugin.reject(new Error('rejected')) + await waitFor(() => { + assert.notEqual(container.innerHTML, '') + }) + assert.equal(container.innerHTML, 'Error: rejected') + }) }) + +/** + * @typedef DeferredPlugin + * @property {Plugin<[]>} plugin + * A unified plugin + * @property {() => void} resolve + * Resolve the plugin. + * @property {(error: Error) => void} reject + * Reject the plugin. + */ + +/** + * Create an async unified plugin which waits until a promise is resolved. + * + * @returns {DeferredPlugin} + * The plugin and resolver. + */ +function deferPlugin() { + /** @type {() => void} */ + let res + /** @type {(error: Error) => void} */ + let rej + /** @type {Promise} */ + const promise = new Promise((resolve, reject) => { + res = resolve + rej = reject + }) + + return { + resolve() { + res() + }, + reject(error) { + rej(error) + }, + plugin() { + return () => promise + } + } +} + +class ErrorBoundary extends Component { + state = { + error: null + } + + /** + * @param {Error} error + */ + componentDidCatch(error) { + this.setState({error}) + } + + render() { + const {children} = /** @type {{children: ReactNode}} */ (this.props) + const {error} = this.state + + return error ? String(error) : children + } +} From a40ae2e3131eca0421c43bc179b63f05be0bfbb9 Mon Sep 17 00:00:00 2001 From: Remco Haszing Date: Fri, 7 Mar 2025 10:56:37 +0100 Subject: [PATCH 117/125] Fix race condition in `MarkdownHooks` Running asynchronous code inside a `useEffect` causes race conditions. This is now handled correctly. Closes GH-896. Reviewed-by: Titus Wormer --- lib/index.js | 11 +++++++++-- test.jsx | 21 +++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/lib/index.js b/lib/index.js index c6067477..42fbe589 100644 --- a/lib/index.js +++ b/lib/index.js @@ -207,11 +207,18 @@ export function MarkdownHooks(options) { useEffect( function () { + let cancelled = false const file = createFile(options) processor.run(processor.parse(file), file, function (error, tree) { - setError(error) - setTree(tree) + if (!cancelled) { + setError(error) + setTree(tree) + } }) + + return () => { + cancelled = true + } }, [ options.children, diff --git a/test.jsx b/test.jsx index 8bbf4f7b..12b6caa5 100644 --- a/test.jsx +++ b/test.jsx @@ -1165,6 +1165,27 @@ test('MarkdownHooks', async function (t) { }) assert.equal(container.innerHTML, 'Error: rejected') }) + + await t.test('should support `MarkdownHooks` rerenders', async function () { + const pluginA = deferPlugin() + const pluginB = deferPlugin() + + const result = render( + + ) + + result.rerender( + + ) + + assert.equal(result.container.innerHTML, '') + pluginB.resolve() + pluginA.resolve() + await waitFor(() => { + assert.notEqual(result.container.innerHTML, '') + }) + assert.equal(result.container.innerHTML, '

    b

    ') + }) }) /** From 939c6671c9dbffccfe8e27bba256f62405031193 Mon Sep 17 00:00:00 2001 From: Remco Haszing Date: Fri, 7 Mar 2025 11:01:56 +0100 Subject: [PATCH 118/125] Add `fallback` prop to `MarkdownHooks` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `` component now supports a new prop named `fallback`. This fallback is displayed while the initial content hasn’t loaded yet. The name `fallback` was chosen to match the same prop from ``. Closes GH-897. Reviewed-by: Titus Wormer --- index.js | 1 + lib/index.js | 24 ++++++++++++++++++------ test.jsx | 22 ++++++++++++++++++++++ 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/index.js b/index.js index 629aec01..d0fc80e0 100644 --- a/index.js +++ b/index.js @@ -2,6 +2,7 @@ * @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 */ diff --git a/lib/index.js b/lib/index.js index 42fbe589..1cc48682 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,7 +1,7 @@ /** * @import {Element, Nodes, Parents, Root} from 'hast' * @import {Root as MdastRoot} from 'mdast' - * @import {ComponentType, JSX, ReactElement} from 'react' + * @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' @@ -77,6 +77,18 @@ * Change URLs (default: `defaultUrlTransform`) */ +/** + * @typedef HooksOptionsOnly + * Configuration specifically for {@linkcode MarkdownHooks}. + * @property {ReactNode} [fallback] + * Fallback content to render while the processor processing the markdown. + */ + +/** + * @typedef {Options & HooksOptionsOnly} HooksOptions + * Configuration for {@linkcode MarkdownHooks}. + */ + /** * @callback UrlTransform * Transform all URLs. @@ -94,7 +106,7 @@ 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 {createElement, useEffect, useState} from 'react' +import {useEffect, useState} from 'react' import remarkParse from 'remark-parse' import remarkRehype from 'remark-rehype' import {unified} from 'unified' @@ -193,10 +205,10 @@ export async function MarkdownAsync(options) { * For async support on the server, * see {@linkcode MarkdownAsync}. * - * @param {Readonly} options + * @param {Readonly} options * Props. - * @returns {ReactElement} - * React element. + * @returns {ReactNode} + * React node. */ export function MarkdownHooks(options) { const processor = createProcessor(options) @@ -230,7 +242,7 @@ export function MarkdownHooks(options) { if (error) throw error - return tree ? post(tree, options) : createElement(Fragment) + return tree ? post(tree, options) : options.fallback } /** diff --git a/test.jsx b/test.jsx index 12b6caa5..60c4ad84 100644 --- a/test.jsx +++ b/test.jsx @@ -1149,6 +1149,28 @@ test('MarkdownHooks', async function (t) { } ) + await t.test( + 'should support `MarkdownHooks` loading fallback', + async function () { + const plugin = deferPlugin() + + const {container} = render( + + ) + + assert.equal(container.innerHTML, 'Loading') + plugin.resolve() + await waitFor(() => { + assert.notEqual(container.innerHTML, 'Loading') + }) + assert.equal(container.innerHTML, '

    a

    ') + } + ) + await t.test('should support `MarkdownHooks` that error', async function () { const plugin = deferPlugin() From 544bff69fbd406b397bed3bc411f7bb12ad82b08 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Fri, 7 Mar 2025 11:24:04 +0100 Subject: [PATCH 119/125] Refactor code-style --- lib/index.js | 14 +++-- test.jsx | 169 +++++++++++++++++++++++++++++---------------------- 2 files changed, 108 insertions(+), 75 deletions(-) diff --git a/lib/index.js b/lib/index.js index 1cc48682..ebc3fdfb 100644 --- a/lib/index.js +++ b/lib/index.js @@ -80,13 +80,14 @@ /** * @typedef HooksOptionsOnly * Configuration specifically for {@linkcode MarkdownHooks}. - * @property {ReactNode} [fallback] - * Fallback content to render while the processor processing the markdown. + * @property {ReactNode | null | undefined} [fallback] + * Content to render while the processor processing the markdown (optional). */ /** * @typedef {Options & HooksOptionsOnly} HooksOptions - * Configuration for {@linkcode MarkdownHooks}. + * Configuration for {@linkcode MarkdownHooks}, + * extending the regular {@linkcode Options} with a `fallback` prop. */ /** @@ -221,6 +222,7 @@ export function MarkdownHooks(options) { function () { let cancelled = false const file = createFile(options) + processor.run(processor.parse(file), file, function (error, tree) { if (!cancelled) { setError(error) @@ -228,7 +230,11 @@ export function MarkdownHooks(options) { } }) - return () => { + /** + * @returns {undefined} + * Nothing. + */ + return function () { cancelled = true } }, diff --git a/test.jsx b/test.jsx index 60c4ad84..d9644a18 100644 --- a/test.jsx +++ b/test.jsx @@ -6,6 +6,17 @@ * @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' @@ -1112,83 +1123,92 @@ test('MarkdownAsync', async function (t) { test('MarkdownHooks', async function (t) { await t.test('should support `MarkdownHooks`', async function () { const plugin = deferPlugin() - - const {container} = render( + const result = render( ) - assert.equal(container.innerHTML, '') + assert.equal(result.container.innerHTML, '') + plugin.resolve() - await waitFor(() => { - assert.notEqual(container.innerHTML, '') + + await waitFor(function () { + assert.notEqual(result.container.innerHTML, '') }) - assert.equal(container.innerHTML, '

    a

    ') + + assert.equal(result.container.innerHTML, '

    a

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

    a

    ') - } - ) + plugin.resolve() - await t.test('should support `MarkdownHooks` that error', async function () { - const plugin = deferPlugin() + await waitFor(function () { + assert.notEqual(result.container.innerHTML, 'Loading') + }) - const {container} = render( + assert.equal(result.container.innerHTML, '

    a

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

    b

    ') }) }) /** - * @typedef DeferredPlugin - * @property {Plugin<[]>} plugin - * A unified plugin - * @property {() => void} resolve - * Resolve the plugin. - * @property {(error: Error) => void} reject - * Reject the plugin. - */ - -/** - * Create an async unified plugin which waits until a promise is resolved. + * Create an async unified plugin that waits until a promise is resolved or + * rejected from the outside. * * @returns {DeferredPlugin} - * The plugin and resolver. + * Deferred plugin object. */ function deferPlugin() { - /** @type {() => void} */ - let res /** @type {(error: Error) => void} */ - let rej + let hoistedReject + /** @type {() => void} */ + let hoistedResolve /** @type {Promise} */ - const promise = new Promise((resolve, reject) => { - res = resolve - rej = reject + const promise = new Promise(function (resolve, reject) { + hoistedResolve = resolve + hoistedReject = reject }) return { - resolve() { - res() + plugin() { + return function () { + return promise + } }, reject(error) { - rej(error) + hoistedReject(error) }, - plugin() { - return () => promise + resolve() { + hoistedResolve() } } } +/** + * Basic error boundary. + */ class ErrorBoundary extends Component { - state = { - error: null - } - /** * @param {Error} error + * Error. + * @returns {undefined} + * Nothing. */ componentDidCatch(error) { this.setState({error}) } render() { - const {children} = /** @type {{children: ReactNode}} */ (this.props) - const {error} = this.state + const props = /** @type {{children: ReactNode}} */ (this.props) - return error ? String(error) : children + return this.state.error ? String(this.state.error) : props.children + } + + state = { + /** + * @type {Error | undefined} + * Error. + */ + error: undefined } } From 26fdfe037516f9eee7e4c9472d633b795acc53e5 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Fri, 7 Mar 2025 11:29:06 +0100 Subject: [PATCH 120/125] Update docs --- lib/index.js | 4 ++-- readme.md | 22 ++++++++++++++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/lib/index.js b/lib/index.js index ebc3fdfb..b6f5ed00 100644 --- a/lib/index.js +++ b/lib/index.js @@ -86,8 +86,8 @@ /** * @typedef {Options & HooksOptionsOnly} HooksOptions - * Configuration for {@linkcode MarkdownHooks}, - * extending the regular {@linkcode Options} with a `fallback` prop. + * Configuration for {@linkcode MarkdownHooks}; + * extends the regular {@linkcode Options} with a `fallback` prop. */ /** diff --git a/readme.md b/readme.md index 69bf5bde..90514ea1 100644 --- a/readme.md +++ b/readme.md @@ -38,6 +38,7 @@ React component to render markdown. * [`AllowElement`](#allowelement) * [`Components`](#components) * [`ExtraProps`](#extraprops) + * [`HooksOptions`](#hooksoptions) * [`Options`](#options) * [`UrlTransform`](#urltransform) * [Examples](#examples) @@ -227,7 +228,7 @@ see [`MarkdownAsync`][api-markdown-async]. ###### Returns -React element (`ReactElement`). +React node (`ReactNode`). ### `defaultUrlTransform(url)` @@ -283,6 +284,20 @@ Extra fields we pass to components (TypeScript type). * `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). @@ -583,8 +598,9 @@ extensions. This package is fully typed with [TypeScript][]. It exports the additional types [`AllowElement`][api-allow-element], -[`ExtraProps`][api-extra-props], [`Components`][api-components], +[`ExtraProps`][api-extra-props], +[`HooksOptions`][api-hooks-options], [`Options`][api-options], and [`UrlTransform`][api-url-transform]. @@ -816,6 +832,8 @@ abide by its terms. [api-extra-props]: #extraprops +[api-hooks-options]: #hooksoptions + [api-markdown]: #markdown [api-markdown-async]: #markdownasync From f2369cd7b7f3c8eb01b7ba1221cf305b7474716f Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Fri, 7 Mar 2025 11:30:19 +0100 Subject: [PATCH 121/125] Refactor docs --- readme.md | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/readme.md b/readme.md index 90514ea1..ecdd661b 100644 --- a/readme.md +++ b/readme.md @@ -48,7 +48,6 @@ React component to render markdown. * [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) @@ -176,6 +175,15 @@ 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. @@ -593,17 +601,6 @@ 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 the additional types -[`AllowElement`][api-allow-element], -[`Components`][api-components], -[`ExtraProps`][api-extra-props], -[`HooksOptions`][api-hooks-options], -[`Options`][api-options], and -[`UrlTransform`][api-url-transform]. - ## Compatibility Projects maintained by the unified collective are compatible with maintained From 44d2e4a44b37461ab7778d6870c1a9eb36393ad2 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Fri, 7 Mar 2025 11:32:45 +0100 Subject: [PATCH 122/125] 10.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d0689a33..35fe1fa9 100644 --- a/package.json +++ b/package.json @@ -143,7 +143,7 @@ "strict": true }, "type": "module", - "version": "10.0.1", + "version": "10.1.0", "xo": { "envs": [ "shared-node-browser" From dea7340755c17335147be63ac135b461b3e7e582 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Wed, 2 Apr 2025 15:10:55 +0200 Subject: [PATCH 123/125] Fix tests for changes in React --- test.jsx | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/test.jsx b/test.jsx index d9644a18..1b254db0 100644 --- a/test.jsx +++ b/test.jsx @@ -544,12 +544,6 @@ test('Markdown', async function (t) { }) await t.test('should fail on an invalid component', function () { - const warn = console.warn - /** @type {unknown} */ - let message - - console.error = capture - assert.throws(function () { renderToStaticMarkup( ) }, /Element type is invalid/) - - console.error = warn - - assert.match(String(message), /type is invalid/) - - /** - * @param {unknown} d - * @returns {undefined} - */ - function capture(d) { - message = d - } }) await t.test('should support `components` (headings)', function () { From 8545ebd7416beb2af2bfef59440db231abe69f13 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Wed, 9 Apr 2025 12:02:23 +0200 Subject: [PATCH 124/125] Add docs on internals of `defaultUrlTransform` Related-to GH-904. --- lib/index.js | 4 ++++ readme.md | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/lib/index.js b/lib/index.js index b6f5ed00..6e3fdd21 100644 --- a/lib/index.js +++ b/lib/index.js @@ -412,6 +412,10 @@ function post(tree, options) { /** * 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. diff --git a/readme.md b/readme.md index ecdd661b..949b1801 100644 --- a/readme.md +++ b/readme.md @@ -242,6 +242,10 @@ React node (`ReactNode`). 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`) From fda7fa560bec901a6103e195f9b1979dab543b17 Mon Sep 17 00:00:00 2001 From: Remco Haszing Date: Mon, 21 Apr 2025 12:10:48 +0200 Subject: [PATCH 125/125] Add memo for processor to `MarkdownHooks` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some plugins may perform heavy work in preparation to make the transformer light-weight. A good example of this is `rehype-starry-night`. Without this change, an interactive markdown editor which includes `rehype-starry-night` is very sluggish. With this change, performance is as good as if `rehype-starry-night` isn’t even used. Closes GH-909. Reviewed-by: JounQin Reviewed-by: Titus Wormer --- lib/index.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/index.js b/lib/index.js index 6e3fdd21..06f07e12 100644 --- a/lib/index.js +++ b/lib/index.js @@ -107,7 +107,7 @@ 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, useState} from 'react' +import {useEffect, useMemo, useState} from 'react' import remarkParse from 'remark-parse' import remarkRehype from 'remark-rehype' import {unified} from 'unified' @@ -212,7 +212,12 @@ export async function MarkdownAsync(options) { * React node. */ export function MarkdownHooks(options) { - const processor = createProcessor(options) + const processor = useMemo( + function () { + return createProcessor(options) + }, + [options.rehypePlugins, options.remarkPlugins, options.remarkRehypeOptions] + ) const [error, setError] = useState( /** @type {Error | undefined} */ (undefined) ) @@ -238,12 +243,7 @@ export function MarkdownHooks(options) { cancelled = true } }, - [ - options.children, - options.rehypePlugins, - options.remarkPlugins, - options.remarkRehypeOptions - ] + [options.children, processor] ) if (error) throw error