feat(ui): finalize E-TIB modernization with footer redesign, video optimization, and TS fixes

Former-commit-id: 67ac02c8404cc66893fdf97308574701cca6000c
This commit is contained in:
2026-05-07 10:43:20 +02:00
parent f2fdea96ec
commit 5439df72ea
3712 changed files with 932332 additions and 1867 deletions
@@ -0,0 +1 @@
../../comma-separated-tokens@2.0.3/node_modules/comma-separated-tokens
@@ -0,0 +1 @@
../../devlop@1.1.0/node_modules/devlop
@@ -0,0 +1 @@
../../estree-util-attach-comments@3.0.0/node_modules/estree-util-attach-comments
@@ -0,0 +1 @@
../../estree-util-is-identifier-name@3.0.0/node_modules/estree-util-is-identifier-name
@@ -0,0 +1,11 @@
/**
* @typedef {import('./lib/state.js').ElementAttributeNameCase} ElementAttributeNameCase
* @typedef {import('./lib/state.js').Handle} Handle
* @typedef {import('./lib/state.js').Options} Options
* @typedef {import('./lib/state.js').Space} Space
* @typedef {import('./lib/state.js').State} State
* @typedef {import('./lib/state.js').StylePropertyNameCase} StylePropertyNameCase
*/
export {handlers as defaultHandlers} from './lib/handlers/index.js'
export {toEstree} from './lib/index.js'
@@ -0,0 +1,42 @@
/**
* @import {
* JSXEmptyExpression as JsxEmptyExpression,
* JSXExpressionContainer as JsxExpressionContainer,
* } from 'estree-jsx'
* @import {Comment} from 'estree'
* @import {State} from 'hast-util-to-estree'
* @import {Comment as HastComment} from 'hast'
*/
// Make VS Code show references to the above types.
''
/**
* Turn a hast comment into an estree node.
*
* @param {HastComment} node
* hast node to transform.
* @param {State} state
* Info passed around about the current state.
* @returns {JsxExpressionContainer}
* estree expression.
*/
export function comment(node, state) {
/** @type {Comment} */
const result = {type: 'Block', value: node.value}
state.inherit(node, result)
state.comments.push(result)
/** @type {JsxEmptyExpression} */
const expression = {
type: 'JSXEmptyExpression',
// @ts-expect-error: `comments` is custom.
comments: [Object.assign({}, result, {leading: false, trailing: true})]
}
state.patch(node, expression)
/** @type {JsxExpressionContainer} */
const container = {type: 'JSXExpressionContainer', expression}
state.patch(node, container)
return container
}
@@ -0,0 +1,288 @@
/**
* @import {
* JSXAttribute as JsxAttribute,
* JSXElement as JsxElement,
* JSXSpreadAttribute as JsxSpreadAttribute,
* } from 'estree-jsx'
* @import {Property} from 'estree'
* @import {State} from 'hast-util-to-estree'
* @import {Element as HastElement} from 'hast'
*/
import {stringify as commas} from 'comma-separated-tokens'
import {name as identifierName} from 'estree-util-is-identifier-name'
import {find, hastToReact, svg} from 'property-information'
import {stringify as spaces} from 'space-separated-tokens'
import styleToJs from 'style-to-js'
const own = {}.hasOwnProperty
const cap = /[A-Z]/g
const tableCellElement = new Set(['td', 'th'])
/**
* Turn a hast element into an estree node.
*
* @param {HastElement} node
* hast node to transform.
* @param {State} state
* Info passed around about the current state.
* @returns {JsxElement}
* estree expression.
*/
// eslint-disable-next-line complexity
export function element(node, state) {
const parentSchema = state.schema
let schema = parentSchema
const properties = node.properties || {}
if (parentSchema.space === 'html' && node.tagName.toLowerCase() === 'svg') {
schema = svg
state.schema = schema
}
const children = state.all(node)
/** @type {Array<JsxAttribute | JsxSpreadAttribute>} */
const attributes = []
/** @type {string} */
let property
/** @type {string | undefined} */
let alignValue
/** @type {Array<Property> | undefined} */
let styleProperties
for (property in properties) {
if (own.call(properties, property)) {
let value = properties[property]
const info = find(schema, property)
/** @type {JsxAttribute['value']} */
let attributeValue
// Ignore nullish and `NaN` values.
// Ignore `false` and falsey known booleans.
if (
value === null ||
value === undefined ||
value === false ||
(typeof value === 'number' && Number.isNaN(value)) ||
(!value && info.boolean)
) {
continue
}
property =
state.elementAttributeNameCase === 'react' && info.space
? hastToReact[info.property] || info.property
: info.attribute
if (Array.isArray(value)) {
// Accept `array`.
// Most properties are space-separated.
value = info.commaSeparated ? commas(value) : spaces(value)
}
if (property === 'style') {
let styleObject =
typeof value === 'object'
? value
: parseStyle(String(value), node.tagName)
if (state.stylePropertyNameCase === 'css') {
styleObject = transformStylesToCssCasing(styleObject)
}
/** @type {Array<Property>} */
const cssProperties = []
/** @type {string} */
let cssProperty
for (cssProperty in styleObject) {
// eslint-disable-next-line max-depth
if (own.call(styleObject, cssProperty)) {
cssProperties.push({
type: 'Property',
method: false,
shorthand: false,
computed: false,
key: identifierName(cssProperty)
? {type: 'Identifier', name: cssProperty}
: {type: 'Literal', value: cssProperty},
value: {type: 'Literal', value: String(styleObject[cssProperty])},
kind: 'init'
})
}
}
styleProperties = cssProperties
attributeValue = {
type: 'JSXExpressionContainer',
expression: {type: 'ObjectExpression', properties: cssProperties}
}
} else if (value === true) {
attributeValue = null
} else if (
state.tableCellAlignToStyle &&
tableCellElement.has(node.tagName) &&
property === 'align'
) {
alignValue = String(value)
continue
} else {
attributeValue = {type: 'Literal', value: String(value)}
}
if (identifierName(property, {jsx: true})) {
attributes.push({
type: 'JSXAttribute',
name: {type: 'JSXIdentifier', name: property},
value: attributeValue
})
} else {
attributes.push({
type: 'JSXSpreadAttribute',
argument: {
type: 'ObjectExpression',
properties: [
{
type: 'Property',
method: false,
shorthand: false,
computed: false,
key: {type: 'Literal', value: String(property)},
// @ts-expect-error No need to worry about `style` (which has a
// `JSXExpressionContainer` value) because that’s a valid identifier.
value: attributeValue || {type: 'Literal', value: true},
kind: 'init'
}
]
}
})
}
}
}
if (alignValue !== undefined) {
if (!styleProperties) {
styleProperties = []
attributes.push({
type: 'JSXAttribute',
name: {type: 'JSXIdentifier', name: 'style'},
value: {
type: 'JSXExpressionContainer',
expression: {type: 'ObjectExpression', properties: styleProperties}
}
})
}
const cssProperty =
state.stylePropertyNameCase === 'css'
? transformStyleToCssCasing('textAlign')
: 'textAlign'
styleProperties.push({
type: 'Property',
method: false,
shorthand: false,
computed: false,
key: identifierName(cssProperty)
? {type: 'Identifier', name: cssProperty}
: {type: 'Literal', value: cssProperty},
value: {type: 'Literal', value: alignValue},
kind: 'init'
})
}
// Restore parent schema.
state.schema = parentSchema
/** @type {JsxElement} */
const result = {
type: 'JSXElement',
openingElement: {
type: 'JSXOpeningElement',
attributes,
name: state.createJsxElementName(node.tagName),
selfClosing: children.length === 0
},
closingElement:
children.length > 0
? {
type: 'JSXClosingElement',
name: state.createJsxElementName(node.tagName)
}
: null,
children
}
state.inherit(node, result)
return result
}
/**
* Parse CSS rules as a declaration.
*
* @param {string} value
* CSS text.
* @param {string} tagName
* Element name.
* @returns {Record<string, string>}
* Properties.
*/
function parseStyle(value, tagName) {
try {
return styleToJs(value, {reactCompat: true})
} catch (error) {
const cause = /** @type {Error} */ (error)
const exception = new Error(
'Could not parse `style` attribute on `' + tagName + '`',
{cause}
)
throw exception
}
}
/**
* Transform a DOM casing style object to a CSS casing style object.
*
* @param {Record<string, string>} domCasing
* @returns {Record<string, string>}
*/
function transformStylesToCssCasing(domCasing) {
/** @type {Record<string, string>} */
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 property to a CSS casing style property.
*
* @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()
}
@@ -0,0 +1,28 @@
import {comment} from './comment.js'
import {element} from './element.js'
import {mdxExpression} from './mdx-expression.js'
import {mdxJsxElement} from './mdx-jsx-element.js'
import {mdxjsEsm} from './mdxjs-esm.js'
import {root} from './root.js'
import {text} from './text.js'
export const handlers = {
comment,
doctype: ignore,
element,
mdxFlowExpression: mdxExpression,
mdxJsxFlowElement: mdxJsxElement,
mdxJsxTextElement: mdxJsxElement,
mdxTextExpression: mdxExpression,
mdxjsEsm,
root,
text
}
/**
* Handle a node that is ignored.
*
* @returns {undefined}
* Nothing.
*/
function ignore() {}
@@ -0,0 +1,51 @@
/**
* @import {
* JSXEmptyExpression as JsxEmptyExpression,
* JSXExpressionContainer as JsxExpressionContainer
* } from 'estree-jsx'
* @import {Expression} from 'estree'
* @import {
* MdxFlowExpressionHast as MdxFlowExpression,
* MdxTextExpressionHast as MdxTextExpression
* } from 'mdast-util-mdx-expression'
* @import {State} from 'hast-util-to-estree'
*/
import {attachComments} from 'estree-util-attach-comments'
/**
* Turn an MDX expression node into an estree node.
*
* @param {MdxFlowExpression | MdxTextExpression} node
* hast node to transform.
* @param {State} state
* Info passed around about the current state.
* @returns {JsxExpressionContainer}
* estree expression.
*/
export function mdxExpression(node, state) {
const estree = node.data && node.data.estree
const comments = (estree && estree.comments) || []
/** @type {Expression | JsxEmptyExpression | undefined} */
let expression
if (estree) {
state.comments.push(...comments)
attachComments(estree, estree.comments)
expression =
(estree.body[0] &&
estree.body[0].type === 'ExpressionStatement' &&
estree.body[0].expression) ||
undefined
}
if (!expression) {
expression = {type: 'JSXEmptyExpression'}
state.patch(node, expression)
}
/** @type {JsxExpressionContainer} */
const result = {type: 'JSXExpressionContainer', expression}
state.inherit(node, result)
return result
}
@@ -0,0 +1,165 @@
/**
* @import {
* JSXAttribute as JsxAttribute,
* JSXElement as JsxElement,
* JSXFragment as JsxFragment,
* JSXSpreadAttribute as JsxSpreadAttribute
* } from 'estree-jsx'
* @import {Expression} from 'estree'
* @import {State} from 'hast-util-to-estree'
* @import {
* MdxJsxFlowElementHast as MdxJsxFlowElement,
* MdxJsxTextElementHast as MdxJsxTextElement
* } from 'mdast-util-mdx-jsx'
*/
import {attachComments} from 'estree-util-attach-comments'
import {svg} from 'property-information'
/**
* Turn an MDX JSX element node into an estree node.
*
* @param {MdxJsxFlowElement | MdxJsxTextElement} node
* hast node to transform.
* @param {State} state
* Info passed around about the current state.
* @returns {JsxElement | JsxFragment}
* JSX element or fragment.
*/
// eslint-disable-next-line complexity
export function mdxJsxElement(node, state) {
const parentSchema = state.schema
let schema = parentSchema
const attributes = node.attributes || []
let index = -1
if (
node.name &&
parentSchema.space === 'html' &&
node.name.toLowerCase() === 'svg'
) {
schema = svg
state.schema = schema
}
const children = state.all(node)
/** @type {Array<JsxAttribute | JsxSpreadAttribute>} */
const jsxAttributes = []
while (++index < attributes.length) {
const attribute = attributes[index]
const value = attribute.value
/** @type {JsxAttribute['value']} */
let attributeValue
if (attribute.type === 'mdxJsxAttribute') {
if (value === null || value === undefined) {
attributeValue = null
// Empty.
}
// `MdxJsxAttributeValueExpression`.
else if (typeof value === 'object') {
const estree = value.data && value.data.estree
const comments = (estree && estree.comments) || []
/** @type {Expression | undefined} */
let expression
if (estree) {
state.comments.push(...comments)
attachComments(estree, estree.comments)
// Should exist.
/* c8 ignore next 5 */
expression =
(estree.body[0] &&
estree.body[0].type === 'ExpressionStatement' &&
estree.body[0].expression) ||
undefined
}
attributeValue = {
type: 'JSXExpressionContainer',
expression: expression || {type: 'JSXEmptyExpression'}
}
state.inherit(value, attributeValue)
}
// Anything else.
else {
attributeValue = {type: 'Literal', value: String(value)}
}
/** @type {JsxAttribute} */
const jsxAttribute = {
type: 'JSXAttribute',
name: state.createJsxAttributeName(attribute.name),
value: attributeValue
}
state.inherit(attribute, jsxAttribute)
jsxAttributes.push(jsxAttribute)
}
// MdxJsxExpressionAttribute.
else {
const estree = attribute.data && attribute.data.estree
const comments = (estree && estree.comments) || []
/** @type {JsxSpreadAttribute['argument'] | undefined} */
let argumentValue
if (estree) {
state.comments.push(...comments)
attachComments(estree, estree.comments)
// Should exist.
/* c8 ignore next 10 */
argumentValue =
(estree.body[0] &&
estree.body[0].type === 'ExpressionStatement' &&
estree.body[0].expression &&
estree.body[0].expression.type === 'ObjectExpression' &&
estree.body[0].expression.properties &&
estree.body[0].expression.properties[0] &&
estree.body[0].expression.properties[0].type === 'SpreadElement' &&
estree.body[0].expression.properties[0].argument) ||
undefined
}
/** @type {JsxSpreadAttribute} */
const jsxAttribute = {
type: 'JSXSpreadAttribute',
argument: argumentValue || {type: 'ObjectExpression', properties: []}
}
state.inherit(attribute, jsxAttribute)
jsxAttributes.push(jsxAttribute)
}
}
// Restore parent schema.
state.schema = parentSchema
/** @type {JsxElement | JsxFragment} */
const result = node.name
? {
type: 'JSXElement',
openingElement: {
type: 'JSXOpeningElement',
attributes: jsxAttributes,
name: state.createJsxElementName(node.name),
selfClosing: children.length === 0
},
closingElement:
children.length > 0
? {
type: 'JSXClosingElement',
name: state.createJsxElementName(node.name)
}
: null,
children
}
: {
type: 'JSXFragment',
openingFragment: {type: 'JSXOpeningFragment'},
closingFragment: {type: 'JSXClosingFragment'},
children
}
state.inherit(node, result)
return result
}
@@ -0,0 +1,27 @@
/**
* @import {MdxjsEsmHast as MdxjsEsm} from 'mdast-util-mdxjs-esm'
* @import {State} from 'hast-util-to-estree'
*/
import {attachComments} from 'estree-util-attach-comments'
/**
* Handle an MDX ESM node.
*
* @param {MdxjsEsm} node
* hast node to transform.
* @param {State} state
* Info passed around about the current state.
* @returns {undefined}
* Nothing.
*/
export function mdxjsEsm(node, state) {
const estree = node.data && node.data.estree
const comments = (estree && estree.comments) || []
if (estree) {
state.comments.push(...comments)
attachComments(estree, comments)
state.esm.push(...estree.body)
}
}
@@ -0,0 +1,59 @@
/**
* @import {
* JSXElement as JsxElement,
* JSXExpressionContainer as JsxExpressionContainer,
* JSXFragment as JsxFragment,
* JSXSpreadChild as JsxSpreadChild,
* JSXText as JsxText,
* } from 'estree-jsx'
* @import {State} from 'hast-util-to-estree'
* @import {Root as HastRoot} from 'hast'
*/
import {whitespace} from 'hast-util-whitespace'
/**
* Turn a hast root node into an estree node.
*
* @param {HastRoot} node
* hast node to transform.
* @param {State} state
* Info passed around about the current state.
* @returns {JsxFragment}
* estree JSX fragment.
*/
export function root(node, state) {
const children = state.all(node)
/** @type {Array<JsxElement | JsxExpressionContainer | JsxFragment | JsxSpreadChild | JsxText>} */
const cleanChildren = []
let index = -1
/** @type {Array<JsxElement | JsxExpressionContainer | JsxFragment | JsxSpreadChild | JsxText> | undefined} */
let queue
// Remove surrounding whitespace nodes from the fragment.
while (++index < children.length) {
const child = children[index]
if (
child.type === 'JSXExpressionContainer' &&
child.expression.type === 'Literal' &&
whitespace(String(child.expression.value))
) {
if (queue) queue.push(child)
} else {
if (queue) cleanChildren.push(...queue)
cleanChildren.push(child)
queue = []
}
}
/** @type {JsxFragment} */
const result = {
type: 'JSXFragment',
openingFragment: {type: 'JSXOpeningFragment'},
closingFragment: {type: 'JSXClosingFragment'},
children: cleanChildren
}
state.inherit(node, result)
return result
}
@@ -0,0 +1,33 @@
/**
* @import {JSXExpressionContainer as JsxExpressionContainer} from 'estree-jsx'
* @import {Literal} from 'estree'
* @import {State} from 'hast-util-to-estree'
* @import {Text as HastText} from 'hast'
*/
// Make VS Code show references to the above types.
''
/**
* Turn a hast text node into an estree node.
*
* @param {HastText} node
* hast node to transform.
* @param {State} state
* Info passed around about the current state.
* @returns {JsxExpressionContainer | undefined}
* JSX expression.
*/
export function text(node, state) {
const value = String(node.value || '')
if (value) {
/** @type {Literal} */
const result = {type: 'Literal', value}
state.inherit(node, result)
/** @type {JsxExpressionContainer} */
const container = {type: 'JSXExpressionContainer', expression: result}
state.patch(node, container)
return container
}
}
@@ -0,0 +1,93 @@
/**
* @import {} from 'mdast-util-mdx-expression'
* @import {} from 'mdast-util-mdx-jsx'
* @import {} from 'mdast-util-mdxjs-esm'
* @import {ExpressionStatement, Program} from 'estree'
* @import {Options} from 'hast-util-to-estree'
* @import {Nodes as HastNodes} from 'hast'
*/
import {createState} from './state.js'
/**
* Transform a hast tree (with embedded MDX nodes) into an estree.
*
* ##### Notes
*
* ###### Comments
*
* Comments are attached to the tree in their neighbouring nodes (`recast`,
* `babel` style) and also added as a `comments` array on the program node
* (`espree` style).
* You may have to do `program.comments = undefined` for certain compilers.
*
* ###### Frameworks
*
* There are differences between what JSX frameworks accept, such as whether they
* accept `class` or `className`, or `background-color` or `backgroundColor`.
*
* For JSX components written in MDX, the author has to be aware of this
* difference and write code accordingly.
* For hast elements transformed by this project, this will be handled through
* options.
*
* | Framework | `elementAttributeNameCase` | `stylePropertyNameCase` |
* | --------- | -------------------------- | ----------------------- |
* | Preact | `'html'` | `'dom'` |
* | React | `'react'` | `'dom'` |
* | Solid | `'html'` | `'css'` |
* | Vue | `'html'` | `'dom'` |
*
* @param {HastNodes} tree
* hast tree.
* @param {Options | null | undefined} [options]
* Configuration (optional).
* @returns {Program}
* estree program node.
*
* The program’s last child in `body` is most likely an `ExpressionStatement`,
* whose expression is a `JSXFragment` or a `JSXElement`.
*
* Typically, there is only one node in `body`, however, this utility also
* supports embedded MDX nodes in the HTML (when `mdast-util-mdx` is used
* with mdast to parse markdown before passing its nodes through to hast).
* When MDX ESM import/exports are used, those nodes are added before the
* fragment or element in body.
*
* There aren’t many great estree serializers out there that support JSX.
* To do that, you can use `estree-util-to-js`.
* Or, use `estree-util-build-jsx` to turn JSX into function calls, and then
* serialize with whatever (`astring`, `escodegen`).
*/
export function toEstree(tree, options) {
const state = createState(options || {})
let result = state.handle(tree)
const body = state.esm
if (result) {
if (result.type !== 'JSXFragment' && result.type !== 'JSXElement') {
result = {
type: 'JSXFragment',
openingFragment: {type: 'JSXOpeningFragment'},
closingFragment: {type: 'JSXClosingFragment'},
children: [result]
}
state.patch(tree, result)
}
/** @type {ExpressionStatement} */
const statement = {type: 'ExpressionStatement', expression: result}
state.patch(tree, statement)
body.push(statement)
}
/** @type {Program} */
const program = {
type: 'Program',
body,
sourceType: 'module',
comments: state.comments
}
state.patch(tree, program)
return program
}
@@ -0,0 +1,354 @@
/**
* @import {
* JSXElement as JsxElement,
* JSXExpressionContainer as JsxExpressionContainer,
* JSXFragment as JsxFragment,
* JSXIdentifier as JsxIdentifier,
* JSXMemberExpression as JsxMemberExpression,
* JSXNamespacedName as JsxNamespacedName,
* JSXSpreadChild as JsxSpreadChild,
* JSXText as JsxText,
* } from 'estree-jsx'
* @import {Comment, Directive, ModuleDeclaration, Node as EstreeNode, Statement} from 'estree'
* @import {MdxJsxAttribute, MdxJsxAttributeValueExpression, MdxJsxExpressionAttribute} from 'mdast-util-mdx-jsx'
* @import {Nodes as HastNodes, Parents as HastParents} from 'hast'
* @import {Schema} from 'property-information'
*/
/**
* @typedef {'html' | 'react'} ElementAttributeNameCase
* Specify casing to use for attribute names.
*
* HTML casing is for example `class`, `stroke-linecap`, `xml:lang`.
* React casing is for example `className`, `strokeLinecap`, `xmlLang`.
*
* @callback Handle
* Turn a hast node into an estree node.
* @param {any} node
* Expected hast node.
* @param {State} state
* Info passed around about the current state.
* @returns {JsxElement | JsxExpressionContainer | JsxFragment | JsxSpreadChild | JsxText | null | undefined}
* estree node.
*
* @typedef Options
* Configuration.
* @property {ElementAttributeNameCase | null | undefined} [elementAttributeNameCase='react']
* Specify casing to use for attribute names (default: `'react'`).
*
* This casing is used for hast elements, not for embedded MDX JSX nodes
* (components that someone authored manually).
* @property {Record<string, Handle | null | undefined> | null | undefined} [handlers={}]
* Custom handlers (optional).
* @property {Space | null | undefined} [space='html']
* Which space the document is in (default: `'html'`).
*
* When an `<svg>` element is found in the HTML space, this package already
* automatically switches to and from the SVG space when entering and exiting
* it.
* @property {StylePropertyNameCase | null | undefined} [stylePropertyNameCase='dom']
* Specify casing to use for property names in `style` objects (default: `'dom'`).
*
* This casing is used for hast elements, not for embedded MDX JSX nodes
* (components that someone authored manually).
* @property {boolean | null | undefined} [tableCellAlignToStyle=true]
* Turn obsolete `align` props on `td` and `th` into CSS `style` props
* (default: `true`).
*
* @typedef {'html' | 'svg'} Space
* Namespace.
*
* @typedef {'css' | 'dom'} StylePropertyNameCase
* Casing to use for property names in `style` objects.
*
* CSS casing is for example `background-color` and `-webkit-line-clamp`.
* DOM casing is for example `backgroundColor` and `WebkitLineClamp`.
*
* @typedef State
* Info passed around about the current state.
* @property {(parent: HastParents) => Array<JsxElement | JsxExpressionContainer | JsxFragment | JsxSpreadChild | JsxText>} all
* Transform children of a hast parent to estree.
* @property {Array<Comment>} comments
* List of estree comments.
* @property {(name: string) => JsxIdentifier | JsxNamespacedName} createJsxAttributeName
* Create a JSX attribute name.
* @property {(name: string) => JsxIdentifier | JsxMemberExpression | JsxNamespacedName} createJsxElementName
* Create a JSX element name.
* @property {ElementAttributeNameCase} elementAttributeNameCase
* Casing to use for attribute names.
* @property {Array<Directive | ModuleDeclaration | Statement>} esm
* List of top-level estree nodes.
* @property {(node: any) => JsxElement | JsxExpressionContainer | JsxFragment | JsxSpreadChild | JsxText | null | undefined} handle
* Transform a hast node to estree.
* @property {(from: HastNodes | MdxJsxAttribute | MdxJsxAttributeValueExpression | MdxJsxExpressionAttribute, to: Comment | EstreeNode) => undefined} inherit
* Take positional info and data from `from` (use `patch` if you don’t want data).
* @property {(from: HastNodes, to: Comment | EstreeNode) => undefined} patch
* Take positional info from `from` (use `inherit` if you also want data).
* @property {Schema} schema
* Current schema.
* @property {StylePropertyNameCase} stylePropertyNameCase
* Casing to use for property names in `style` objects.
* @property {boolean} tableCellAlignToStyle
* Turn obsolete `align` props on `td` and `th` into CSS `style` props.
*/
import {ok as assert} from 'devlop'
import {html, svg} from 'property-information'
import {position} from 'unist-util-position'
import {zwitch} from 'zwitch'
import {handlers} from './handlers/index.js'
const own = {}.hasOwnProperty
// `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>.
// See: <https://github.com/mdx-js/mdx/issues/2000>
const tableElements = new Set(['table', 'tbody', 'thead', 'tfoot', 'tr'])
/**
* Create a state from options.
*
* @param {Options} options
* Configuration.
* @returns {State}
* Info passed around about the current state.
*/
export function createState(options) {
/** @type {Handle} */
const one = zwitch('type', {
invalid,
unknown,
handlers: {...handlers, ...options.handlers}
})
return {
// Current space.
elementAttributeNameCase: options.elementAttributeNameCase || 'react',
schema: options.space === 'svg' ? svg : html,
stylePropertyNameCase: options.stylePropertyNameCase || 'dom',
tableCellAlignToStyle: options.tableCellAlignToStyle !== false,
// Results.
comments: [],
esm: [],
// Useful functions.
all,
createJsxAttributeName,
createJsxElementName,
handle,
inherit,
patch
}
/**
* @this {State}
* @param {any} node
* @returns {JsxElement | JsxExpressionContainer | JsxFragment | JsxSpreadChild | JsxText | null | undefined}
*/
function handle(node) {
return one(node, this)
}
}
/**
* Crash on an invalid value.
*
* @param {unknown} value
* Non-node.
* @returns {never}
* Nothing (crashes).
*/
function invalid(value) {
throw new Error('Cannot handle value `' + value + '`, expected node')
}
/**
* Crash on an unknown node.
*
* @param {unknown} node
* Unknown node.
* @returns {never}
* Nothing (crashes).
*/
function unknown(node) {
assert(node && typeof node === 'object')
assert('type' in node)
throw new Error('Cannot handle unknown node `' + node.type + '`')
}
/**
* @this {State} state
* Info passed around about the current state.
* @param {HastParents} parent
* hast node whose children to transform.
* @returns {Array<JsxElement | JsxExpressionContainer | JsxFragment | JsxSpreadChild | JsxText>}
* estree nodes.
*/
function all(parent) {
const children = parent.children || []
let index = -1
/** @type {Array<JsxElement | JsxExpressionContainer | JsxFragment | JsxSpreadChild | JsxText>} */
const results = []
const ignoreLineBreak =
this.schema.space === 'html' &&
parent.type === 'element' &&
tableElements.has(parent.tagName.toLowerCase())
while (++index < children.length) {
const child = children[index]
if (ignoreLineBreak && child.type === 'text' && child.value === '\n') {
continue
}
const result = this.handle(child)
if (Array.isArray(result)) {
results.push(...result)
} else if (result) {
results.push(result)
}
}
return results
}
/**
* Take positional info and data from `hast`.
*
* Use `patch` if you don’t want data.
*
* @param {HastNodes | MdxJsxAttribute | MdxJsxAttributeValueExpression | MdxJsxExpressionAttribute} from
* hast node to take positional info and data from.
* @param {Comment | EstreeNode} to
* estree node to add positional info and data to.
* @returns {undefined}
* Nothing.
*/
function inherit(from, to) {
const left = /** @type {Record<string, unknown> | undefined} */ (from.data)
/** @type {Record<string, unknown> | undefined} */
let right
/** @type {string} */
let key
patch(from, to)
if (left) {
for (key in left) {
if (own.call(left, key) && key !== 'estree') {
if (!right) right = {}
right[key] = left[key]
}
}
if (right) {
// @ts-expect-error `esast` extension.
to.data = right
}
}
}
/**
* Take positional info from `from`.
*
* Use `inherit` if you also want data.
*
* @param {HastNodes | MdxJsxAttribute | MdxJsxAttributeValueExpression | MdxJsxExpressionAttribute} from
* hast node to take positional info from.
* @param {Comment | EstreeNode} to
* estree node to add positional info to.
* @returns {undefined}
* Nothing.
*/
function patch(from, to) {
const p = position(from)
if (p && p.start.offset !== undefined && p.end.offset !== undefined) {
// @ts-expect-error acorn-style.
to.start = p.start.offset
// @ts-expect-error acorn-style.
to.end = p.end.offset
to.loc = {
start: {line: p.start.line, column: p.start.column - 1},
end: {line: p.end.line, column: p.end.column - 1}
}
to.range = [p.start.offset, p.end.offset]
}
}
/**
* Create a JSX attribute name.
*
* @param {string} name
* @returns {JsxIdentifier | JsxNamespacedName}
*/
function createJsxAttributeName(name) {
const node = createJsxNameFromString(name)
// MDX never generates this.
/* c8 ignore next 3 */
if (node.type === 'JSXMemberExpression') {
throw new Error('Member expressions in attribute names are not supported')
}
return node
}
/**
* Create a JSX element name.
*
* @param {string} name
* @returns {JsxIdentifier | JsxMemberExpression | JsxNamespacedName}
*/
function createJsxElementName(name) {
return createJsxNameFromString(name)
}
/**
* Create a JSX name from a string.
*
* @param {string} name
* Name.
* @returns {JsxIdentifier | JsxMemberExpression | JsxNamespacedName}
* Node.
*/
function createJsxNameFromString(name) {
if (name.includes('.')) {
const names = name.split('.')
let part = names.shift()
assert(part, 'Expected `part` to be defined')
/** @type {JsxIdentifier | JsxMemberExpression} */
let node = {type: 'JSXIdentifier', name: part}
while ((part = names.shift())) {
node = {
type: 'JSXMemberExpression',
object: node,
property: {type: 'JSXIdentifier', name: part}
}
}
return node
}
if (name.includes(':')) {
const parts = name.split(':')
return {
type: 'JSXNamespacedName',
namespace: {type: 'JSXIdentifier', name: parts[0]},
name: {type: 'JSXIdentifier', name: parts[1]}
}
}
return {type: 'JSXIdentifier', name}
}
@@ -0,0 +1,116 @@
{
"author": "Titus Wormer <tituswormer@gmail.com> (https://wooorm.com)",
"bugs": "https://github.com/syntax-tree/hast-util-to-estree/issues",
"contributors": [
"Titus Wormer <tituswormer@gmail.com> (https://wooorm.com)"
],
"dependencies": {
"@types/estree": "^1.0.0",
"@types/estree-jsx": "^1.0.0",
"@types/hast": "^3.0.0",
"comma-separated-tokens": "^2.0.0",
"devlop": "^1.0.0",
"estree-util-attach-comments": "^3.0.0",
"estree-util-is-identifier-name": "^3.0.0",
"hast-util-whitespace": "^3.0.0",
"mdast-util-mdx-expression": "^2.0.0",
"mdast-util-mdx-jsx": "^3.0.0",
"mdast-util-mdxjs-esm": "^2.0.0",
"property-information": "^7.0.0",
"space-separated-tokens": "^2.0.0",
"style-to-js": "^1.0.0",
"unist-util-position": "^5.0.0",
"zwitch": "^2.0.0"
},
"description": "hast utility to transform to estree (JavaScript AST) JSX",
"devDependencies": {
"@types/node": "^22.0.0",
"acorn-jsx": "^5.0.0",
"c8": "^10.0.0",
"esast-util-from-js": "^2.0.0",
"estree-util-to-js": "^2.0.0",
"estree-walker": "^3.0.0",
"hastscript": "^9.0.0",
"mdast-util-from-markdown": "^2.0.0",
"mdast-util-mdx": "^3.0.0",
"mdast-util-to-hast": "^13.0.0",
"micromark-extension-mdxjs": "^3.0.0",
"prettier": "^3.0.0",
"remark-cli": "^12.0.0",
"remark-preset-wooorm": "^11.0.0",
"type-coverage": "^2.0.0",
"typescript": "^5.0.0",
"unist-util-visit": "^5.0.0",
"xo": "^0.60.0"
},
"exports": "./index.js",
"files": [
"index.d.ts.map",
"index.d.ts",
"index.js",
"lib/"
],
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
},
"keywords": [
"change",
"ecmascript",
"estree",
"hast-util",
"hast",
"html",
"javascript",
"jsx",
"mdx",
"rehype",
"transform",
"unist",
"utility",
"util"
],
"license": "MIT",
"name": "hast-util-to-estree",
"prettier": {
"bracketSpacing": false,
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "none",
"useTabs": false
},
"remarkConfig": {
"plugins": [
"remark-preset-wooorm"
]
},
"repository": "syntax-tree/hast-util-to-estree",
"scripts": {
"build": "tsc --build --clean && tsc --build && type-coverage",
"format": "remark --frail --output --quiet -- . && prettier --log-level warn --write -- . && xo --fix",
"test-api": "node --conditions development test.js",
"test-coverage": "c8 --100 --reporter lcov -- npm run test-api",
"test": "npm run build && npm run format && npm run test-coverage"
},
"sideEffects": false,
"typeCoverage": {
"atLeast": 100,
"ignoreFiles#": "needed `any`",
"ignoreFiles": [
"lib/state.d.ts",
"lib/state.js"
],
"strict": true
},
"type": "module",
"version": "3.1.3",
"xo": {
"prettier": true,
"rules": {
"logical-assignment-operators": "off",
"unicorn/prefer-code-point": "off",
"unicorn/prefer-string-replace-all": "off"
}
}
}
@@ -0,0 +1 @@
../../hast-util-whitespace@3.0.0/node_modules/hast-util-whitespace
@@ -0,0 +1 @@
../../property-information@7.1.0/node_modules/property-information
@@ -0,0 +1 @@
../../space-separated-tokens@2.0.2/node_modules/space-separated-tokens
@@ -0,0 +1 @@
../../style-to-js@1.1.21/node_modules/style-to-js
@@ -0,0 +1 @@
../../unist-util-position@5.0.0/node_modules/unist-util-position
@@ -0,0 +1 @@
../../zwitch@2.0.4/node_modules/zwitch