CoolFace
Apppublic

AK-21/Graphite-Industrial-Intelligence

sourceHugging Faceupdated 3mo agoView on Hugging Face
0likes
index.js788 linesDownload Raw Back to lib
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