diff --git a/.gitignore b/.gitignore index f746949..fcb2607 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,4 @@ node_modules/ *.d.ts *.log yarn.lock -!/lib/complex-types.d.ts !/index.d.ts diff --git a/index.d.ts b/index.d.ts index 6b5e293..dee7c4d 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,16 +1,47 @@ import type {Root} from 'hast' -import type {ReactElement} from 'react' import type {Plugin} from 'unified' import type {Options} from './lib/index.js' +export type {Components, Options} from 'hast-util-to-jsx-runtime' + /** - * Plugin to compile to React + * Turn HTML into preact, react, solid, svelte, vue, etc. + * + * ###### Result + * + * This plugin registers a compiler that returns a `JSX.Element` where + * compilers typically return `string`. + * When using `.stringify` on `unified`, the result is such a `JSX.Element`. + * When using `.process` (or `.processSync`), the result is available at + * `file.result`. + * + * ###### Frameworks + * + * There are differences between what JSX frameworks accept, such as whether + * they accept `class` or `className`, or `background-color` or + * `backgroundColor`. + * + * For hast elements transformed by this project, this is be handled through + * options: + * + * | Framework | `elementAttributeNameCase` | `stylePropertyNameCase` | + * | --------- | -------------------------- | ----------------------- | + * | Preact | `'html'` | `'dom'` | + * | React | `'react'` | `'dom'` | + * | Solid | `'html'` | `'css'` | + * | Vue | `'html'` | `'dom'` | * * @param options - * Configuration. + * Configuration (required). + * @returns + * Nothing. */ -// Note: defining all react nodes as result value seems to trip TS up. -declare const rehypeReact: Plugin<[Options], Root, ReactElement> +declare const rehypeReact: Plugin<[Options], Root, JSX.Element> export default rehypeReact -export type {Options} from './lib/index.js' +// Register the result type. +declare module 'unified' { + interface CompileResultMap { + JsxElement: JSX.Element + } +} diff --git a/index.js b/index.js index dab322b..6a4c25d 100644 --- a/index.js +++ b/index.js @@ -1 +1,2 @@ +// Note: types exposed from `index.d.ts`. export {default} from './lib/index.js' diff --git a/lib/complex-types.d.ts b/lib/complex-types.d.ts deleted file mode 100644 index a018ca7..0000000 --- a/lib/complex-types.d.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type {ComponentType} from 'react' -import type {Element} from 'hast' - -type WithNode = { - node: Element -} - -export type ComponentsWithNodeOptions = { - /** - * Expose hast elements as a `node` field in components - */ - passNode: true - /** - * Override default elements (such as ``, `

`, etcetera) by passing an - * object mapping tag names to components. - */ - components?: Partial<{ - [TagName in keyof JSX.IntrinsicElements]: - | keyof JSX.IntrinsicElements - | ComponentType - }> -} - -export type ComponentsWithoutNodeOptions = { - /** - * Expose hast elements as a `node` field in components. - */ - passNode?: false | undefined - - /** - * Override default elements (such as ``, `

`, etcetera) by passing an - * object mapping tag names to components. - */ - components?: Partial<{ - [TagName in keyof JSX.IntrinsicElements]: - | keyof JSX.IntrinsicElements - | ComponentType - }> -} diff --git a/lib/index.js b/lib/index.js index de9720f..0be12d3 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,131 +1,29 @@ /** * @typedef {import('hast').Root} Root - * @typedef {import('react').ReactNode} ReactNode - * @typedef {import('react').ReactElement} ReactElement - * - * @callback CreateElementLike - * @param {any} name - * @param {any} props - * @param {...ReactNode} children - * @returns {ReactNode} - * - * @typedef SharedOptions - * Base configuration (without `components`). - * @property {CreateElementLike} createElement - * How to create elements or components. - * You should typically pass `React.createElement`. - * @property {((props: any) => ReactNode)|undefined} [Fragment] - * Create fragments instead of an outer `

