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 This is a statement1 with a citation. This is a footnote for the citation. ↩ a a This is a statement1 with a citation. This is a footnote for the citation. ↩ This is a statement1 with a citation. This is a footnote for the citation. ↩ a a Test Languages are fun, right? User is writing a table by hand a This is a link to Espen.Codes. This is a link to Espen.Codes. Some characters, like æ, & and similar should be handled properly. 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
+Footnotes
\n\n
\n\n
'
+ '\n
'
assert.equal(actual, expected)
})
@@ -1134,7 +1134,7 @@ test('supports checkbox lists', () => {
const actual = asHtml(\n
\n
\n\n
'
+ '\n
\n
\n\n
'
)
})
From 27122277cc55dccb7fa22f77c26cce2fdbf622f0 Mon Sep 17 00:00:00 2001
From: Titus Wormer Footnotes
\n\n
\nFootnotes
\n\n
\n
+
{children}
)
From 184a1a320c8a5bb12fae622727e9d33cd54308e2 Mon Sep 17 00:00:00 2001
From: Titus Wormer
'
)
})
@@ -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(ID English Norwegian Italian 1 one en uno 2 two to due 3 three tre tre
'
)
})
@@ -889,7 +888,7 @@ test('should render footnote with custom options', () => {
asHtml(
Test Test b c\n
\n
\n\n
'
@@ -1202,7 +1209,9 @@ test('should render table of contents plugin', () => {
'## Third Section'
].join('\n')
- const actual = asHtml(Header
\nTable of Contents
\n\n
\n\n
\nFirst Section
\nSecond Section
\nSubsection
\nThird 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(\n
'
assert.equal(actual, expected)
@@ -1408,7 +1419,11 @@ test('should support table cells w/ style', () => {
}
const actual = asHtml(
-
'
From d044e34b4f22caa24545c0b355d2205de42dfc32 Mon Sep 17 00:00:00 2001
From: Titus Wormer a HTML entities
HTML
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.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 likecode, 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.
\nThis is a footnote for the citation. ↩
\nThis is a statement1 with a citation.
\nThis is a footnote for the citation. ↩
\n
+
{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
-
-- Lorem ipsum dolor sit amet
-- Consectetur adipiscing elit
-- Integer molestie lorem at massa
-
-Or:
-
-- You can use sequential numbers...
-- ...or keep all the numbers as
1.
-
-Start numbering with offset:
-
-- foo
-- 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
-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
-
-
-Autoconverted link https://github.com/remarkjs/react-markdown
-
-Images
-
-
-Like links, Images also have a footnote style syntax
-
-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
-
-
-
-
-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
\nCombining = 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, '')
-})
+ /**
+ * @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, '')
-})
+ 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( ),
+ '\na
\n
'
+ )
+ })
-test('should handle images without title attribute', () => {
- const input = 'This is .'
- const actual = asHtml( )
- assert.equal(actual, 'This is
.
')
-})
+ await t.test('should support a break', function () {
+ assert.equal(asHtml( ), 'a
\nb
')
+ })
-test('should handle images with title attribute', () => {
- const input = 'This is .'
- const actual = asHtml( )
- assert.equal(
- actual,
- 'This is
.
'
- )
-})
+ 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 .'
- const actual = asHtml(
- uri.replace(/\.png$/, '.jpg')}
- />
- )
- assert.equal(actual, 'This is
.
')
-})
+ 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
\nFootnotes
\n\n- \n
y ↩
\n \n
\n '
+ )
+ })
-test('should handle images w/ titles with custom uri transformer', () => {
- const input = 'Empty: '
- 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
.
'
- )
-})
+ 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 ."
- const actual = asHtml( )
- assert.equal(
- actual,
- 'This is
.
'
- )
-})
+ await t.test('should support an image', function () {
+ assert.equal(
+ asHtml( ),
+ '
'
+ )
+ })
-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( ),
+ '
'
+ )
+ })
-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( ),
+ '
'
+ )
+ })
-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( ),
+ ''
+ )
+ })
-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( ),
+ ''
+ )
+ })
-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( ),
+ ''
+ )
+ })
+
+ await t.test('should support prototype poluting identifiers', function () {
+ assert.equal(
+ asHtml(
+
+ ),
+ ''
+ )
+ })
-test('should handle code blocks by indentation', () => {
- const input = [
- '',
- ''
- ].join(' ')
- assert.equal(
- asHtml( ),
- '<footer class="footer">\n © 2014 Foo Bar\n</footer>\n
'
- )
-})
+ await t.test('should support duplicate definitions', function () {
+ assert.equal(
+ asHtml( ),
+ ''
+ )
+ })
-test('should handle blockquotes', () => {
- const input = '> Moo\n> Tools\n> FTW\n'
- const actual = asHtml( )
- assert.equal(actual, '\nMoo\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\nLots of ex-Mootoolers on the React team
\n
\nTotally didn't know that.
\n\nThere's a reason why it turned out so awesome
\n
\nHaha I guess you're right!
\n
'
- )
-})
+ await t.test('should support a list (ordered) / list item', function () {
+ assert.equal(
+ asHtml( ),
+ '\n- a
\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(
+
+ ),
+ 'a b c d
'
+ )
+ })
-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- Ordered
\n- Lists
\n- Are cool
\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- Ordered
\n- Lists
\n- Are cool
\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- bar
\n- baz
\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
\nb\n
\nc
'
- 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(
+
+ ),
+ ''
+ )
+ })
+
+ 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(
+
+ ),
+ '![c 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 ',
- 'Foo \n \n
\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>\nThis 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 ',
- 'Foo \n \n
\n\nThis is another',
- ' regular paragraph.'
- ].join('')
-
- const actual = asHtml( )
- assert.equal(
- actual,
- 'This is a regular paragraph.
\n\nThis 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 ',
- 'Foo \n \n
\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>\nThis 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
\nBar
')
-})
-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
\nBar
'
+ 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
\nBar
'
- )
-})
+ await t.test('should support `remarkRehypeOptions`', function () {
+ assert.equal(
+ asHtml(
+
+ ),
+ '\nFootnotes
\n\n- \n
a ↩
\n \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
\nb
'
+ )
-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,
- '\nParagraph
\n\n\n- List item
\n- List item 2
\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
\nParagraph
\nNew header
\n\n\n\n
\nFoo
'
- )
-})
+ /**
+ * @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
\nb\n
\nc
'
+ )
-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?
\nID English Norwegian Italian 1 one en uno 2 two to due 3 three tre tre
'
- )
-})
+ 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- b
\n
'
+ )
- assert.equal(
- asHtml( ),
- 'User is writing a table by hand
\nTest Test
'
+ 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- a
\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:
. 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.
\nFootnotes
\n\n- \n
This is a footnote for the citation. ↩
\n \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 = ''
- assert.equal(actual, expected)
-})
-test('should support duplicate definitions', () => {
- const input = '[a][]\n\n[a]: b\n[a]: c'
- const actual = asHtml( )
- const expected = ''
- 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\nParagraph
\nNew header\n\n- List item
\n- List item 2
\n
\nFoo
'
- )
-})
-
-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
\nCan be had with XSS links
\n\n\n
\nEntities can be tricky, too
\n\n\nShould allow mailto and tel links tho
\nAlso, protocol-agnostic should be allowed
\n\nallow 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 =
- ''
- assert.equal(actual, expected)
-})
+ })
+
+ await t.test('should support plugins (`remark-toc`)', function () {
+ assert.equal(
+ asHtml(
+
+ ),
+ `a
+Contents
+
+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
\nBar
\nBaz
')
-})
+ 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,
- ''
- )
-})
+ 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
\nTable of Contents
\n\n- First Section
\n- Second Section\n
\n- Subsection
\n
\n \n- Third Section
\n
\nFirst Section
\nSecond Section
\nSubsection
\nThird 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 =
- '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( ),
+ '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- b
\n
'
- )
+ assert.equal(
+ asHtml(
+
+ }
+ }}
+ remarkPlugins={[remarkGfm]}
+ />
+ ),
+ '\n- a
\n
\n\n- b
\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- a
\n
'
- )
+ assert.equal(
+ asHtml(
+
+ }
+ }}
+ />
+ ),
+ '\n- a
\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:
- a
- b
+ a
+ b
@@ -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( ),
'\na
\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
\nFootnotes
\n\n- \n
y ↩
\n \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( ),
'
'
)
})
await t.test('should support an image w/ a title', function () {
assert.equal(
- asHtml( ),
+ renderToStaticMarkup( ),
'
'
)
})
await t.test('should support an image reference / definition', function () {
assert.equal(
- asHtml( ),
+ renderToStaticMarkup( ),
'
'
)
})
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( ),
''
)
})
await t.test('should support a link w/ a title', function () {
assert.equal(
- asHtml( ),
+ renderToStaticMarkup( ),
''
)
})
await t.test('should support a link reference / definition', function () {
assert.equal(
- asHtml( ),
+ renderToStaticMarkup( ),
''
)
})
await t.test('should support prototype poluting identifiers', function () {
assert.equal(
- asHtml(
+ renderToStaticMarkup(
),
+ renderToStaticMarkup( ),
''
)
})
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- a
\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( ),
'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( ),
- '
'
+ // Note: React weirdly adds `rel="preload"`.
+ '
'
)
})
await t.test('should support an image w/ a title', function () {
assert.equal(
renderToStaticMarkup( ),
- '
'
+ // Note: React weirdly adds `rel="preload"`.
+ '
'
)
})
await t.test('should support an image reference / definition', function () {
assert.equal(
renderToStaticMarkup( ),
- '
'
+ // Note: React weirdly adds `rel="preload"`.
+ '
'
)
})
@@ -410,7 +413,7 @@ test('react-markdown', async function (t) {
}}
/>
),
- '![c a]()
'
+ '![c 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