| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787 |
- /**
- * @import {Identifier, Literal, MemberExpression} from 'estree'
- * @import {Jsx, JsxDev, Options, Props} from 'hast-util-to-jsx-runtime'
- * @import {Element, Nodes, Parents, Root, Text} from 'hast'
- * @import {MdxFlowExpressionHast, MdxTextExpressionHast} from 'mdast-util-mdx-expression'
- * @import {MdxJsxFlowElementHast, MdxJsxTextElementHast} from 'mdast-util-mdx-jsx'
- * @import {MdxjsEsmHast} from 'mdast-util-mdxjs-esm'
- * @import {Position} from 'unist'
- * @import {Child, Create, Field, JsxElement, State, Style} from './types.js'
- */
- import {stringify as commas} from 'comma-separated-tokens'
- import {ok as assert} from 'devlop'
- import {name as isIdentifierName} from 'estree-util-is-identifier-name'
- import {whitespace} from 'hast-util-whitespace'
- import {find, hastToReact, html, svg} from 'property-information'
- import {stringify as spaces} from 'space-separated-tokens'
- import styleToJs from 'style-to-js'
- import {pointStart} from 'unist-util-position'
- import {VFileMessage} from 'vfile-message'
- // To do: next major: `Object.hasOwn`.
- const own = {}.hasOwnProperty
- /** @type {Map<string, number>} */
- const emptyMap = new Map()
- const cap = /[A-Z]/g
- // `react-dom` triggers a warning for *any* white space in tables.
- // To follow GFM, `mdast-util-to-hast` injects line endings between elements.
- // Other tools might do so too, but they don’t do here, so we remove all of
- // that.
- // 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>.
- const tableElements = new Set(['table', 'tbody', 'thead', 'tfoot', 'tr'])
- const tableCellElement = new Set(['td', 'th'])
- const docs = 'https://github.com/syntax-tree/hast-util-to-jsx-runtime'
- /**
- * Transform a hast tree to preact, react, solid, svelte, vue, etc.,
- * with an automatic JSX runtime.
- *
- * @param {Nodes} tree
- * Tree to transform.
- * @param {Options} options
- * Configuration (required).
- * @returns {JsxElement}
- * JSX element.
- */
- export function toJsxRuntime(tree, options) {
- if (!options || options.Fragment === undefined) {
- throw new TypeError('Expected `Fragment` in options')
- }
- const filePath = options.filePath || undefined
- /** @type {Create} */
- let create
- if (options.development) {
- if (typeof options.jsxDEV !== 'function') {
- throw new TypeError(
- 'Expected `jsxDEV` in options when `development: true`'
- )
- }
- create = developmentCreate(filePath, options.jsxDEV)
- } else {
- if (typeof options.jsx !== 'function') {
- throw new TypeError('Expected `jsx` in production options')
- }
- if (typeof options.jsxs !== 'function') {
- throw new TypeError('Expected `jsxs` in production options')
- }
- create = productionCreate(filePath, options.jsx, options.jsxs)
- }
- /** @type {State} */
- const state = {
- Fragment: options.Fragment,
- ancestors: [],
- components: options.components || {},
- create,
- elementAttributeNameCase: options.elementAttributeNameCase || 'react',
- evaluater: options.createEvaluater ? options.createEvaluater() : undefined,
- filePath,
- ignoreInvalidStyle: options.ignoreInvalidStyle || false,
- passKeys: options.passKeys !== false,
- passNode: options.passNode || false,
- schema: options.space === 'svg' ? svg : html,
- stylePropertyNameCase: options.stylePropertyNameCase || 'dom',
- tableCellAlignToStyle: options.tableCellAlignToStyle !== false
- }
- const result = one(state, tree, undefined)
- // JSX element.
- if (result && typeof result !== 'string') {
- return result
- }
- // Text node or something that turned into nothing.
- return state.create(
- tree,
- state.Fragment,
- {children: result || undefined},
- undefined
- )
- }
- /**
- * Transform a node.
- *
- * @param {State} state
- * Info passed around.
- * @param {Nodes} node
- * Current node.
- * @param {string | undefined} key
- * Key.
- * @returns {Child | undefined}
- * Child, optional.
- */
- function one(state, node, key) {
- if (node.type === 'element') {
- return element(state, node, key)
- }
- if (node.type === 'mdxFlowExpression' || node.type === 'mdxTextExpression') {
- return mdxExpression(state, node)
- }
- if (node.type === 'mdxJsxFlowElement' || node.type === 'mdxJsxTextElement') {
- return mdxJsxElement(state, node, key)
- }
- if (node.type === 'mdxjsEsm') {
- return mdxEsm(state, node)
- }
- if (node.type === 'root') {
- return root(state, node, key)
- }
- if (node.type === 'text') {
- return text(state, node)
- }
- }
- /**
- * Handle element.
- *
- * @param {State} state
- * Info passed around.
- * @param {Element} node
- * Current node.
- * @param {string | undefined} key
- * Key.
- * @returns {Child | undefined}
- * Child, optional.
- */
- function element(state, node, key) {
- const parentSchema = state.schema
- let schema = parentSchema
- if (node.tagName.toLowerCase() === 'svg' && parentSchema.space === 'html') {
- schema = svg
- state.schema = schema
- }
- state.ancestors.push(node)
- const type = findComponentFromName(state, node.tagName, false)
- const props = createElementProps(state, node)
- let children = createChildren(state, node)
- if (tableElements.has(node.tagName)) {
- children = children.filter(function (child) {
- return typeof child === 'string' ? !whitespace(child) : true
- })
- }
- addNode(state, props, type, node)
- addChildren(props, children)
- // Restore.
- state.ancestors.pop()
- state.schema = parentSchema
- return state.create(node, type, props, key)
- }
- /**
- * Handle MDX expression.
- *
- * @param {State} state
- * Info passed around.
- * @param {MdxFlowExpressionHast | MdxTextExpressionHast} node
- * Current node.
- * @returns {Child | undefined}
- * Child, optional.
- */
- function mdxExpression(state, node) {
- if (node.data && node.data.estree && state.evaluater) {
- const program = node.data.estree
- const expression = program.body[0]
- assert(expression.type === 'ExpressionStatement')
- // Assume result is a child.
- return /** @type {Child | undefined} */ (
- state.evaluater.evaluateExpression(expression.expression)
- )
- }
- crashEstree(state, node.position)
- }
- /**
- * Handle MDX ESM.
- *
- * @param {State} state
- * Info passed around.
- * @param {MdxjsEsmHast} node
- * Current node.
- * @returns {Child | undefined}
- * Child, optional.
- */
- function mdxEsm(state, node) {
- if (node.data && node.data.estree && state.evaluater) {
- // Assume result is a child.
- return /** @type {Child | undefined} */ (
- state.evaluater.evaluateProgram(node.data.estree)
- )
- }
- crashEstree(state, node.position)
- }
- /**
- * Handle MDX JSX.
- *
- * @param {State} state
- * Info passed around.
- * @param {MdxJsxFlowElementHast | MdxJsxTextElementHast} node
- * Current node.
- * @param {string | undefined} key
- * Key.
- * @returns {Child | undefined}
- * Child, optional.
- */
- function mdxJsxElement(state, node, key) {
- const parentSchema = state.schema
- let schema = parentSchema
- if (node.name === 'svg' && parentSchema.space === 'html') {
- schema = svg
- state.schema = schema
- }
- state.ancestors.push(node)
- const type =
- node.name === null
- ? state.Fragment
- : findComponentFromName(state, node.name, true)
- const props = createJsxElementProps(state, node)
- const children = createChildren(state, node)
- addNode(state, props, type, node)
- addChildren(props, children)
- // Restore.
- state.ancestors.pop()
- state.schema = parentSchema
- return state.create(node, type, props, key)
- }
- /**
- * Handle root.
- *
- * @param {State} state
- * Info passed around.
- * @param {Root} node
- * Current node.
- * @param {string | undefined} key
- * Key.
- * @returns {Child | undefined}
- * Child, optional.
- */
- function root(state, node, key) {
- /** @type {Props} */
- const props = {}
- addChildren(props, createChildren(state, node))
- return state.create(node, state.Fragment, props, key)
- }
- /**
- * Handle text.
- *
- * @param {State} _
- * Info passed around.
- * @param {Text} node
- * Current node.
- * @returns {Child | undefined}
- * Child, optional.
- */
- function text(_, node) {
- return node.value
- }
- /**
- * Add `node` to props.
- *
- * @param {State} state
- * Info passed around.
- * @param {Props} props
- * Props.
- * @param {unknown} type
- * Type.
- * @param {Element | MdxJsxFlowElementHast | MdxJsxTextElementHast} node
- * Node.
- * @returns {undefined}
- * Nothing.
- */
- function addNode(state, props, type, node) {
- // If this is swapped out for a component:
- if (typeof type !== 'string' && type !== state.Fragment && state.passNode) {
- props.node = node
- }
- }
- /**
- * Add children to props.
- *
- * @param {Props} props
- * Props.
- * @param {Array<Child>} children
- * Children.
- * @returns {undefined}
- * Nothing.
- */
- function addChildren(props, children) {
- if (children.length > 0) {
- const value = children.length > 1 ? children : children[0]
- if (value) {
- props.children = value
- }
- }
- }
- /**
- * @param {string | undefined} _
- * Path to file.
- * @param {Jsx} jsx
- * Dynamic.
- * @param {Jsx} jsxs
- * Static.
- * @returns {Create}
- * Create a production element.
- */
- function productionCreate(_, jsx, jsxs) {
- return create
- /** @type {Create} */
- function create(_, type, props, key) {
- // Only an array when there are 2 or more children.
- const isStaticChildren = Array.isArray(props.children)
- const fn = isStaticChildren ? jsxs : jsx
- return key ? fn(type, props, key) : fn(type, props)
- }
- }
- /**
- * @param {string | undefined} filePath
- * Path to file.
- * @param {JsxDev} jsxDEV
- * Development.
- * @returns {Create}
- * Create a development element.
- */
- function developmentCreate(filePath, jsxDEV) {
- return create
- /** @type {Create} */
- function create(node, type, props, key) {
- // Only an array when there are 2 or more children.
- const isStaticChildren = Array.isArray(props.children)
- const point = pointStart(node)
- return jsxDEV(
- type,
- props,
- key,
- isStaticChildren,
- {
- columnNumber: point ? point.column - 1 : undefined,
- fileName: filePath,
- lineNumber: point ? point.line : undefined
- },
- undefined
- )
- }
- }
- /**
- * Create props from an element.
- *
- * @param {State} state
- * Info passed around.
- * @param {Element} node
- * Current element.
- * @returns {Props}
- * Props.
- */
- function createElementProps(state, node) {
- /** @type {Props} */
- const props = {}
- /** @type {string | undefined} */
- let alignValue
- /** @type {string} */
- let prop
- for (prop in node.properties) {
- if (prop !== 'children' && own.call(node.properties, prop)) {
- const result = createProperty(state, prop, node.properties[prop])
- if (result) {
- const [key, value] = result
- if (
- state.tableCellAlignToStyle &&
- key === 'align' &&
- typeof value === 'string' &&
- tableCellElement.has(node.tagName)
- ) {
- alignValue = value
- } else {
- props[key] = value
- }
- }
- }
- }
- if (alignValue) {
- // Assume style is an object.
- const style = /** @type {Style} */ (props.style || (props.style = {}))
- style[state.stylePropertyNameCase === 'css' ? 'text-align' : 'textAlign'] =
- alignValue
- }
- return props
- }
- /**
- * Create props from a JSX element.
- *
- * @param {State} state
- * Info passed around.
- * @param {MdxJsxFlowElementHast | MdxJsxTextElementHast} node
- * Current JSX element.
- * @returns {Props}
- * Props.
- */
- function createJsxElementProps(state, node) {
- /** @type {Props} */
- const props = {}
- for (const attribute of node.attributes) {
- if (attribute.type === 'mdxJsxExpressionAttribute') {
- if (attribute.data && attribute.data.estree && state.evaluater) {
- const program = attribute.data.estree
- const expression = program.body[0]
- assert(expression.type === 'ExpressionStatement')
- const objectExpression = expression.expression
- assert(objectExpression.type === 'ObjectExpression')
- const property = objectExpression.properties[0]
- assert(property.type === 'SpreadElement')
- Object.assign(
- props,
- state.evaluater.evaluateExpression(property.argument)
- )
- } else {
- crashEstree(state, node.position)
- }
- } else {
- // For JSX, the author is responsible of passing in the correct values.
- const name = attribute.name
- /** @type {unknown} */
- let value
- if (attribute.value && typeof attribute.value === 'object') {
- if (
- attribute.value.data &&
- attribute.value.data.estree &&
- state.evaluater
- ) {
- const program = attribute.value.data.estree
- const expression = program.body[0]
- assert(expression.type === 'ExpressionStatement')
- value = state.evaluater.evaluateExpression(expression.expression)
- } else {
- crashEstree(state, node.position)
- }
- } else {
- value = attribute.value === null ? true : attribute.value
- }
- // Assume a prop.
- props[name] = /** @type {Props[keyof Props]} */ (value)
- }
- }
- return props
- }
- /**
- * Create children.
- *
- * @param {State} state
- * Info passed around.
- * @param {Parents} node
- * Current element.
- * @returns {Array<Child>}
- * Children.
- */
- function createChildren(state, node) {
- /** @type {Array<Child>} */
- const children = []
- let index = -1
- /** @type {Map<string, number>} */
- // Note: test this when Solid doesn’t want to merge my upcoming PR.
- /* c8 ignore next */
- const countsByName = state.passKeys ? new Map() : emptyMap
- while (++index < node.children.length) {
- const child = node.children[index]
- /** @type {string | undefined} */
- let key
- if (state.passKeys) {
- const name =
- child.type === 'element'
- ? child.tagName
- : child.type === 'mdxJsxFlowElement' ||
- child.type === 'mdxJsxTextElement'
- ? child.name
- : undefined
- if (name) {
- const count = countsByName.get(name) || 0
- key = name + '-' + count
- countsByName.set(name, count + 1)
- }
- }
- const result = one(state, child, key)
- if (result !== undefined) children.push(result)
- }
- return children
- }
- /**
- * Handle a property.
- *
- * @param {State} state
- * Info passed around.
- * @param {string} prop
- * Key.
- * @param {Array<number | string> | boolean | number | string | null | undefined} value
- * hast property value.
- * @returns {Field | undefined}
- * Field for runtime, optional.
- */
- function createProperty(state, prop, value) {
- const info = find(state.schema, prop)
- // Ignore nullish and `NaN` values.
- if (
- value === null ||
- value === undefined ||
- (typeof value === 'number' && Number.isNaN(value))
- ) {
- return
- }
- if (Array.isArray(value)) {
- // Accept `array`.
- // Most props are space-separated.
- value = info.commaSeparated ? commas(value) : spaces(value)
- }
- // React only accepts `style` as object.
- if (info.property === 'style') {
- let styleObject =
- typeof value === 'object' ? value : parseStyle(state, String(value))
- if (state.stylePropertyNameCase === 'css') {
- styleObject = transformStylesToCssCasing(styleObject)
- }
- return ['style', styleObject]
- }
- return [
- state.elementAttributeNameCase === 'react' && info.space
- ? hastToReact[info.property] || info.property
- : info.attribute,
- value
- ]
- }
- /**
- * Parse a CSS declaration to an object.
- *
- * @param {State} state
- * Info passed around.
- * @param {string} value
- * CSS declarations.
- * @returns {Style}
- * Properties.
- * @throws
- * Throws `VFileMessage` when CSS cannot be parsed.
- */
- function parseStyle(state, value) {
- try {
- return styleToJs(value, {reactCompat: true})
- } catch (error) {
- if (state.ignoreInvalidStyle) {
- return {}
- }
- const cause = /** @type {Error} */ (error)
- const message = new VFileMessage('Cannot parse `style` attribute', {
- ancestors: state.ancestors,
- cause,
- ruleId: 'style',
- source: 'hast-util-to-jsx-runtime'
- })
- message.file = state.filePath || undefined
- message.url = docs + '#cannot-parse-style-attribute'
- throw message
- }
- }
- /**
- * Create a JSX name from a string.
- *
- * @param {State} state
- * To do.
- * @param {string} name
- * Name.
- * @param {boolean} allowExpression
- * Allow member expressions and identifiers.
- * @returns {unknown}
- * To do.
- */
- function findComponentFromName(state, name, allowExpression) {
- /** @type {Identifier | Literal | MemberExpression} */
- let result
- if (!allowExpression) {
- result = {type: 'Literal', value: name}
- } else if (name.includes('.')) {
- const identifiers = name.split('.')
- let index = -1
- /** @type {Identifier | Literal | MemberExpression | undefined} */
- let node
- while (++index < identifiers.length) {
- /** @type {Identifier | Literal} */
- const prop = isIdentifierName(identifiers[index])
- ? {type: 'Identifier', name: identifiers[index]}
- : {type: 'Literal', value: identifiers[index]}
- node = node
- ? {
- type: 'MemberExpression',
- object: node,
- property: prop,
- computed: Boolean(index && prop.type === 'Literal'),
- optional: false
- }
- : prop
- }
- assert(node, 'always a result')
- result = node
- } else {
- result =
- isIdentifierName(name) && !/^[a-z]/.test(name)
- ? {type: 'Identifier', name}
- : {type: 'Literal', value: name}
- }
- // Only literals can be passed in `components` currently.
- // No identifiers / member expressions.
- if (result.type === 'Literal') {
- const name = /** @type {string | number} */ (result.value)
- return own.call(state.components, name) ? state.components[name] : name
- }
- // Assume component.
- if (state.evaluater) {
- return state.evaluater.evaluateExpression(result)
- }
- crashEstree(state)
- }
- /**
- * @param {State} state
- * @param {Position | undefined} [place]
- * @returns {never}
- */
- function crashEstree(state, place) {
- const message = new VFileMessage(
- 'Cannot handle MDX estrees without `createEvaluater`',
- {
- ancestors: state.ancestors,
- place,
- ruleId: 'mdx-estree',
- source: 'hast-util-to-jsx-runtime'
- }
- )
- message.file = state.filePath || undefined
- message.url = docs + '#cannot-handle-mdx-estrees-without-createevaluater'
- throw message
- }
- /**
- * Transform a DOM casing style object to a CSS casing style object.
- *
- * @param {Style} domCasing
- * @returns {Style}
- */
- function transformStylesToCssCasing(domCasing) {
- /** @type {Style} */
- const cssCasing = {}
- /** @type {string} */
- let from
- for (from in domCasing) {
- if (own.call(domCasing, from)) {
- cssCasing[transformStyleToCssCasing(from)] = domCasing[from]
- }
- }
- return cssCasing
- }
- /**
- * Transform a DOM casing style field to a CSS casing style field.
- *
- * @param {string} from
- * @returns {string}
- */
- function transformStyleToCssCasing(from) {
- let to = from.replace(cap, toDash)
- // Handle `ms-xxx` -> `-ms-xxx`.
- if (to.slice(0, 3) === 'ms-') to = '-' + to
- return to
- }
- /**
- * Make `$0` dash cased.
- *
- * @param {string} $0
- * Capitalized ASCII leter.
- * @returns {string}
- * Dash and lower letter.
- */
- function toDash($0) {
- return '-' + $0.toLowerCase()
- }
|