` if available. - * You should typically pass `React.Fragment`. - * @property {string|undefined} [prefix='h-'] - * React key prefix. - * @property {boolean|undefined} [fixTableCellAlign=true] - * Fix obsolete align attributes on table cells by turning them - * into inline styles. - * Keep it on when working with markdown, turn it off when working - * with markup for emails. - * The default is `true`. - * - * @typedef {SharedOptions & (import('./complex-types.js').ComponentsWithNodeOptions | import('./complex-types.js').ComponentsWithoutNodeOptions)} Options - * Configuration. + * @typedef {import('hast-util-to-jsx-runtime').Options} Options + * @typedef {import('unified').Compiler} Compiler + * @typedef {import('unified').Processor} Processor */ -import {toH} from 'hast-to-hyperscript' -// @ts-expect-error: hush. -import tableCellStyle from '@mapbox/hast-util-table-cell-style' -import {whitespace} from 'hast-util-whitespace' - -const own = {}.hasOwnProperty -const tableElements = new Set(['table', 'thead', 'tbody', 'tfoot', 'tr']) +import {toJsxRuntime} from 'hast-util-to-jsx-runtime' /** - * Compile HTML to React nodes. - * - * > 👉 **Note**: this compiler returns a React node where compilers typically - * > return `string`. - * > When using `.stringify`, the result is such a React node. - * > When using `.process` (or `.processSync`), the result is available at - * > `file.result`. + * Turn HTML into preact, react, solid, svelte, vue, etc. * - * @this {import('unified').Processor} - * @type {import('unified').Plugin<[Options], Root, ReactElement>} + * @param {Options} options + * Configuration (required). + * @returns {undefined} + * Nothing. */ export default function rehypeReact(options) { - if (!options || typeof options.createElement !== 'function') { - throw new TypeError('createElement is not a function') - } - - const createElement = options.createElement - - const fixTableCellAlign = options.fixTableCellAlign !== false - - Object.assign(this, {Compiler: compiler}) - - // @ts-expect-error: to do: register result. - /** @type {import('unified').Compiler} */ - function compiler(node) { - /** @type {ReactNode} */ - let result = toH( - // @ts-expect-error: assume `name` is a known element. - h, - fixTableCellAlign ? tableCellStyle(node) : node, - options.prefix - ) - - if (node.type === 'root') { - // Invert . - result = - result && - typeof result === 'object' && - 'type' in result && - 'props' in result && - result.type === 'div' && - (node.children.length !== 1 || node.children[0].type !== 'element') - ? // `children` does exist. - // type-coverage:ignore-next-line - result.props.children - : [result] - - return createElement(options.Fragment || 'div', {}, result) - } - - return result - } - - /** - * @param {keyof JSX.IntrinsicElements} name - * @param {Record} props - * @param {Array} [children] - * @returns {ReactNode} - */ - function h(name, props, children) { - // Currently, a warning is triggered by react for *any* white space in - // tables. - // So we remove the pretty lines for now. - // See: . - // See: . - // See: . - // See: . - // See: . - // See: . - if (children && tableElements.has(name)) { - children = children.filter( - (child) => typeof child !== 'string' || !whitespace(child) - ) - } - - if (options.components && own.call(options.components, name)) { - const component = options.components[name] - - if (options.passNode && typeof component === 'function') { - // @ts-expect-error: `toH` passes the current node. - // type-coverage:ignore-next-line - props = Object.assign({node: this}, props) - } + // @ts-expect-error: TypeScript doesn’t handle `this` well. + // eslint-disable-next-line unicorn/no-this-assignment + const self = /** @type {Processor} */ (this) - return createElement(component, props, children) - } + self.compiler = compiler - return createElement(name, props, children) + /** @type {Compiler} */ + function compiler(tree, file) { + return toJsxRuntime(tree, {filePath: file.path, ...options}) } } diff --git a/package.json b/package.json index 70f6b8a..12f6373 100644 --- a/package.json +++ b/package.json @@ -45,29 +45,24 @@ "index.js" ], "dependencies": { - "@mapbox/hast-util-table-cell-style": "^0.2.0", "@types/hast": "^3.0.0", - "hast-to-hyperscript": "^10.0.0", - "hast-util-whitespace": "^3.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "rehype-parse": "^9.0.0", "unified": "^11.0.0" }, - "peerDependencies": { - "@types/react": ">=17" - }, "devDependencies": { "@types/node": "^20.0.0", + "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "c8": "^8.0.0", "hastscript": "^8.0.0", "prettier": "^3.0.0", "react": "^18.0.0", "react-dom": "^18.0.0", - "remark": "^14.0.0", "remark-cli": "^11.0.0", "remark-preset-wooorm": "^9.0.0", "type-coverage": "^2.0.0", "typescript": "^5.0.0", - "unist-builder": "^4.0.0", "xo": "^0.56.0" }, "scripts": { @@ -95,13 +90,19 @@ "atLeast": 100, "detail": true, "ignoreCatch": true, - "#": "needed `any`s", - "ignoreFiles": [ - "lib/index.d.ts" - ], "strict": true }, "xo": { + "overrides": [ + { + "files": [ + "*.ts" + ], + "rules": { + "@typescript-eslint/consistent-type-definitions": "off" + } + } + ], "prettier": true, "#": "`xo` is wrong about file extensions", "rules": { diff --git a/readme.md b/readme.md index e7e8e67..0f1b8ea 100644 --- a/readme.md +++ b/readme.md @@ -8,7 +8,7 @@ [![Backers][backers-badge]][collective] [![Chat][chat-badge]][chat] -**[rehype][]** plugin to compile HTML to React nodes. +**[rehype][]** plugin to turn HTML into preact, react, solid, svelte, vue, etc. ## Contents @@ -18,6 +18,8 @@ * [Use](#use) * [API](#api) * [`unified().use(rehypeReact, options)`](#unifieduserehypereact-options) + * [`Components`](#components) + * [`Options`](#options) * [Types](#types) * [Compatibility](#compatibility) * [Security](#security) @@ -28,21 +30,21 @@ ## What is this? This package is a [unified][] ([rehype][]) plugin that compiles HTML (hast) to -React nodes (the virtual DOM that React uses). +any JSX runtime (preact, react, solid, svelte, vue, etc). **unified** is a project that transforms content with abstract syntax trees (ASTs). **rehype** adds support for HTML to unified. **hast** is the HTML AST that rehype uses. -This is a rehype plugin that adds a compiler to compile hast to React nodes. +This is a rehype plugin that adds a compiler to compile hast to a JSX runtime. ## When should I use this? This plugin adds a compiler for rehype, which means that it turns the final -HTML (hast) syntax tree into something else (in this case, a React node). +HTML (hast) syntax tree into something else (in this case, a `JSX.Element`). It’s useful when you’re already using unified (whether remark or rehype) or are open to learning about ASTs (they’re powerful!) and want to render content in -your React app. +your app. If you’re not familiar with unified, then [`react-markdown`][react-markdown] might be a better fit. @@ -77,29 +79,41 @@ In browsers with [`esm.sh`][esmsh]: ## Use -Say our React app module `example.js` looks as follows: +Say our React app `example.js` looks as follows: ```js -import {createElement, Fragment, useEffect, useState} from 'react' -import {unified} from 'unified' +import {Fragment, createElement, useEffect, useState} from 'react' +import * as prod from 'react/jsx-runtime' import rehypeParse from 'rehype-parse' import rehypeReact from 'rehype-react' +import {unified} from 'unified' + +// @ts-expect-error: the react types are missing. +const production = {Fragment: prod.Fragment, jsx: prod.jsx, jsxs: prod.jsxs} const text = `

