From 77c680fb7f51593466042a897025ecb6c264c784 Mon Sep 17 00:00:00 2001 From: Titus Date: Mon, 12 Apr 2021 08:31:03 +0200 Subject: [PATCH] Replace `renderers` w/ `components`, remove HTML parser from core (#563) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Replace `renderers` w/ `components` * Replace `allowNode` w/ `allowElement`, which is now given a hast element (as the first parameter) * Replace `allowedTypes` w/ `allowedElements` * Replace `disallowedTypes` w/ `disallowedElements` * Change signature of `linkTarget` and `transformLinkUri`, which are now given hast children (as the second parameter) * Change signature of `transformImageUri`, which is now given the `alt` string as the second parameter (instead of the fourth) * Replace `plugins` w/ `remarkPlugins` (backwards compatible change) * Add `rehypePlugins` * Change `includeNodeIndex` to `includeElementIndex`: it still sets an `index`, but that value now represents the number of preceding elements, it also sets a `siblingCount` (instead of `parentChildCount`) with the number of sibling elements in the parent * The `columnAlignment` prop is no longer given to table elements: it’s available as `style` on `th` and `td` elements instead * The `spread` prop is no longer given to list elements: it’s already handled Remove buggy HTML parsers from core * If you want HTML, add [`rehype-raw`](https://github.com/rehypejs/rehype-raw) to `rehypePlugins` and it’ll work without bugs! * Remove `allowDangerousHtml` (previously called `escapeHtml`) option: pass `rehype-raw` in `rehypePlugins` to allow HTML instead * Remove `with-html.js`, `plugins/html-parser.js` entries from library * Remove naïve HTML parser too: either use `rehype-raw` to properly support HTML, or don’t allow it at all Closes GH-549. Closes GH-563. The following issues are solved as rehype is now available: Closes GH-522. Closes GH-465. Closes GH-427. Closes GH-384. Closes GH-356. The following issues are solved as a proper HTML parser (`rehype-raw`) is now available: Closes GH-562. Closes GH-460. Closes GH-454. Closes GH-452. Closes GH-433. Closes GH-386. Closes GH-385. Closes GH-345. Closes GH-320. Closes GH-302. Closes GH-267. Closes GH-259. The following issues are solved as docs are improved: Closes GH-251. --- index.d.ts | 69 +- package.json | 26 +- plugins/html-parser.js | 1 - react-markdown-types-test.tsx | 22 +- readme.md | 321 ++-- src/ast-to-react.js | 409 +++-- src/get-definitions.js | 12 - src/plugins/html-parser.js | 173 -- src/react-markdown.js | 85 +- src/rehype-filter.js | 46 + src/remark-filter-nodes.js | 48 - src/remark-parse-html.js | 102 -- src/remark-wrap-table-rows.js | 36 - src/renderers.js | 131 -- src/with-html.js | 16 - .../__snapshots__/react-markdown.test.js.snap | 1433 +++++++++++------ test/fixtures/runthrough.md | 4 +- test/react-markdown.test.js | 634 ++++---- tsconfig.json | 3 +- with-html.d.ts | 9 - with-html.js | 1 - 21 files changed, 1799 insertions(+), 1782 deletions(-) delete mode 100644 plugins/html-parser.js delete mode 100644 src/get-definitions.js delete mode 100644 src/plugins/html-parser.js create mode 100644 src/rehype-filter.js delete mode 100644 src/remark-filter-nodes.js delete mode 100644 src/remark-parse-html.js delete mode 100644 src/remark-wrap-table-rows.js delete mode 100644 src/renderers.js delete mode 100644 src/with-html.js delete mode 100644 with-html.d.ts delete mode 100644 with-html.js diff --git a/index.d.ts b/index.d.ts index ff96264e..55a678d5 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,9 +1,9 @@ // TypeScript Version: 3.4 -import {ElementType, ReactNode, ReactElement} from 'react' +import {ElementType, ReactElement} from 'react' import {PluggableList} from 'unified' import * as unist from 'unist' -import * as mdast from 'mdast' +import * as hast from 'hast' type Not = { [key in keyof T]?: never @@ -16,34 +16,40 @@ declare namespace ReactMarkdown { type Position = unist.Position - type NodeType = mdast.Content['type'] + type LinkTargetResolver = ( + href: string, + children: Array, + title?: string + ) => string - type AlignType = mdast.AlignType - - type ReferenceType = mdast.ReferenceType - - type LinkTargetResolver = (uri: string, text: string, title?: string) => string - - type Renderer = (props: T) => ElementType - interface Renderers { - [key: string]: string | Renderer + type Component = (props: T) => ElementType + interface Components { + [key: string]: string | Component } interface ReactMarkdownPropsBase { readonly className?: string + readonly skipHtml?: boolean readonly sourcePos?: boolean - readonly includeNodeIndex?: boolean + readonly includeElementIndex?: boolean readonly rawSourcePos?: boolean - readonly allowNode?: (node: mdast.Content, index: number, parent: NodeType) => boolean + readonly allowElement?: ( + node: hast.Element, + index: number, + parent: hast.Element | hast.Root + ) => boolean readonly linkTarget?: string | LinkTargetResolver readonly transformLinkUri?: - | ((uri: string, children?: ReactNode, title?: string) => string) - | null - readonly transformImageUri?: - | ((uri: string, children?: ReactNode, title?: string, alt?: string) => string) + | (( + href: string, + children: Array, + title?: string + ) => string) | null - readonly renderers?: {[nodeType: string]: ElementType} - readonly plugins?: PluggableList + readonly transformImageUri?: ((src: string, alt: string, title?: string) => string) | null + readonly components?: {[tagName: string]: ElementType} + readonly remarkPlugins?: PluggableList + readonly rehypePlugins?: PluggableList readonly unwrapDisallowed?: boolean } @@ -51,30 +57,19 @@ declare namespace ReactMarkdown { readonly children: string } - interface AllowedTypesProp { - readonly allowedTypes?: NodeType[] + interface AllowedElementsProp { + readonly allowedElements?: string[] } - interface DisallowedTypesProp { - readonly disallowedTypes: NodeType[] - } - - interface AllowDangerousHtmlProp { - readonly allowDangerousHtml?: boolean - } - - interface SkipHtmlProp { - readonly skipHtml?: boolean + interface DisallowedElementsProp { + readonly disallowedElements: string[] } type ReactMarkdownProps = ReactMarkdownPropsBase & ChildrenProp & - MutuallyExclusive & - MutuallyExclusive + MutuallyExclusive - const types: NodeType[] - const renderers: Renderers - function uriTransformer(uri: string): string + function uriTransformer(url: string): string } declare function ReactMarkdown(props: ReactMarkdown.ReactMarkdownProps): ReactElement diff --git a/package.json b/package.json index a64f0084..db39b355 100644 --- a/package.json +++ b/package.json @@ -74,19 +74,21 @@ "lib/", "plugins/", "index.d.ts", - "react-markdown.min.js", - "with-html.d.ts", - "with-html.js" + "react-markdown.min.js" ], "dependencies": { - "@types/mdast": "^3.0.3", + "@types/hast": "^2.0.0", "@types/unist": "^2.0.3", - "html-to-react": "^1.4.5", - "mdast-add-list-metadata": "1.0.1", + "comma-separated-tokens": "^1.0.0", "prop-types": "^15.7.2", + "property-information": "^5.0.0", "react-is": "^17.0.0", "remark-parse": "^9.0.0", + "remark-rehype": "^8.0.0", + "space-separated-tokens": "^1.1.0", + "style-to-object": "^0.3.0", "unified": "^9.0.0", + "unist-util-is": "^4.1.0", "unist-util-visit": "^2.0.0" }, "peerDependencies": { @@ -121,6 +123,7 @@ "react": "^17.0.0", "react-dom": "^17.0.0", "react-test-renderer": "^17.0.0", + "rehype-raw": "^5.0.0", "remark-cli": "^9.0.0", "remark-gfm": "^1.0.0", "remark-math": "^4.0.0", @@ -199,19 +202,24 @@ "overrides": [ { "files": [ - "src/**/*.js", - "with-html.js" + "src/**/*.js" ], "extends": [ "plugin:es/restrict-to-es2015" ] }, { + "env": { + "jest": true + }, "files": [ "test/**/*.js" ], "rules": { - "react/no-children-prop": 0 + "react/no-children-prop": 0, + "react/display-name": 0, + "no-nested-ternary": 0, + "react/prop-types": 0 } } ] diff --git a/plugins/html-parser.js b/plugins/html-parser.js deleted file mode 100644 index d5121530..00000000 --- a/plugins/html-parser.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('../lib/plugins/html-parser') diff --git a/react-markdown-types-test.tsx b/react-markdown-types-test.tsx index dc9c4df4..93aea3aa 100644 --- a/react-markdown-types-test.tsx +++ b/react-markdown-types-test.tsx @@ -1,7 +1,6 @@ import * as React from 'react' import * as ReactDom from 'react-dom' import * as ReactMarkdown from 'react-markdown' -import * as ReactMarkdownWitHtml from 'react-markdown/with-html' /* must have children */ let test = # header @@ -9,35 +8,26 @@ test = // $ExpectError test = -/* should support allowedTypes or disallowedTypes, but not both */ -test = # header -test = # header +/* should support allowedElements or disallowedElements, but not both */ +test = # header +test = # header test = ( - + # header ) test = ( - + # header ) test = ( // $ExpectError - + # header ) -/* should support skipHtml or allowDangerousHtml, but not both */ test = # header -test = # header -test = ( - // $ExpectError - - # header - -) ReactDom.render(# header, document.body) -ReactDom.render(# header, document.body) diff --git a/readme.md b/readme.md index 000149b3..941e3a16 100644 --- a/readme.md +++ b/readme.md @@ -8,6 +8,10 @@ [![Backers][backers-badge]][collective] [![Chat][chat-badge]][chat] +⚠️ Note: the below readme is for the upcoming release of `react-markdown@6.0.0`. +[See the last released readme (`5.0.3`) here +»](https://github.com/remarkjs/react-markdown/tree/22bb78747d768181cb9ea8711b5e13c3768921d8#readme) + Markdown component for React using [**remark**][remark]. [Learn markdown here][learn] and [check out the demo here][demo]. @@ -65,7 +69,7 @@ const gfm = require('remark-gfm') const markdown = `Just a link: https://reactjs.com.` -render(, document.body) +render(, document.body) ```
@@ -87,57 +91,57 @@ render(, document.body) Markdown to parse * `className` (`string?`)\ Wrap the markdown in a `div` with this class name -* `allowDangerousHtml` (`boolean`, default: `false`)\ - This project is safe by default and escapes HTML. - Use `allowDangerousHtml: true` to allow dangerous html instead. - See [security][] * `skipHtml` (`boolean`, default: `false`)\ - Ignore HTML in Markdown + Ignore HTML in Markdown completely * `sourcePos` (`boolean`, default: `false`)\ - Pass a prop to all renderers with a serialized position + Pass a prop to all components with a serialized position (`data-sourcepos="3:1-3:13"`) * `rawSourcePos` (`boolean`, default: `false`)\ - Pass a prop to all renderers with their [position][] + Pass a prop to all components with their [position][] (`sourcePosition: {start: {line: 3, column: 1}, end:…}`) -* `includeNodeIndex` (`boolean`, default: `false`)\ - Pass [`index`][index] and `parentChildCount` in props to all renderers -* `allowedTypes` (`Array.`, default: list of all types)\ - Node types to allow (can’t combine w/ `disallowedTypes`). - All types are available at `ReactMarkdown.types` -* `disallowedTypes` (`Array.`, default: `[]`)\ - Node types to disallow (can’t combine w/ `allowedTypes`) -* `allowNode` (`(node, index, parent) => boolean?`, optional)\ - Function called to check if a node is allowed (when truthy) or not. - `allowedTypes` / `disallowedTypes` is used first! +* `includeElementIndex` (`boolean`, default: `false`)\ + Pass the `index` (number of elements before it) and `siblingCount` (number + of elements in parent) as props to all components +* `allowedElements` (`Array.`, default: `undefined`)\ + Tag names to allow (can’t combine w/ `disallowedElements`). + By default all elements are allowed +* `disallowedElements` (`Array.`, default: `undefined`)\ + Tag names to disallow (can’t combine w/ `allowedElements`). + By default no elements are disallowed +* `allowElement` (`(element, index, parent) => boolean?`, optional)\ + Function called to check if an element is allowed (when truthy) or not. + `allowedElements` / `disallowedElements` is used first! * `unwrapDisallowed` (`boolean`, default: `false`)\ - Extract (unwrap) the children of not allowed nodes. - By default, when `strong` is not allowed, it and it’s content is dropped, - but with `unwrapDisallowed` the node itself is dropped but the content used -* `linkTarget` (`string` or `(url, text, title) => string`, optional)\ + Extract (unwrap) the children of not allowed elements. + By default, when `strong` is not allowed, it and it’s children is dropped, + but with `unwrapDisallowed` the element itself is dropped but the children + used +* `linkTarget` (`string` or `(href, children, title) => string`, optional)\ Target to use on links (such as `_blank` for ` string`, default: +* `transformLinkUri` (`(href, children, title) => string`, default: [`./uri-transformer.js`][uri], optional)\ URL to use for links. The default allows only `http`, `https`, `mailto`, and `tel`, and is available at `ReactMarkdown.uriTransformer`. Pass `null` to allow all URLs. See [security][] -* `transformImageUri` (`(uri) => string`, default: +* `transformImageUri` (`(src, alt, title) => string`, default: [`./uri-transformer.js`][uri], optional)\ Same as `transformLinkUri` but for images -* `renderers` (`Object.`, default: `{}`)\ - Object mapping node types to React components. - Merged with the default renderers (available at `ReactMarkdown.renderers`). - Which props are passed varies based on the node -* `plugins` (`Array.`, default: `[]`)\ +* `components` (`Object.`, default: `{}`)\ + Object mapping tag names to React components +* `remarkPlugins` (`Array.`, default: `[]`)\ List of [remark plugins][remark-plugins] to use. See the next section for examples on how to pass options +* `rehypePlugins` (`Array.`, default: `[]`)\ + List of [rehype plugins][rehype-plugins] to use. + See the next section for examples on how to pass options ## Examples ### Use a plugin -This example shows how to 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: @@ -161,7 +165,7 @@ A table: | - | - | ` -render(, document.body) +render(, document.body) ```
@@ -215,7 +219,7 @@ import {render} from 'react-dom' import gfm from 'remark-gfm' render( - + This ~is not~ strikethrough, but ~~this is~~! , document.body @@ -233,10 +237,10 @@ render(
-### Use custom renderers (syntax highlight) +### Use custom components (syntax highlight) -This example shows how you can overwrite the normal handling of a node by -passing a renderer. +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]: @@ -248,9 +252,12 @@ import {Prism as SyntaxHighlighter} from 'react-syntax-highlighter' import {dark} from 'react-syntax-highlighter/dist/esm/styles/prism' import {render} from 'react-dom' -const renderers = { - code: ({language, value}) => { - return +const components = { + code({node, className, ...props}) { + const match = /language-(\w+)/.exec(className || '') + return match + ? + : } } @@ -262,7 +269,7 @@ console.log('It works!') ~~~ ` -render(, document.body) +render(, document.body) ```
@@ -271,35 +278,32 @@ render(, document.bod ```jsx <>

Here is some JavaScript code:

- +
+    
+  
```
-### Use a plugin and custom renderers (math) +### Use remark and rehype plugins (math) -This example shows how a syntax extension is used to support math in markdown -that adds new node types ([`remark-math`][math]), which are then handled by -renderers to use [`@matejmazur/react-katex`][react-katex]: +This example shows how a syntax extension (through [`remark-math`][math]) +is used to support math in markdown, and a transform plugin +([`rehype-katex`][katex]) to render that math. ```jsx import React from 'react' -import ReactMarkdown from 'react-markdown' -import Tex from '@matejmazur/react-katex' import {render} from 'react-dom' -import math from 'remark-math' -import 'katex/dist/katex.min.css' // `react-katex` does not import the CSS for you - -const renderers = { - inlineMath: ({value}) => , - math: ({value}) => -} +import ReactMarkdown from 'react-markdown' +import remarkMath from 'remark-math' +import rehypeKatex from 'rehype-katex' +import 'katex/dist/katex.min.css' // `rehype-katex` does not import the CSS for you render( , document.body @@ -311,7 +315,18 @@ render( ```jsx

- The lift coefficient () is a dimensionless coefficient. + The lift coefficient ( + + + + {/* … */} + + + + + ) is a dimensionless coefficient.

``` @@ -319,97 +334,145 @@ render( ## Appendix A: HTML in markdown -`react-markdown` typically escapes HTML (or ignores it, with `skipHtml`), +`react-markdown` typically escapes HTML (or ignores it, with `skipHtml`) because it is dangerous and defeats the purpose of this library. -However, if you are in a trusted environment (you trust the markdown), you can -`react-markdown/with-html`: +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]: ```jsx -const React = require('react') -const ReactMarkdownWithHtml = require('react-markdown/with-html') -const render = require('react-dom').render +import React from 'react' +import ReactMarkdown from 'react-markdown' +import rehypeRaw from 'rehype-raw' +import {render} from 'react-dom' -const markdown = ` -This Markdown contains
HTML, and will require the html-parser AST plugin to be loaded, in addition to setting the allowDangerousHtml property to false. -` +const input = `
+ +Some *emphasis* and strong! -render(, document.body) +
` + +render(, document.body) ```
Show equivalent JSX ```jsx -

- This Markdown contains HTML, and will require - the html-parser AST plugin to be loaded, in addition to setting the{' '} - allowDangerousHtml property to false. -

+
+

Some emphasis and strong!

+
```
-If you want to specify options for the HTML parsing step, you can instead import -the extension directly: +**Note**: HTML in markdown is still bound by how [HTML works in +CommonMark][cm-html]. +Make sure to use blank lines around block-level HTML that again contains +markdown! -```jsx -const ReactMarkdown = require('react-markdown') -const htmlParser = require('react-markdown/plugins/html-parser') +## Appendix B: Components -// For more info on the processing instructions, see -// -const parse = htmlParser({ - isValidNode: (node) => node.type !== 'script', - processingInstructions: [/* ... */] -}) +You can also change the things that come from markdown: - +```js + + }} +/> ``` -## Appendix B: Node types - -The node types available by default are: - -* `root` — Whole document -* `text` — Text (`foo`) -* `break` — Hard break (`
`) -* `paragraph` — Paragraph (`

`) -* `emphasis` — Emphasis (``) -* `strong` — Strong (``) -* `thematicBreak` — Horizontal rule (`


`) -* `blockquote` — Block quote (`
`) -* `link` — Link (``) -* `image` — Image (``) -* `linkReference` — Link through a reference (``) -* `imageReference` — Image through a reference (``) -* `list` — List (`