AK-21/Graphite-Industrial-Intelligence
0
1/**2 * @import {Identifier, Literal, MemberExpression} from 'estree'3 * @import {Jsx, JsxDev, Options, Props} from 'hast-util-to-jsx-runtime'4 * @import {Element, Nodes, Parents, Root, Text} from 'hast'5 * @import {MdxFlowExpressionHast, MdxTextExpressionHast} from 'mdast-util-mdx-expression'6 * @import {MdxJsxFlowElementHast, MdxJsxTextElementHast} from 'mdast-util-mdx-jsx'7 * @import {MdxjsEsmHast} from 'mdast-util-mdxjs-esm'8 * @import {Position} from 'unist'9 * @import {Child, Create, Field, JsxElement, State, Style} from './types.js'10 */11 12import {stringify as commas} from 'comma-separated-tokens'13import {ok as assert} from 'devlop'14import {name as isIdentifierName} from 'estree-util-is-identifier-name'15import {whitespace} from 'hast-util-whitespace'16import {find, hastToReact, html, svg} from 'property-information'17import {stringify as spaces} from 'space-separated-tokens'18import styleToJs from 'style-to-js'19import {pointStart} from 'unist-util-position'20import {VFileMessage} from 'vfile-message'21 22// To do: next major: `Object.hasOwn`.23const own = {}.hasOwnProperty24 25/** @type {Map<string, number>} */26const emptyMap = new Map()27 28const cap = /[A-Z]/g29 30// `react-dom` triggers a warning for *any* white space in tables.31// To follow GFM, `mdast-util-to-hast` injects line endings between elements.32// Other tools might do so too, but they don’t do here, so we remove all of33// that.34 35// See: <https://github.com/facebook/react/pull/7081>.36// See: <https://github.com/facebook/react/pull/7515>.37// See: <https://github.com/remarkjs/remark-react/issues/64>.38// See: <https://github.com/rehypejs/rehype-react/pull/29>.39// See: <https://github.com/rehypejs/rehype-react/pull/32>.40// See: <https://github.com/rehypejs/rehype-react/pull/45>.41const tableElements = new Set(['table', 'tbody', 'thead', 'tfoot', 'tr'])42 43const tableCellElement = new Set(['td', 'th'])44 45const docs = 'https://github.com/syntax-tree/hast-util-to-jsx-runtime'46 47/**48 * Transform a hast tree to preact, react, solid, svelte, vue, etc.,49 * with an automatic JSX runtime.50 *51 * @param {Nodes} tree52 * Tree to transform.53 * @param {Options} options54 * Configuration (required).55 * @returns {JsxElement}56 * JSX element.57 */58 59export function toJsxRuntime(tree, options) {60 if (!options || options.Fragment === undefined) {61 throw new TypeError('Expected `Fragment` in options')62 }63 64 const filePath = options.filePath || undefined65 /** @type {Create} */66 let create67 68 if (options.development) {69 if (typeof options.jsxDEV !== 'function') {70 throw new TypeError(71 'Expected `jsxDEV` in options when `development: true`'72 )73 }74 75 create = developmentCreate(filePath, options.jsxDEV)76 } else {77 if (typeof options.jsx !== 'function') {78 throw new TypeError('Expected `jsx` in production options')79 }80 81 if (typeof options.jsxs !== 'function') {82 throw new TypeError('Expected `jsxs` in production options')83 }84 85 create = productionCreate(filePath, options.jsx, options.jsxs)86 }87 88 /** @type {State} */89 const state = {90 Fragment: options.Fragment,91 ancestors: [],92 components: options.components || {},93 create,94 elementAttributeNameCase: options.elementAttributeNameCase || 'react',95 evaluater: options.createEvaluater ? options.createEvaluater() : undefined,96 filePath,97 ignoreInvalidStyle: options.ignoreInvalidStyle || false,98 passKeys: options.passKeys !== false,99 passNode: options.passNode || false,100 schema: options.space === 'svg' ? svg : html,101 stylePropertyNameCase: options.stylePropertyNameCase || 'dom',102 tableCellAlignToStyle: options.tableCellAlignToStyle !== false103 }104 105 const result = one(state, tree, undefined)106 107 // JSX element.108 if (result && typeof result !== 'string') {109 return result110 }111 112 // Text node or something that turned into nothing.113 return state.create(114 tree,115 state.Fragment,116 {children: result || undefined},117 undefined118 )119}120 121/**122 * Transform a node.123 *124 * @param {State} state125 * Info passed around.126 * @param {Nodes} node127 * Current node.128 * @param {string | undefined} key129 * Key.130 * @returns {Child | undefined}131 * Child, optional.132 */133function one(state, node, key) {134 if (node.type === 'element') {135 return element(state, node, key)136 }137 138 if (node.type === 'mdxFlowExpression' || node.type === 'mdxTextExpression') {139 return mdxExpression(state, node)140 }141 142 if (node.type === 'mdxJsxFlowElement' || node.type === 'mdxJsxTextElement') {143 return mdxJsxElement(state, node, key)144 }145 146 if (node.type === 'mdxjsEsm') {147 return mdxEsm(state, node)148 }149 150 if (node.type === 'root') {151 return root(state, node, key)152 }153 154 if (node.type === 'text') {155 return text(state, node)156 }157}158 159/**160 * Handle element.161 *162 * @param {State} state163 * Info passed around.164 * @param {Element} node165 * Current node.166 * @param {string | undefined} key167 * Key.168 * @returns {Child | undefined}169 * Child, optional.170 */171function element(state, node, key) {172 const parentSchema = state.schema173 let schema = parentSchema174 175 if (node.tagName.toLowerCase() === 'svg' && parentSchema.space === 'html') {176 schema = svg177 state.schema = schema178 }179 180 state.ancestors.push(node)181 182 const type = findComponentFromName(state, node.tagName, false)183 const props = createElementProps(state, node)184 let children = createChildren(state, node)185 186 if (tableElements.has(node.tagName)) {187 children = children.filter(function (child) {188 return typeof child === 'string' ? !whitespace(child) : true189 })190 }191 192 addNode(state, props, type, node)193 addChildren(props, children)194 195 // Restore.196 state.ancestors.pop()197 state.schema = parentSchema198 199 return state.create(node, type, props, key)200}201 202/**203 * Handle MDX expression.204 *205 * @param {State} state206 * Info passed around.207 * @param {MdxFlowExpressionHast | MdxTextExpressionHast} node208 * Current node.209 * @returns {Child | undefined}210 * Child, optional.211 */212function mdxExpression(state, node) {213 if (node.data && node.data.estree && state.evaluater) {214 const program = node.data.estree215 const expression = program.body[0]216 assert(expression.type === 'ExpressionStatement')217 218 // Assume result is a child.219 return /** @type {Child | undefined} */ (220 state.evaluater.evaluateExpression(expression.expression)221 )222 }223 224 crashEstree(state, node.position)225}226 227/**228 * Handle MDX ESM.229 *230 * @param {State} state231 * Info passed around.232 * @param {MdxjsEsmHast} node233 * Current node.234 * @returns {Child | undefined}235 * Child, optional.236 */237function mdxEsm(state, node) {238 if (node.data && node.data.estree && state.evaluater) {239 // Assume result is a child.240 return /** @type {Child | undefined} */ (241 state.evaluater.evaluateProgram(node.data.estree)242 )243 }244 245 crashEstree(state, node.position)246}247 248/**249 * Handle MDX JSX.250 *251 * @param {State} state252 * Info passed around.253 * @param {MdxJsxFlowElementHast | MdxJsxTextElementHast} node254 * Current node.255 * @param {string | undefined} key256 * Key.257 * @returns {Child | undefined}258 * Child, optional.259 */260function mdxJsxElement(state, node, key) {261 const parentSchema = state.schema262 let schema = parentSchema263 264 if (node.name === 'svg' && parentSchema.space === 'html') {265 schema = svg266 state.schema = schema267 }268 269 state.ancestors.push(node)270 271 const type =272 node.name === null273 ? state.Fragment274 : findComponentFromName(state, node.name, true)275 const props = createJsxElementProps(state, node)276 const children = createChildren(state, node)277 278 addNode(state, props, type, node)279 addChildren(props, children)280 281 // Restore.282 state.ancestors.pop()283 state.schema = parentSchema284 285 return state.create(node, type, props, key)286}287 288/**289 * Handle root.290 *291 * @param {State} state292 * Info passed around.293 * @param {Root} node294 * Current node.295 * @param {string | undefined} key296 * Key.297 * @returns {Child | undefined}298 * Child, optional.299 */300function root(state, node, key) {301 /** @type {Props} */302 const props = {}303 304 addChildren(props, createChildren(state, node))305 306 return state.create(node, state.Fragment, props, key)307}308 309/**310 * Handle text.311 *312 * @param {State} _313 * Info passed around.314 * @param {Text} node315 * Current node.316 * @returns {Child | undefined}317 * Child, optional.318 */319function text(_, node) {320 return node.value321}322 323/**324 * Add `node` to props.325 *326 * @param {State} state327 * Info passed around.328 * @param {Props} props329 * Props.330 * @param {unknown} type331 * Type.332 * @param {Element | MdxJsxFlowElementHast | MdxJsxTextElementHast} node333 * Node.334 * @returns {undefined}335 * Nothing.336 */337function addNode(state, props, type, node) {338 // If this is swapped out for a component:339 if (typeof type !== 'string' && type !== state.Fragment && state.passNode) {340 props.node = node341 }342}343 344/**345 * Add children to props.346 *347 * @param {Props} props348 * Props.349 * @param {Array<Child>} children350 * Children.351 * @returns {undefined}352 * Nothing.353 */354function addChildren(props, children) {355 if (children.length > 0) {356 const value = children.length > 1 ? children : children[0]357 358 if (value) {359 props.children = value360 }361 }362}363 364/**365 * @param {string | undefined} _366 * Path to file.367 * @param {Jsx} jsx368 * Dynamic.369 * @param {Jsx} jsxs370 * Static.371 * @returns {Create}372 * Create a production element.373 */374function productionCreate(_, jsx, jsxs) {375 return create376 /** @type {Create} */377 function create(_, type, props, key) {378 // Only an array when there are 2 or more children.379 const isStaticChildren = Array.isArray(props.children)380 const fn = isStaticChildren ? jsxs : jsx381 return key ? fn(type, props, key) : fn(type, props)382 }383}384 385/**386 * @param {string | undefined} filePath387 * Path to file.388 * @param {JsxDev} jsxDEV389 * Development.390 * @returns {Create}391 * Create a development element.392 */393function developmentCreate(filePath, jsxDEV) {394 return create395 /** @type {Create} */396 function create(node, type, props, key) {397 // Only an array when there are 2 or more children.398 const isStaticChildren = Array.isArray(props.children)399 const point = pointStart(node)400 return jsxDEV(401 type,402 props,403 key,404 isStaticChildren,405 {406 columnNumber: point ? point.column - 1 : undefined,407 fileName: filePath,408 lineNumber: point ? point.line : undefined409 },410 undefined411 )412 }413}414 415/**416 * Create props from an element.417 *418 * @param {State} state419 * Info passed around.420 * @param {Element} node421 * Current element.422 * @returns {Props}423 * Props.424 */425function createElementProps(state, node) {426 /** @type {Props} */427 const props = {}428 /** @type {string | undefined} */429 let alignValue430 /** @type {string} */431 let prop432 433 for (prop in node.properties) {434 if (prop !== 'children' && own.call(node.properties, prop)) {435 const result = createProperty(state, prop, node.properties[prop])436 437 if (result) {438 const [key, value] = result439 440 if (441 state.tableCellAlignToStyle &&442 key === 'align' &&443 typeof value === 'string' &&444 tableCellElement.has(node.tagName)445 ) {446 alignValue = value447 } else {448 props[key] = value449 }450 }451 }452 }453 454 if (alignValue) {455 // Assume style is an object.456 const style = /** @type {Style} */ (props.style || (props.style = {}))457 style[state.stylePropertyNameCase === 'css' ? 'text-align' : 'textAlign'] =458 alignValue459 }460 461 return props462}463 464/**465 * Create props from a JSX element.466 *467 * @param {State} state468 * Info passed around.469 * @param {MdxJsxFlowElementHast | MdxJsxTextElementHast} node470 * Current JSX element.471 * @returns {Props}472 * Props.473 */474function createJsxElementProps(state, node) {475 /** @type {Props} */476 const props = {}477 478 for (const attribute of node.attributes) {479 if (attribute.type === 'mdxJsxExpressionAttribute') {480 if (attribute.data && attribute.data.estree && state.evaluater) {481 const program = attribute.data.estree482 const expression = program.body[0]483 assert(expression.type === 'ExpressionStatement')484 const objectExpression = expression.expression485 assert(objectExpression.type === 'ObjectExpression')486 const property = objectExpression.properties[0]487 assert(property.type === 'SpreadElement')488 489 Object.assign(490 props,491 state.evaluater.evaluateExpression(property.argument)492 )493 } else {494 crashEstree(state, node.position)495 }496 } else {497 // For JSX, the author is responsible of passing in the correct values.498 const name = attribute.name499 /** @type {unknown} */500 let value501 502 if (attribute.value && typeof attribute.value === 'object') {503 if (504 attribute.value.data &&505 attribute.value.data.estree &&506 state.evaluater507 ) {508 const program = attribute.value.data.estree509 const expression = program.body[0]510 assert(expression.type === 'ExpressionStatement')511 value = state.evaluater.evaluateExpression(expression.expression)512 } else {513 crashEstree(state, node.position)514 }515 } else {516 value = attribute.value === null ? true : attribute.value517 }518 519 // Assume a prop.520 props[name] = /** @type {Props[keyof Props]} */ (value)521 }522 }523 524 return props525}526 527/**528 * Create children.529 *530 * @param {State} state531 * Info passed around.532 * @param {Parents} node533 * Current element.534 * @returns {Array<Child>}535 * Children.536 */537function createChildren(state, node) {538 /** @type {Array<Child>} */539 const children = []540 let index = -1541 /** @type {Map<string, number>} */542 // Note: test this when Solid doesn’t want to merge my upcoming PR.543 /* c8 ignore next */544 const countsByName = state.passKeys ? new Map() : emptyMap545 546 while (++index < node.children.length) {547 const child = node.children[index]548 /** @type {string | undefined} */549 let key550 551 if (state.passKeys) {552 const name =553 child.type === 'element'554 ? child.tagName555 : child.type === 'mdxJsxFlowElement' ||556 child.type === 'mdxJsxTextElement'557 ? child.name558 : undefined559 560 if (name) {561 const count = countsByName.get(name) || 0562 key = name + '-' + count563 countsByName.set(name, count + 1)564 }565 }566 567 const result = one(state, child, key)568 if (result !== undefined) children.push(result)569 }570 571 return children572}573 574/**575 * Handle a property.576 *577 * @param {State} state578 * Info passed around.579 * @param {string} prop580 * Key.581 * @param {Array<number | string> | boolean | number | string | null | undefined} value582 * hast property value.583 * @returns {Field | undefined}584 * Field for runtime, optional.585 */586function createProperty(state, prop, value) {587 const info = find(state.schema, prop)588 589 // Ignore nullish and `NaN` values.590 if (591 value === null ||592 value === undefined ||593 (typeof value === 'number' && Number.isNaN(value))594 ) {595 return596 }597 598 if (Array.isArray(value)) {599 // Accept `array`.600 // Most props are space-separated.601 value = info.commaSeparated ? commas(value) : spaces(value)602 }603 604 // React only accepts `style` as object.605 if (info.property === 'style') {606 let styleObject =607 typeof value === 'object' ? value : parseStyle(state, String(value))608 609 if (state.stylePropertyNameCase === 'css') {610 styleObject = transformStylesToCssCasing(styleObject)611 }612 613 return ['style', styleObject]614 }615 616 return [617 state.elementAttributeNameCase === 'react' && info.space618 ? hastToReact[info.property] || info.property619 : info.attribute,620 value621 ]622}623 624/**625 * Parse a CSS declaration to an object.626 *627 * @param {State} state628 * Info passed around.629 * @param {string} value630 * CSS declarations.631 * @returns {Style}632 * Properties.633 * @throws634 * Throws `VFileMessage` when CSS cannot be parsed.635 */636function parseStyle(state, value) {637 try {638 return styleToJs(value, {reactCompat: true})639 } catch (error) {640 if (state.ignoreInvalidStyle) {641 return {}642 }643 644 const cause = /** @type {Error} */ (error)645 const message = new VFileMessage('Cannot parse `style` attribute', {646 ancestors: state.ancestors,647 cause,648 ruleId: 'style',649 source: 'hast-util-to-jsx-runtime'650 })651 message.file = state.filePath || undefined652 message.url = docs + '#cannot-parse-style-attribute'653 654 throw message655 }656}657 658/**659 * Create a JSX name from a string.660 *661 * @param {State} state662 * To do.663 * @param {string} name664 * Name.665 * @param {boolean} allowExpression666 * Allow member expressions and identifiers.667 * @returns {unknown}668 * To do.669 */670function findComponentFromName(state, name, allowExpression) {671 /** @type {Identifier | Literal | MemberExpression} */672 let result673 674 if (!allowExpression) {675 result = {type: 'Literal', value: name}676 } else if (name.includes('.')) {677 const identifiers = name.split('.')678 let index = -1679 /** @type {Identifier | Literal | MemberExpression | undefined} */680 let node681 682 while (++index < identifiers.length) {683 /** @type {Identifier | Literal} */684 const prop = isIdentifierName(identifiers[index])685 ? {type: 'Identifier', name: identifiers[index]}686 : {type: 'Literal', value: identifiers[index]}687 node = node688 ? {689 type: 'MemberExpression',690 object: node,691 property: prop,692 computed: Boolean(index && prop.type === 'Literal'),693 optional: false694 }695 : prop696 }697 698 assert(node, 'always a result')699 result = node700 } else {701 result =702 isIdentifierName(name) && !/^[a-z]/.test(name)703 ? {type: 'Identifier', name}704 : {type: 'Literal', value: name}705 }706 707 // Only literals can be passed in `components` currently.708 // No identifiers / member expressions.709 if (result.type === 'Literal') {710 const name = /** @type {string | number} */ (result.value)711 return own.call(state.components, name) ? state.components[name] : name712 }713 714 // Assume component.715 if (state.evaluater) {716 return state.evaluater.evaluateExpression(result)717 }718 719 crashEstree(state)720}721 722/**723 * @param {State} state724 * @param {Position | undefined} [place]725 * @returns {never}726 */727function crashEstree(state, place) {728 const message = new VFileMessage(729 'Cannot handle MDX estrees without `createEvaluater`',730 {731 ancestors: state.ancestors,732 place,733 ruleId: 'mdx-estree',734 source: 'hast-util-to-jsx-runtime'735 }736 )737 message.file = state.filePath || undefined738 message.url = docs + '#cannot-handle-mdx-estrees-without-createevaluater'739 740 throw message741}742 743/**744 * Transform a DOM casing style object to a CSS casing style object.745 *746 * @param {Style} domCasing747 * @returns {Style}748 */749function transformStylesToCssCasing(domCasing) {750 /** @type {Style} */751 const cssCasing = {}752 /** @type {string} */753 let from754 755 for (from in domCasing) {756 if (own.call(domCasing, from)) {757 cssCasing[transformStyleToCssCasing(from)] = domCasing[from]758 }759 }760 761 return cssCasing762}763 764/**765 * Transform a DOM casing style field to a CSS casing style field.766 *767 * @param {string} from768 * @returns {string}769 */770function transformStyleToCssCasing(from) {771 let to = from.replace(cap, toDash)772 // Handle `ms-xxx` -> `-ms-xxx`.773 if (to.slice(0, 3) === 'ms-') to = '-' + to774 return to775}776 777/**778 * Make `$0` dash cased.779 *780 * @param {string} $0781 * Capitalized ASCII leter.782 * @returns {string}783 * Dash and lower letter.784 */785function toDash($0) {786 return '-' + $0.toLowerCase()787}788 