Hello, world!

Welcome to my page 👀

` +/** + * @param {string} text + * @returns {JSX.Element} + */ function useProcessor(text) { - const [Content, setContent] = useState(Fragment) - - useEffect(() => { - unified() - .use(rehypeParse, {fragment: true}) - .use(rehypeReact, {createElement, Fragment}) - .process(text) - .then((file) => { + const [Content, setContent] = useState(createElement(Fragment)) + + useEffect( + function () { + ;(async function () { + const file = await unified() + .use(rehypeParse, {fragment: true}) + .use(rehypeReact, production) + .process(text) + setContent(file.result) - }) - }, [text]) + })() + }, + [text] + ) return Content } @@ -109,7 +123,7 @@ export default function App() { } ``` -Assuming that runs in Next.js, Create React App (CRA), or similar, we’d get: +…running that in Next.js or similar, we’d get: ```html

Hello, world!

@@ -123,69 +137,96 @@ The default export is `rehypeReact`. ### `unified().use(rehypeReact, options)` -Compile HTML to React nodes. - -> 👉 **Note**: this compiler returns a React node where compilers typically -> return `string`. -> When using `.stringify`, the result is such a React node. -> When using `.process` (or `.processSync`), the result is available at -> `file.result`. - -##### `options` - -Configuration (optional). - -###### `options.createElement` - -How to create elements or components (`Function`, required). -You should typically pass `React.createElement`. - -###### `options.Fragment` - -Create fragments instead of an outer `
` if available (`symbol`). -You should typically pass `React.Fragment`. - -###### `options.components` - -Override default elements (such as ``, `

