Skip to content

Commit

Permalink
Change to support preact, solid, svelte, vue, too
Browse files Browse the repository at this point in the history
This switches from `Fragment`/`createElement` to a JSX runtime (such as
from `'react/jsx-runtime'`), to support many more frameworks:

```diff
- import {Fragment, createElement} from 'react'
+ import * as production from 'react/jsx-runtime'

-   .use(rehypeReact, {Fragment, createElement})
+   .use(rehypeReact, production)
```
  • Loading branch information
wooorm committed Sep 1, 2023
1 parent c4577ec commit 579589b
Show file tree
Hide file tree
Showing 9 changed files with 393 additions and 432 deletions.
1 change: 0 additions & 1 deletion .gitignore
Expand Up @@ -4,5 +4,4 @@ node_modules/
*.d.ts
*.log
yarn.lock
!/lib/complex-types.d.ts
!/index.d.ts
43 changes: 37 additions & 6 deletions 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<unknown>>
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
}
}
1 change: 1 addition & 0 deletions index.js
@@ -1 +1,2 @@
// Note: types exposed from `index.d.ts`.
export {default} from './lib/index.js'
39 changes: 0 additions & 39 deletions lib/complex-types.d.ts

This file was deleted.

134 changes: 16 additions & 118 deletions lib/index.js
@@ -1,131 +1,29 @@
/**
* @typedef {import('hast').Root} Root
* @typedef {import('react').ReactNode} ReactNode
* @typedef {import('react').ReactElement<unknown>} 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 `<div>` 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<Root, JSX.Element>} Compiler
* @typedef {import('unified').Processor<undefined, undefined, undefined, Root, JSX.Element>} 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<Root, ReactNode>} */
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 <https://github.com/syntax-tree/hast-to-hyperscript/blob/d227372/index.js#L46-L56>.
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<string, unknown>} props
* @param {Array<ReactNode>} [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: <https://github.com/facebook/react/pull/7081>.
// See: <https://github.com/facebook/react/pull/7515>.
// See: <https://github.com/remarkjs/remark-react/issues/64>.
// See: <https://github.com/rehypejs/rehype-react/pull/29>.
// See: <https://github.com/rehypejs/rehype-react/pull/32>.
// See: <https://github.com/rehypejs/rehype-react/pull/45>.
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})
}
}
25 changes: 13 additions & 12 deletions package.json
Expand Up @@ -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": {
Expand Down Expand Up @@ -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": {
Expand Down

0 comments on commit 579589b

Please sign in to comment.