`, etc.) by passing an object -mapping tag names to components (`Record`, default: `{}`). - -For example, to use `` components instead of ``, and `` -instead of `

`, so something like this: - -```js - // … - .use(rehypeReact, { - createElement: React.createElement, - components: { - a: MyLink, - p: MyParagraph - } - }) - // … -``` - -###### `options.prefix` - -React key prefix (`string`, default: `'h-'`). - -###### `options.passNode` - -Pass the original hast node as `props.node` to custom React components -(`boolean`, default: `false`). - -###### `options.fixTableCellAlign` - -Fix obsolete align attributes on table cells by turning them -into inline styles (`boolean`, default: `true`). -Keep it on when working with markdown, turn it off when working -with markup for emails. +Turn HTML into preact, react, solid, svelte, vue, etc. + +###### Result + +This plugin registers a compiler that returns a `JSX.Element` where compilers +typically return `string`. +When using `.stringify` on `unified`, the result is such a `JSX.Element`. +When using `.process` (or `.processSync`), the result is available at +`file.result`. + +###### Frameworks + +There are differences between what JSX frameworks accept, such as whether they +accept `class` or `className`, or `background-color` or `backgroundColor`. + +For hast elements transformed by this project, this is be handled through +options: + +| Framework | `elementAttributeNameCase` | `stylePropertyNameCase` | +| --------- | -------------------------- | ----------------------- | +| Preact | `'html'` | `'dom'` | +| React | `'react'` | `'dom'` | +| Solid | `'html'` | `'css'` | +| Vue | `'html'` | `'dom'` | + +###### Parameters + +* `options` ([`Options`][api-options], required) + — configuration + +###### Returns + +Nothing (`undefined`). + +### `Components` + +Possible components to use (TypeScript type). + +See [`Components` from +`hast-util-to-jsx-runtime`](https://github.com/syntax-tree/hast-util-to-jsx-runtime#components) +for more info. + +### `Options` + +Configuration (TypeScript type). + +###### Fields + +* `Fragment` ([`Fragment` from + `hast-util-to-jsx-runtime`](https://github.com/syntax-tree/hast-util-to-jsx-runtime#fragment), + required) + — fragment +* `jsx` ([`Jsx` from + `hast-util-to-jsx-runtime`](https://github.com/syntax-tree/hast-util-to-jsx-runtime#jsx), + required in production) + — dynamic JSX +* `jsxs` ([`Jsx` from + `hast-util-to-jsx-runtime`](https://github.com/syntax-tree/hast-util-to-jsx-runtime#jsx), + required in production) + — static JSX +* `jsxDEV` ([`JsxDev` from + `hast-util-to-jsx-runtime`](https://github.com/syntax-tree/hast-util-to-jsx-runtime#jsxdev), + required in development) + — development JSX +* `components` ([`Partial`][api-components], optional) + — components to use +* `development` (`boolean`, default: `false`) + — whether to use `jsxDEV` when on or `jsx` and `jsxs` when off +* `elementAttributeNameCase` (`'html'` or `'react'`, default: `'react'`) + — specify casing to use for attribute names +* `passNode` (`boolean`, default: `false`) + — pass the hast element node to components +* `space` (`'html'` or `'svg'`, default: `'html'`) + — whether `tree` is in the `'html'` or `'svg'` space, when an `` + element is found in the HTML space, this package already automatically + switches to and from the SVG space when entering and exiting it +* `stylePropertyNameCase` + (`'css'` or `'dom'`, default: `'dom'`) + — specify casing to use for property names in `style` objects +* `tableCellAlignToStyle` + (`boolean`, default: `true`) + — turn obsolete `align` props on `td` and `th` into CSS `style` props ## Types This package is fully typed with [TypeScript][]. -It exports an `Options` type, which specifies the interface of the accepted -options. +It exports the additional types [`Components`][api-components] and +[`Options`][api-options]. +More advanced types are exposed from +[`hast-util-to-jsx-runtime`][hast-util-to-jsx-runtime]. ## Compatibility @@ -287,6 +328,8 @@ abide by its terms. [xss]: https://en.wikipedia.org/wiki/Cross-site_scripting +[hast-util-to-jsx-runtime]: https://github.com/syntax-tree/hast-util-to-jsx-runtime + [rehype-sanitize]: https://github.com/rehypejs/rehype-sanitize [react-markdown]: https://github.com/remarkjs/react-markdown @@ -294,3 +337,7 @@ abide by its terms. [react-remark]: https://github.com/remarkjs/react-remark [mdx]: https://github.com/mdx-js/mdx/ + +[api-components]: #components + +[api-options]: #options diff --git a/test.js b/test.js index 1714568..b733801 100644 --- a/test.js +++ b/test.js @@ -1,39 +1,60 @@ +/** + * @typedef {import('hast-util-to-jsx-runtime').Fragment} Fragment + * @typedef {import('hast-util-to-jsx-runtime').Jsx} Jsx + * @typedef {import('hast-util-to-jsx-runtime').JsxDev} JsxDev + */ + import assert from 'node:assert/strict' import test from 'node:test' +import {h} from 'hastscript' import React from 'react' +import * as dev from 'react/jsx-dev-runtime' +import * as prod from 'react/jsx-runtime' import server from 'react-dom/server' import {unified} from 'unified' -import {u} from 'unist-builder' -import {h} from 'hastscript' import rehypeReact from './index.js' -const options = {createElement: React.createElement} -const processor = unified().use(rehypeReact, options) +/** @type {{Fragment: Fragment, jsx: Jsx, jsxs: Jsx}} */ +// @ts-expect-error: the react types are missing. +const production = {Fragment: prod.Fragment, jsx: prod.jsx, jsxs: prod.jsxs} + +/** @type {{Fragment: Fragment, jsxDEV: JsxDev}} */ +// @ts-expect-error: the react types are missing. +const development = {Fragment: dev.Fragment, jsxDEV: dev.jsxDEV} test('React ' + React.version, async function (t) { - await t.test('should fail without `createElement`', async function () { - assert.throws(function () { - // @ts-expect-error: options missing. - unified() - .use(rehypeReact) - .stringify(u('root', [h('p')])) - }, /^TypeError: createElement is not a function$/) - }) + await t.test( + 'should fail without `Fragment`, `jsx`, `jsxs`', + async function () { + assert.throws(function () { + // @ts-expect-error: options missing. + unified() + .use(rehypeReact) + .stringify(h(undefined, [h('p')])) + }, /Expected `Fragment` in options/) + } + ) await t.test('should transform a root', async function () { assert.deepEqual( - processor.stringify(u('root', [h('p')])), - React.createElement('div', {}, [ - React.createElement('p', {key: 'h-1'}, undefined) - ]) + unified() + .use(rehypeReact, production) + .stringify(h(undefined, [h('p')])), + React.createElement( + React.Fragment, + {}, + React.createElement('p', {key: 'p-0'}) + ) ) }) await t.test('should transform an element', async function () { assert.deepEqual( - // To do: this should error, because it’s not a root. - processor.stringify(h('p')), - React.createElement('p', {key: 'h-1'}, undefined) + unified() + .use(rehypeReact, production) + // @ts-expect-error typed to only support roots. + .stringify(h('p')), + React.createElement('p') ) }) @@ -41,16 +62,18 @@ test('React ' + React.version, async function (t) { 'should transform an element with properties', async function () { assert.deepEqual( - processor.stringify( - u('root', [h('h1.main-heading', {dataFoo: 'bar'})]) - ), - React.createElement('div', {}, [ - React.createElement( - 'h1', - {className: 'main-heading', 'data-foo': 'bar', key: 'h-1'}, - undefined - ) - ]) + unified() + .use(rehypeReact, production) + .stringify(h(undefined, [h('h1.main-heading', {dataFoo: 'bar'})])), + React.createElement( + React.Fragment, + {}, + React.createElement('h1', { + className: 'main-heading', + 'data-foo': 'bar', + key: 'h1-0' + }) + ) ) } ) @@ -59,10 +82,14 @@ test('React ' + React.version, async function (t) { 'should transform an element with a text node', async function () { assert.deepEqual( - processor.stringify(u('root', [h('p', 'baz')])), - React.createElement('div', {}, [ - React.createElement('p', {key: 'h-1'}, ['baz']) - ]) + unified() + .use(rehypeReact, production) + .stringify(h(undefined, [h('p', 'baz')])), + React.createElement( + React.Fragment, + {}, + React.createElement('p', {key: 'p-0'}, 'baz') + ) ) } ) @@ -71,12 +98,18 @@ test('React ' + React.version, async function (t) { 'should transform an element with a child element', async function () { assert.deepEqual( - processor.stringify(u('root', [h('p', h('strong', 'qux'))])), - React.createElement('div', {}, [ - React.createElement('p', {key: 'h-1'}, [ - React.createElement('strong', {key: 'h-2'}, ['qux']) - ]) - ]) + unified() + .use(rehypeReact, production) + .stringify(h(undefined, [h('p', h('strong', 'qux'))])), + React.createElement( + React.Fragment, + {}, + React.createElement( + 'p', + {key: 'p-0'}, + React.createElement('strong', {key: 'strong-0'}, 'qux') + ) + ) ) } ) @@ -85,100 +118,91 @@ test('React ' + React.version, async function (t) { 'should transform an element with mixed contents', async function () { assert.deepEqual( - processor.stringify( - u('root', [h('p', [h('em', 'qux'), ' foo ', h('i', 'bar')])]) - ), - React.createElement('div', {}, [ - React.createElement('p', {key: 'h-1'}, [ - React.createElement('em', {key: 'h-2'}, ['qux']), + unified() + .use(rehypeReact, production) + .stringify( + h(undefined, [h('p', [h('em', 'qux'), ' foo ', h('i', 'bar')])]) + ), + React.createElement( + React.Fragment, + {}, + React.createElement('p', {key: 'p-0'}, [ + React.createElement('em', {key: 'em-0'}, 'qux'), ' foo ', - React.createElement('i', {key: 'h-3'}, ['bar']) + React.createElement('i', {key: 'i-0'}, 'bar') ]) - ]) - ) - } - ) - - await t.test( - 'should transform `root` to a `div` by default', - async function () { - assert.deepEqual( - processor.stringify(u('root', [h('p')])), - React.createElement('div', {}, [ - React.createElement('p', {key: 'h-1'}, undefined) - ]) - ) - } - ) - - await t.test( - 'should transform `root` to a `Fragment` if given', - async function () { - assert.deepEqual( - unified() - .use(rehypeReact, { - createElement: React.createElement, - Fragment: React.Fragment - }) - .stringify(u('root', [h('h1'), h('p')])), - React.createElement(React.Fragment, {}, [ - React.createElement('h1', {key: 'h-2'}, undefined), - React.createElement('p', {key: 'h-3'}, undefined) - ]) + ) ) } ) await t.test('should skip `doctype`s', async function () { assert.deepEqual( - processor.stringify(u('root', [u('doctype', {name: 'html'})])), - React.createElement('div', {}, undefined) + unified() + .use(rehypeReact, production) + .stringify(h(undefined, [{type: 'doctype'}])), + React.createElement(React.Fragment, {}) ) }) await t.test('should transform trees', async function () { assert.deepEqual( - processor.stringify( - u('root', [ - h('section', h('h1.main-heading', {dataFoo: 'bar'}, h('span', 'baz'))) - ]) - ), - React.createElement('div', {}, [ - React.createElement('section', {key: 'h-1'}, [ + unified() + .use(rehypeReact, production) + .stringify( + h(undefined, [ + h('section', [ + h('h1.main-heading', {dataFoo: 'bar'}, [h('span', 'baz')]) + ]) + ]) + ), + React.createElement( + React.Fragment, + {}, + React.createElement( + 'section', + {key: 'section-0'}, React.createElement( 'h1', { - key: 'h-2', + key: 'h1-0', className: 'main-heading', 'data-foo': 'bar' }, - [React.createElement('span', {key: 'h-3'}, ['baz'])] + React.createElement('span', {key: 'span-0'}, 'baz') ) - ]) - ]) + ) + ) ) }) await t.test('should support components', async function () { assert.deepEqual( server.renderToStaticMarkup( - // @ts-expect-error: to do: figure out. unified() .use(rehypeReact, { - createElement: React.createElement, + ...production, components: { - /** @param {object} props */ h1(props) { return React.createElement('h2', props) } } }) - .stringify(u('root', [h('h1')])) + .stringify(h(undefined, [h('h1')])) ), - server.renderToStaticMarkup( - React.createElement('div', {}, [ - React.createElement('h2', {key: 'h-1'}, undefined) - ]) + '

' + ) + }) + + await t.test('should support `development: true`', async function () { + assert.deepEqual( + unified() + .use(rehypeReact, {...development, development: true}) + .stringify(h(undefined, [h('h1')])), + React.createElement( + React.Fragment, + {}, + React.createElement('h1', {key: 'h1-0'}) ) ) }) @@ -187,112 +211,111 @@ test('React ' + React.version, async function (t) { 'should transform an element with align property', async function () { assert.deepEqual( - processor.stringify( - u('root', [h('table', {}, [h('thead', h('th', {align: 'right'}))])]) - ), - React.createElement('div', {}, [ - React.createElement('table', {key: 'h-1'}, [ - React.createElement('thead', {key: 'h-2'}, [ - React.createElement( - 'th', - {style: {textAlign: 'right'}, key: 'h-3'}, - undefined - ) + unified() + .use(rehypeReact, production) + .stringify( + h(undefined, [ + h('table', {}, [h('thead', h('th', {align: 'right'}))]) ]) - ]) - ]) + ), + React.createElement( + React.Fragment, + {}, + React.createElement( + 'table', + {key: 'table-0'}, + React.createElement( + 'thead', + {key: 'thead-0'}, + React.createElement('th', { + style: {textAlign: 'right'}, + key: 'th-0' + }) + ) + ) + ) ) } ) await t.test('should transform a table with whitespace', async function () { assert.deepEqual( - processor.stringify( - u('root', [ - h('table', {}, [ - '\n ', - h('tbody', {}, [ + unified() + .use(rehypeReact, production) + .stringify( + h(undefined, [ + h('table', {}, [ '\n ', - h('tr', {}, [ + h('tbody', {}, [ '\n ', - h('th', {}, ['\n ']), - h('td', {}, ['\n ']) + h('tr', {}, [ + '\n ', + h('th', {}, ['\n ']), + h('td', {}, ['\n ']) + ]) ]) ]) ]) - ]) - ), - React.createElement('div', {}, [ - React.createElement('table', {key: 'h-1'}, [ - React.createElement('tbody', {key: 'h-2'}, [ - React.createElement('tr', {key: 'h-3'}, [ - React.createElement('th', {key: 'h-4'}, ['\n ']), - React.createElement('td', {key: 'h-5'}, ['\n ']) + ), + React.createElement( + React.Fragment, + {}, + React.createElement( + 'table', + {key: 'table-0'}, + React.createElement( + 'tbody', + {key: 'tbody-0'}, + React.createElement('tr', {key: 'tr-0'}, [ + React.createElement('th', {key: 'th-0'}, '\n '), + React.createElement('td', {key: 'td-0'}, '\n ') ]) - ]) - ]) - ]) + ) + ) + ) ) }) await t.test('should expose node from node prop', async function () { const headingNode = h('h1') - /** @param {object} props */ - const Heading1 = function (props) { - return React.createElement('h1', props) + + const Component = function () { + return 'x' } assert.deepEqual( unified() .use(rehypeReact, { - createElement: React.createElement, - passNode: true, - components: {h1: Heading1} + ...production, + components: {h1: Component}, + passNode: true }) - .stringify(u('root', [headingNode, h('p')])), - React.createElement('div', {}, [ - React.createElement( - Heading1, - // @ts-expect-error: yeah it’s not okay per react types, but it works fine. - {key: 'h-2', node: headingNode}, - undefined - ), - React.createElement('p', {key: 'h-3'}, undefined) + .stringify(h(undefined, [headingNode, h('p')])), + React.createElement(React.Fragment, {}, [ + React.createElement(Component, {key: 'h1-0', node: headingNode}), + React.createElement('p', {key: 'p-0'}) ]) ) }) - await t.test('should respect `fixTableCellAlign` option', async function () { - assert.deepEqual( - unified() - .use(rehypeReact, { - createElement: React.createElement, - fixTableCellAlign: false - }) - .stringify( - u('root', [ - h('table', {align: 'top'}, [ - '\n ', - h('tbody', {}, [ - '\n ', - h('tr', {}, [ - h('th', {}, ['\n ']), - h('td', {align: 'center'}, ['\n ']) - ]) - ]) - ]) - ]) - ), - React.createElement('div', {}, [ - React.createElement('table', {key: 'h-1', align: 'top'}, [ - React.createElement('tbody', {key: 'h-2'}, [ - React.createElement('tr', {key: 'h-3'}, [ - React.createElement('th', {key: 'h-4'}, ['\n ']), - React.createElement('td', {key: 'h-5', align: 'center'}, ['\n ']) - ]) + await t.test( + 'should respect `tableCellAlignToStyle: false`', + async function () { + assert.deepEqual( + unified() + .use(rehypeReact, {...production, tableCellAlignToStyle: false}) + .stringify( + h(undefined, [h('tr', {}, [h('th'), h('td', {align: 'center'})])]) + ), + React.createElement( + React.Fragment, + {}, + React.createElement('tr', {key: 'tr-0'}, [ + React.createElement('th', {key: 'th-0'}), + React.createElement('td', {key: 'td-0', align: 'center'}) ]) - ]) - ]) - ) - }) + ) + ) + } + ) }) diff --git a/tsconfig.json b/tsconfig.json index a753d55..ad1496e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,5 +11,5 @@ "target": "es2020" }, "exclude": ["coverage/", "node_modules/"], - "include": ["**/*.js", "lib/complex-types.d.ts", "index.d.ts"] + "include": ["**/*.js", "index.d.ts"] }