hast-util-to-estree
Advanced tools
Comparing version 2.1.0 to 2.2.0
@@ -0,4 +1,6 @@ | ||
export {handlers as defaultHandlers} from './lib/handlers/index.js' | ||
export {toEstree} from './lib/index.js' | ||
export type Handle = import('./lib/index.js').Handle | ||
export type Space = import('./lib/index.js').Space | ||
export type Options = import('./lib/index.js').Options | ||
export type Handle = import('./lib/state.js').Handle | ||
export type Options = import('./lib/state.js').Options | ||
export type Space = import('./lib/state.js').Space | ||
export type State = import('./lib/state.js').State |
/** | ||
* @typedef {import('./lib/index.js').Handle} Handle | ||
* @typedef {import('./lib/index.js').Space} Space | ||
* @typedef {import('./lib/index.js').Options} Options | ||
* @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 | ||
*/ | ||
export {handlers as defaultHandlers} from './lib/handlers/index.js' | ||
export {toEstree} from './lib/index.js' |
/** | ||
* @param {Node|MdxJsxAttributeValueExpression|MdxJsxAttribute|MdxJsxExpressionAttribute|MdxJsxFlowElement|MdxJsxTextElement|MdxFlowExpression|MdxTextExpression} tree | ||
* @param {Options} options | ||
* @returns {EstreeProgram} | ||
* Transform a hast tree (with embedded MDX nodes) into an estree. | ||
* | ||
* ###### Notes | ||
* | ||
* 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. | ||
* | ||
* @param {Node} tree | ||
* hast tree. | ||
* @param {Options | null | undefined} [options] | ||
* Configuration. | ||
* @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: | ||
| Node | ||
| MdxJsxAttributeValueExpression | ||
| MdxJsxAttribute | ||
| MdxJsxExpressionAttribute | ||
| MdxJsxFlowElement | ||
| MdxJsxTextElement | ||
| MdxFlowExpression | ||
| MdxTextExpression, | ||
options?: Options | ||
): EstreeProgram | ||
tree: Node, | ||
options?: Options | null | undefined | ||
): Program | ||
export type Content = import('hast').Content | ||
export type Root = import('hast').Root | ||
export type Element = import('hast').Element | ||
export type Text = import('hast').Text | ||
export type Comment = import('hast').Comment | ||
export type Content = import('hast').Content | ||
export type Node = Root | Content | ||
export type Parent = Extract<Node, import('unist').Parent> | ||
export type EstreeNode = import('estree').Node | ||
export type EstreeProgram = import('estree').Program | ||
export type EstreeComment = import('estree').Comment | ||
export type EstreeDirective = import('estree').Directive | ||
export type EstreeStatement = import('estree').Statement | ||
export type EstreeModuleDeclaration = import('estree').ModuleDeclaration | ||
export type EstreeExpression = import('estree').Expression | ||
export type EstreeProperty = import('estree').Property | ||
export type EstreeJsxExpressionContainer = | ||
import('estree-jsx').JSXExpressionContainer | ||
export type EstreeJsxElement = import('estree-jsx').JSXElement | ||
export type EstreeJsxOpeningElement = import('estree-jsx').JSXOpeningElement | ||
export type EstreeJsxFragment = import('estree-jsx').JSXFragment | ||
export type EstreeJsxAttribute = import('estree-jsx').JSXAttribute | ||
export type EstreeJsxSpreadAttribute = import('estree-jsx').JSXSpreadAttribute | ||
export type JSXIdentifier = import('estree-jsx').JSXIdentifier | ||
export type JSXMemberExpression = import('estree-jsx').JSXMemberExpression | ||
export type EstreeJsxElementName = EstreeJsxOpeningElement['name'] | ||
export type EstreeJsxAttributeName = EstreeJsxAttribute['name'] | ||
export type EstreeJsxChild = EstreeJsxElement['children'][number] | ||
export type ExpressionStatement = import('estree').ExpressionStatement | ||
export type Program = import('estree').Program | ||
export type MdxJsxAttribute = import('mdast-util-mdx-jsx').MdxJsxAttribute | ||
export type MdxJsxAttributeValueExpression = | ||
import('mdast-util-mdx-jsx').MdxJsxAttributeValueExpression | ||
export type MdxJsxAttribute = import('mdast-util-mdx-jsx').MdxJsxAttribute | ||
export type MdxJsxExpressionAttribute = | ||
@@ -56,15 +51,12 @@ import('mdast-util-mdx-jsx').MdxJsxExpressionAttribute | ||
import('mdast-util-mdx-expression').MdxTextExpression | ||
export type MdxjsEsm = import('mdast-util-mdxjs-esm').MdxjsEsm | ||
export type Space = 'html' | 'svg' | ||
export type Handle = (node: any, context: Context) => EstreeJsxChild | null | ||
export type Options = { | ||
space?: Space | undefined | ||
handlers?: Record<string, Handle> | undefined | ||
} | ||
export type Context = { | ||
schema: typeof html | ||
comments: Array<EstreeComment> | ||
esm: Array<EstreeDirective | EstreeStatement | EstreeModuleDeclaration> | ||
handle: Handle | ||
} | ||
import {html} from 'property-information' | ||
export type Options = import('./state.js').Options | ||
export type Node = | ||
| Root | ||
| Content | ||
| MdxJsxAttributeValueExpression | ||
| MdxJsxAttribute | ||
| MdxJsxExpressionAttribute | ||
| MdxJsxFlowElement | ||
| MdxJsxTextElement | ||
| MdxFlowExpression | ||
| MdxTextExpression |
776
lib/index.js
/** | ||
* @typedef {import('hast').Content} Content | ||
* @typedef {import('hast').Root} Root | ||
* @typedef {import('hast').Element} Element | ||
* @typedef {import('hast').Text} Text | ||
* @typedef {import('hast').Comment} Comment | ||
* @typedef {import('hast').Content} Content | ||
* @typedef {Root|Content} Node | ||
* @typedef {Extract<Node, import('unist').Parent>} Parent | ||
* @typedef {import('estree').Node} EstreeNode | ||
* @typedef {import('estree').Program} EstreeProgram | ||
* @typedef {import('estree').Comment} EstreeComment | ||
* @typedef {import('estree').Directive} EstreeDirective | ||
* @typedef {import('estree').Statement} EstreeStatement | ||
* @typedef {import('estree').ModuleDeclaration} EstreeModuleDeclaration | ||
* @typedef {import('estree').Expression} EstreeExpression | ||
* @typedef {import('estree').Property} EstreeProperty | ||
* @typedef {import('estree-jsx').JSXExpressionContainer} EstreeJsxExpressionContainer | ||
* @typedef {import('estree-jsx').JSXElement} EstreeJsxElement | ||
* @typedef {import('estree-jsx').JSXOpeningElement} EstreeJsxOpeningElement | ||
* @typedef {import('estree-jsx').JSXFragment} EstreeJsxFragment | ||
* @typedef {import('estree-jsx').JSXAttribute} EstreeJsxAttribute | ||
* @typedef {import('estree-jsx').JSXSpreadAttribute} EstreeJsxSpreadAttribute | ||
* @typedef {import('estree-jsx').JSXIdentifier} JSXIdentifier | ||
* @typedef {import('estree-jsx').JSXMemberExpression} JSXMemberExpression | ||
* | ||
* @typedef {EstreeJsxOpeningElement['name']} EstreeJsxElementName | ||
* @typedef {EstreeJsxAttribute['name']} EstreeJsxAttributeName | ||
* @typedef {EstreeJsxElement['children'][number]} EstreeJsxChild | ||
* @typedef {import('estree').ExpressionStatement} ExpressionStatement | ||
* @typedef {import('estree').Program} Program | ||
* | ||
* @typedef {import('mdast-util-mdx-jsx').MdxJsxAttribute} MdxJsxAttribute | ||
* @typedef {import('mdast-util-mdx-jsx').MdxJsxAttributeValueExpression} MdxJsxAttributeValueExpression | ||
* @typedef {import('mdast-util-mdx-jsx').MdxJsxAttribute} MdxJsxAttribute | ||
* @typedef {import('mdast-util-mdx-jsx').MdxJsxExpressionAttribute} MdxJsxExpressionAttribute | ||
@@ -39,86 +17,50 @@ * @typedef {import('mdast-util-mdx-jsx').MdxJsxFlowElement} MdxJsxFlowElement | ||
* | ||
* @typedef {import('mdast-util-mdxjs-esm').MdxjsEsm} MdxjsEsm | ||
* | ||
* @typedef {'html'|'svg'} Space | ||
* | ||
* @typedef {(node: any, context: Context) => EstreeJsxChild?} Handle | ||
* | ||
* @typedef Options | ||
* @property {Space} [space='html'] | ||
* @property {Record<string, Handle>} [handlers={}] | ||
* | ||
* @typedef Context | ||
* @property {typeof html} schema | ||
* @property {Array<EstreeComment>} comments | ||
* @property {Array<EstreeDirective|EstreeStatement|EstreeModuleDeclaration>} esm | ||
* @property {Handle} handle | ||
* @typedef {import('./state.js').Options} Options | ||
*/ | ||
import {stringify as commas} from 'comma-separated-tokens' | ||
import {attachComments} from 'estree-util-attach-comments' | ||
import { | ||
start as identifierStart, | ||
cont as identifierCont | ||
} from 'estree-util-is-identifier-name' | ||
import {whitespace} from 'hast-util-whitespace' | ||
import {html, svg, find, hastToReact} from 'property-information' | ||
import {stringify as spaces} from 'space-separated-tokens' | ||
import style from 'style-to-object' | ||
import {position} from 'unist-util-position' | ||
import {zwitch} from 'zwitch' | ||
/** | ||
* @typedef {Root | Content | MdxJsxAttributeValueExpression | MdxJsxAttribute | MdxJsxExpressionAttribute | MdxJsxFlowElement | MdxJsxTextElement | MdxFlowExpression | MdxTextExpression} Node | ||
*/ | ||
const toReact = /** @type {Record<string, string>} */ (hastToReact) | ||
import {createState} from './state.js' | ||
const own = {}.hasOwnProperty | ||
const tableElements = new Set([ | ||
'table', | ||
'thead', | ||
'tbody', | ||
'tfoot', | ||
'tr', | ||
'th', | ||
'td' | ||
]) | ||
/** | ||
* @param {Node|MdxJsxAttributeValueExpression|MdxJsxAttribute|MdxJsxExpressionAttribute|MdxJsxFlowElement|MdxJsxTextElement|MdxFlowExpression|MdxTextExpression} tree | ||
* @param {Options} options | ||
* @returns {EstreeProgram} | ||
* Transform a hast tree (with embedded MDX nodes) into an estree. | ||
* | ||
* ###### Notes | ||
* | ||
* 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. | ||
* | ||
* @param {Node} tree | ||
* hast tree. | ||
* @param {Options | null | undefined} [options] | ||
* Configuration. | ||
* @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 = {}) { | ||
/** @type {Context} */ | ||
const context = { | ||
schema: options.space === 'svg' ? svg : html, | ||
comments: [], | ||
esm: [], | ||
// @ts-expect-error: hush. | ||
handle: zwitch('type', { | ||
invalid, | ||
// @ts-expect-error: hush. | ||
unknown, | ||
// @ts-expect-error: hush. | ||
handlers: Object.assign( | ||
{}, | ||
{ | ||
comment, | ||
doctype: ignore, | ||
element, | ||
mdxjsEsm, | ||
mdxFlowExpression: mdxExpression, | ||
mdxJsxFlowElement: mdxJsxElement, | ||
mdxJsxTextElement: mdxJsxElement, | ||
mdxTextExpression: mdxExpression, | ||
root, | ||
text | ||
}, | ||
options.handlers | ||
) | ||
}) | ||
} | ||
let result = context.handle(tree, context) | ||
const body = context.esm | ||
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 = create(tree, { | ||
result = { | ||
type: 'JSXFragment', | ||
@@ -128,632 +70,22 @@ openingFragment: {type: 'JSXOpeningFragment'}, | ||
children: [result] | ||
}) | ||
} | ||
state.patch(tree, result) | ||
} | ||
/** @type {ExpressionStatement} */ | ||
// @ts-expect-error Types are wrong (`expression` *can* be JSX). | ||
body.push(create(tree, {type: 'ExpressionStatement', expression: result})) | ||
const statement = {type: 'ExpressionStatement', expression: result} | ||
state.patch(tree, statement) | ||
body.push(statement) | ||
} | ||
return create(tree, { | ||
/** @type {Program} */ | ||
const program = { | ||
type: 'Program', | ||
body, | ||
sourceType: 'module', | ||
comments: context.comments | ||
}) | ||
} | ||
/** | ||
* @param {unknown} value | ||
*/ | ||
function invalid(value) { | ||
throw new Error('Cannot handle value `' + value + '`, expected node') | ||
} | ||
/** | ||
* @param {Node} node | ||
*/ | ||
function unknown(node) { | ||
throw new Error('Cannot handle unknown node `' + node.type + '`') | ||
} | ||
function ignore() {} | ||
/** | ||
* @param {Comment} node | ||
* @param {Context} context | ||
* @returns {EstreeJsxExpressionContainer} | ||
*/ | ||
function comment(node, context) { | ||
const esnode = inherit(node, {type: 'Block', value: node.value}) | ||
context.comments.push(esnode) | ||
return create(node, { | ||
type: 'JSXExpressionContainer', | ||
expression: create(node, { | ||
type: 'JSXEmptyExpression', | ||
comments: [Object.assign({}, esnode, {leading: false, trailing: true})] | ||
}) | ||
}) | ||
} | ||
/** | ||
* @param {Element} node | ||
* @param {Context} context | ||
* @returns {EstreeJsxElement} | ||
*/ | ||
// eslint-disable-next-line complexity | ||
function element(node, context) { | ||
const parentSchema = context.schema | ||
let schema = parentSchema | ||
const props = node.properties || {} | ||
if (parentSchema.space === 'html' && node.tagName.toLowerCase() === 'svg') { | ||
schema = svg | ||
context.schema = schema | ||
comments: state.comments | ||
} | ||
const children = all(node, context) | ||
/** @type {Array<EstreeJsxAttribute|EstreeJsxSpreadAttribute>} */ | ||
const attributes = [] | ||
/** @type {string} */ | ||
let prop | ||
for (prop in props) { | ||
if (own.call(props, prop)) { | ||
let value = props[prop] | ||
const info = find(schema, prop) | ||
/** @type {EstreeJsxAttribute['value']} */ | ||
let attributeValue | ||
// Ignore nullish and `NaN` values. | ||
// Ignore `false` and falsey known booleans. | ||
if ( | ||
value === undefined || | ||
value === null || | ||
(typeof value === 'number' && Number.isNaN(value)) || | ||
value === false || | ||
(!value && info.boolean) | ||
) { | ||
continue | ||
} | ||
prop = info.space | ||
? toReact[info.property] || info.property | ||
: info.attribute | ||
if (Array.isArray(value)) { | ||
// Accept `array`. | ||
// Most props are space-separated. | ||
value = info.commaSeparated ? commas(value) : spaces(value) | ||
} | ||
if (prop === 'style') { | ||
/** @type {Record<string, string>} */ | ||
// @ts-expect-error Assume `value` is an object otherwise. | ||
const styleValue = | ||
typeof value === 'string' ? parseStyle(value, node.tagName) : value | ||
/** @type {Array<EstreeProperty>} */ | ||
const cssProperties = [] | ||
/** @type {string} */ | ||
let cssProp | ||
for (cssProp in styleValue) { | ||
// eslint-disable-next-line max-depth | ||
if (own.call(styleValue, cssProp)) { | ||
cssProperties.push({ | ||
type: 'Property', | ||
method: false, | ||
shorthand: false, | ||
computed: false, | ||
key: {type: 'Identifier', name: cssProp}, | ||
value: {type: 'Literal', value: String(styleValue[cssProp])}, | ||
kind: 'init' | ||
}) | ||
} | ||
} | ||
attributeValue = { | ||
type: 'JSXExpressionContainer', | ||
expression: {type: 'ObjectExpression', properties: cssProperties} | ||
} | ||
} else if (value === true) { | ||
attributeValue = null | ||
} else { | ||
attributeValue = {type: 'Literal', value: String(value)} | ||
} | ||
if (jsxIdentifierName(prop)) { | ||
attributes.push({ | ||
type: 'JSXAttribute', | ||
name: {type: 'JSXIdentifier', name: prop}, | ||
value: attributeValue | ||
}) | ||
} else { | ||
attributes.push({ | ||
type: 'JSXSpreadAttribute', | ||
argument: { | ||
type: 'ObjectExpression', | ||
properties: [ | ||
{ | ||
type: 'Property', | ||
method: false, | ||
shorthand: false, | ||
computed: false, | ||
key: {type: 'Literal', value: String(prop)}, | ||
// @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' | ||
} | ||
] | ||
} | ||
}) | ||
} | ||
} | ||
} | ||
// Restore parent schema. | ||
context.schema = parentSchema | ||
return inherit(node, { | ||
type: 'JSXElement', | ||
openingElement: { | ||
type: 'JSXOpeningElement', | ||
attributes, | ||
name: createJsxName(node.tagName), | ||
selfClosing: children.length === 0 | ||
}, | ||
closingElement: | ||
children.length > 0 | ||
? {type: 'JSXClosingElement', name: createJsxName(node.tagName)} | ||
: null, | ||
children | ||
}) | ||
state.patch(tree, program) | ||
return program | ||
} | ||
/** | ||
* @param {MdxjsEsm} node | ||
* @param {Context} context | ||
* @returns {void} | ||
*/ | ||
function mdxjsEsm(node, context) { | ||
const estree = node.data && node.data.estree | ||
const comments = (estree && estree.comments) || [] | ||
if (estree) { | ||
context.comments.push(...comments) | ||
attachComments(estree, comments) | ||
context.esm.push(...estree.body) | ||
} | ||
} | ||
/** | ||
* @param {MdxFlowExpression|MdxTextExpression} node | ||
* @param {Context} context | ||
* @returns {EstreeJsxExpressionContainer} | ||
*/ | ||
function mdxExpression(node, context) { | ||
const estree = node.data && node.data.estree | ||
const comments = (estree && estree.comments) || [] | ||
/** @type {EstreeExpression|undefined} */ | ||
let expression | ||
if (estree) { | ||
context.comments.push(...comments) | ||
attachComments(estree, estree.comments) | ||
expression = | ||
(estree.body[0] && | ||
estree.body[0].type === 'ExpressionStatement' && | ||
estree.body[0].expression) || | ||
undefined | ||
} | ||
return inherit(node, { | ||
type: 'JSXExpressionContainer', | ||
expression: expression || create(node, {type: 'JSXEmptyExpression'}) | ||
}) | ||
} | ||
/** | ||
* @param {MdxJsxFlowElement|MdxJsxTextElement} node | ||
* @param {Context} context | ||
* @returns {EstreeJsxElement|EstreeJsxFragment} | ||
*/ | ||
// eslint-disable-next-line complexity | ||
function mdxJsxElement(node, context) { | ||
const parentSchema = context.schema | ||
let schema = parentSchema | ||
const attrs = node.attributes || [] | ||
let index = -1 | ||
if ( | ||
node.name && | ||
parentSchema.space === 'html' && | ||
node.name.toLowerCase() === 'svg' | ||
) { | ||
schema = svg | ||
context.schema = schema | ||
} | ||
const children = all(node, context) | ||
/** @type {Array<EstreeJsxAttribute|EstreeJsxSpreadAttribute>} */ | ||
const attributes = [] | ||
while (++index < attrs.length) { | ||
const attr = attrs[index] | ||
const value = attr.value | ||
/** @type {EstreeJsxAttribute['value']} */ | ||
let attributeValue | ||
if (attr.type === 'mdxJsxAttribute') { | ||
if (value === undefined || value === null) { | ||
attributeValue = null | ||
// Empty. | ||
} | ||
// `MdxJsxAttributeValueExpression`. | ||
else if (typeof value === 'object') { | ||
const estree = value.data && value.data.estree | ||
const comments = (estree && estree.comments) || [] | ||
/** @type {EstreeExpression|undefined} */ | ||
let expression | ||
if (estree) { | ||
context.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 = inherit(value, { | ||
type: 'JSXExpressionContainer', | ||
expression: expression || {type: 'JSXEmptyExpression'} | ||
}) | ||
} | ||
// Anything else. | ||
else { | ||
attributeValue = {type: 'Literal', value: String(value)} | ||
} | ||
attributes.push( | ||
inherit(attr, { | ||
type: 'JSXAttribute', | ||
name: createJsxName(attr.name, true), | ||
value: attributeValue | ||
}) | ||
) | ||
} | ||
// MdxJsxExpressionAttribute. | ||
else { | ||
const estree = attr.data && attr.data.estree | ||
const comments = (estree && estree.comments) || [] | ||
/** @type {EstreeJsxSpreadAttribute['argument']|undefined} */ | ||
let argumentValue | ||
if (estree) { | ||
context.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 | ||
} | ||
attributes.push( | ||
inherit(attr, { | ||
type: 'JSXSpreadAttribute', | ||
argument: argumentValue || {type: 'ObjectExpression', properties: []} | ||
}) | ||
) | ||
} | ||
} | ||
// Restore parent schema. | ||
context.schema = parentSchema | ||
return inherit( | ||
node, | ||
node.name | ||
? { | ||
type: 'JSXElement', | ||
openingElement: { | ||
type: 'JSXOpeningElement', | ||
attributes, | ||
name: createJsxName(node.name), | ||
selfClosing: children.length === 0 | ||
}, | ||
closingElement: | ||
children.length > 0 | ||
? {type: 'JSXClosingElement', name: createJsxName(node.name)} | ||
: null, | ||
children | ||
} | ||
: { | ||
type: 'JSXFragment', | ||
openingFragment: {type: 'JSXOpeningFragment'}, | ||
closingFragment: {type: 'JSXClosingFragment'}, | ||
children | ||
} | ||
) | ||
} | ||
/** | ||
* @param {Root} node | ||
* @param {Context} context | ||
* @returns {EstreeJsxFragment} | ||
*/ | ||
function root(node, context) { | ||
const children = all(node, context) | ||
/** @type {Array<EstreeJsxChild>} */ | ||
const cleanChildren = [] | ||
let index = -1 | ||
/** @type {Array<EstreeJsxChild>|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(child.expression.value) | ||
) { | ||
if (queue) queue.push(child) | ||
} else { | ||
if (queue) cleanChildren.push(...queue) | ||
cleanChildren.push(child) | ||
queue = [] | ||
} | ||
} | ||
return inherit(node, { | ||
type: 'JSXFragment', | ||
openingFragment: {type: 'JSXOpeningFragment'}, | ||
closingFragment: {type: 'JSXClosingFragment'}, | ||
children: cleanChildren | ||
}) | ||
} | ||
/** | ||
* @param {Text} node | ||
* @returns {EstreeJsxExpressionContainer|void} | ||
*/ | ||
function text(node) { | ||
const value = String(node.value || '') | ||
if (!value) return | ||
return create(node, { | ||
type: 'JSXExpressionContainer', | ||
expression: inherit(node, {type: 'Literal', value}) | ||
}) | ||
} | ||
/** | ||
* @param {Parent|MdxJsxFlowElement|MdxJsxTextElement} parent | ||
* @param {Context} context | ||
* @returns {Array<EstreeJsxChild>} | ||
*/ | ||
function all(parent, context) { | ||
const children = parent.children || [] | ||
let index = -1 | ||
/** @type {Array<EstreeJsxChild>} */ | ||
const results = [] | ||
// Currently, a warning is triggered by react for *any* white space in | ||
// tables. | ||
// So we remove the pretty lines for now. | ||
// See: <https://github.com/facebook/react/pull/7081>. | ||
// See: <https://github.com/facebook/react/pull/7515>. | ||
// See: <https://github.com/remarkjs/remark-react/issues/64>. | ||
const ignoreLineBreak = | ||
context.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 = context.handle(child, context) | ||
if (Array.isArray(result)) { | ||
results.push(...result) | ||
} else if (result) { | ||
results.push(result) | ||
} | ||
} | ||
return results | ||
} | ||
/** | ||
* Take positional info and data from `hast`. | ||
* | ||
* @template {EstreeNode|EstreeComment} T | ||
* @param {Node|MdxJsxAttributeValueExpression|MdxJsxAttribute|MdxJsxExpressionAttribute|MdxJsxFlowElement|MdxJsxTextElement|MdxFlowExpression|MdxTextExpression} hast | ||
* @param {T} esnode | ||
* @returns {T} | ||
*/ | ||
function inherit(hast, esnode) { | ||
const left = hast.data | ||
/** @type {Record<string, unknown>|undefined} */ | ||
let right | ||
/** @type {string} */ | ||
let key | ||
create(hast, esnode) | ||
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. | ||
esnode.data = right | ||
} | ||
} | ||
return esnode | ||
} | ||
/** | ||
* Just positional info. | ||
* | ||
* @template {EstreeNode|EstreeComment} T | ||
* @param {Node|MdxJsxAttributeValueExpression|MdxJsxAttribute|MdxJsxExpressionAttribute|MdxJsxFlowElement|MdxJsxTextElement|MdxFlowExpression|MdxTextExpression} hast | ||
* @param {T} esnode | ||
* @returns {T} | ||
*/ | ||
function create(hast, esnode) { | ||
const p = position(hast) | ||
if ( | ||
p.start.line && | ||
p.start.offset !== undefined && | ||
p.end.offset !== undefined | ||
) { | ||
// @ts-expect-error acorn-style. | ||
esnode.start = p.start.offset | ||
// @ts-expect-error acorn-style. | ||
esnode.end = p.end.offset | ||
esnode.loc = { | ||
start: {line: p.start.line, column: p.start.column - 1}, | ||
end: {line: p.end.line, column: p.end.column - 1} | ||
} | ||
esnode.range = [p.start.offset, p.end.offset] | ||
} | ||
return esnode | ||
} | ||
const createJsxName = | ||
/** | ||
* @type {( | ||
* ((name: string, attribute: true) => EstreeJsxAttributeName) & | ||
* ((name: string, attribute?: false) => EstreeJsxElementName) | ||
* )} | ||
*/ | ||
( | ||
/** | ||
* @param {string} name | ||
* @param {boolean} [attribute=false] | ||
* @returns {EstreeJsxElementName} | ||
*/ | ||
function (name, attribute) { | ||
if (!attribute && name.includes('.')) { | ||
const parts = name.split('.') | ||
let part = parts.shift() | ||
/** @type {JSXIdentifier|JSXMemberExpression} */ | ||
// @ts-expect-error: hush, the first is always defined. | ||
let node = {type: 'JSXIdentifier', name: part} | ||
while ((part = parts.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} | ||
} | ||
) | ||
/** | ||
* @param {string} value | ||
* @param {string} tagName | ||
* @returns {Record<string, string>} | ||
*/ | ||
function parseStyle(value, tagName) { | ||
/** @type {Record<string, string>} */ | ||
const result = {} | ||
try { | ||
style(value, iterator) | ||
} catch (error) { | ||
const exception = /** @type {Error} */ (error) | ||
exception.message = | ||
tagName + '[style]' + exception.message.slice('undefined'.length) | ||
throw error | ||
} | ||
return result | ||
/** | ||
* @param {string} name | ||
* @param {string} value | ||
* @returns {void} | ||
*/ | ||
function iterator(name, value) { | ||
if (name.slice(0, 4) === '-ms-') name = 'ms-' + name.slice(4) | ||
result[name.replace(/-([a-z])/g, styleReplacer)] = value | ||
} | ||
} | ||
/** | ||
* @param {string} _ | ||
* @param {string} $1 | ||
* @returns {string} | ||
*/ | ||
function styleReplacer(_, $1) { | ||
return $1.toUpperCase() | ||
} | ||
/** | ||
* Checks if the given string is a valid identifier name. | ||
* | ||
* @param {string} name | ||
* @returns {boolean} | ||
*/ | ||
function jsxIdentifierName(name) { | ||
let index = -1 | ||
while (++index < name.length) { | ||
if (!(index ? cont : identifierStart)(name.charCodeAt(index))) return false | ||
} | ||
// `false` if `name` is empty. | ||
return index > 0 | ||
/** | ||
* @param {number} code | ||
* @returns {boolean} | ||
*/ | ||
function cont(code) { | ||
return identifierCont(code) || code === 45 /* `-` */ | ||
} | ||
} |
{ | ||
"name": "hast-util-to-estree", | ||
"version": "2.1.0", | ||
"version": "2.2.0", | ||
"description": "hast utility to transform to estree (JavaScript AST) JSX", | ||
@@ -55,3 +55,3 @@ "license": "MIT", | ||
"space-separated-tokens": "^2.0.0", | ||
"style-to-object": "^0.3.0", | ||
"style-to-object": "^0.4.0", | ||
"unist-util-position": "^4.0.0", | ||
@@ -65,3 +65,4 @@ "zwitch": "^2.0.0" | ||
"@types/babel__core": "^7.0.0", | ||
"@types/tape": "^4.0.0", | ||
"@types/babel__generator": "^7.0.0", | ||
"@types/node": "^18.0.0", | ||
"@vue/babel-plugin-jsx": "^1.0.0", | ||
@@ -73,2 +74,3 @@ "acorn-jsx": "^5.0.0", | ||
"estree-util-to-js": "^1.0.0", | ||
"estree-walker": "^3.0.0", | ||
"hastscript": "^7.0.0", | ||
@@ -82,15 +84,13 @@ "mdast-util-from-markdown": "^1.0.0", | ||
"remark-preset-wooorm": "^9.0.0", | ||
"rimraf": "^3.0.0", | ||
"tape": "^5.0.0", | ||
"type-coverage": "^2.0.0", | ||
"typescript": "^4.0.0", | ||
"unist-util-visit": "^4.0.0", | ||
"xo": "^0.51.0" | ||
"xo": "^0.53.0" | ||
}, | ||
"scripts": { | ||
"prepack": "npm run build && npm run format", | ||
"build": "rimraf \"lib/**/*.d.ts\" \"*.d.ts\" && tsc && type-coverage", | ||
"build": "tsc --build --clean && tsc --build && type-coverage", | ||
"format": "remark . -qfo && prettier . -w --loglevel warn && xo --fix", | ||
"test-api": "node test.js", | ||
"test-coverage": "c8 --check-coverage --branches 100 --functions 100 --lines 100 --statements 100 --reporter lcov node test.js", | ||
"test-api": "node --conditions development test.js", | ||
"test-coverage": "c8 --check-coverage --100 --reporter lcov npm run test-api", | ||
"test": "npm run build && npm run format && npm run test-coverage" | ||
@@ -124,5 +124,6 @@ }, | ||
"ignoreFiles": [ | ||
"lib/index.d.ts" | ||
"lib/state.d.ts", | ||
"lib/state.js" | ||
] | ||
} | ||
} |
161
readme.md
@@ -20,3 +20,8 @@ # hast-util-to-estree | ||
* [API](#api) | ||
* [`toEstree(tree, options?)`](#toestreetree-options) | ||
* [`toEstree(tree[, options])`](#toestreetree-options) | ||
* [`defaultHandlers`](#defaulthandlers) | ||
* [`Handle`](#handle) | ||
* [`Options`](#options) | ||
* [`Space`](#space-1) | ||
* [`State`](#state) | ||
* [Types](#types) | ||
@@ -32,3 +37,3 @@ * [Compatibility](#compatibility) | ||
This package is a utility that takes a [hast][] (HTML) syntax tree as input and | ||
turns it into an [estree][] (JavaScript) syntax tree JSX extension. | ||
turns it into an [estree][] (JavaScript) syntax tree (with a JSX extension). | ||
This package also supports embedded MDX nodes. | ||
@@ -40,3 +45,3 @@ | ||
working with syntax trees. | ||
This us used in [MDX][]. | ||
This is used in [MDX][]. | ||
@@ -46,3 +51,3 @@ ## Install | ||
This package is [ESM only][esm]. | ||
In Node.js (version 12.20+, 14.14+, 16.0+, 18.0+), install with [npm][]: | ||
In Node.js (version 14.14+ and 16.0+), install with [npm][]: | ||
@@ -148,31 +153,32 @@ ```sh | ||
This package exports the identifier `toEstree`. | ||
This package exports the identifiers [`defaultHandlers`][defaulthandlers] and | ||
[`toEstree`][toestree]. | ||
There is no default export. | ||
### `toEstree(tree, options?)` | ||
### `toEstree(tree[, options])` | ||
Transform to [estree][] (JSX). | ||
Transform a hast tree (with embedded MDX nodes) into an estree (with JSX | ||
nodes). | ||
##### `options` | ||
###### Notes | ||
Configuration (optional). | ||
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. | ||
##### `options.space` | ||
###### Parameters | ||
Whether `tree` is in the HTML or SVG space (enum, `'svg'` or `'html'`, default: | ||
`'html'`). | ||
If an `svg` element is found when inside the HTML space, `toEstree` | ||
automatically switches to the SVG space when entering the element, and | ||
switches back when exiting | ||
* `tree` ([`HastNode`][hast-node]) | ||
— hast tree | ||
* `options` ([`Options`][options], optional) | ||
— configuration | ||
###### `options.handlers` | ||
###### Returns | ||
Object mapping node types to functions handling the corresponding nodes. | ||
See the code for examples. | ||
estree program node ([`Program`][program]). | ||
###### Returns | ||
The program’s last child in `body` is most likely an `ExpressionStatement`, | ||
whose expression is a `JSXFragment` or a `JSXElement`. | ||
Node ([`Program`][program]) whose 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 | ||
@@ -184,18 +190,93 @@ embedded MDX nodes in the HTML (when [`mdast-util-mdx`][mdast-util-mdx] is used | ||
###### Note | ||
Comments are both attached to the tree in their neighbouring nodes (recast and | ||
babel style), and added as a `comments` array on the program node (espree | ||
style). | ||
You may have to do `program.comments = null` for certain compilers. | ||
There aren’t many great estree serializers out there that support JSX. | ||
To do that, you can use [`estree-util-to-js`][estree-util-to-js]. | ||
Or, use [`estree-util-build-jsx`][build-jsx] to turn JSX into function | ||
calls, and then serialize with whatever (astring, escodegen). | ||
calls, and then serialize with whatever (`astring`, `escodegen`). | ||
### `defaultHandlers` | ||
Default handlers for elements (`Record<string, Handle>`). | ||
Each key is an element name, each value is a [`Handle`][handle]. | ||
### `Handle` | ||
Turn a hast node into an estree node (TypeScript type). | ||
###### Parameters | ||
* `node` ([`HastNode`][hast-node]) | ||
— expected hast node | ||
* `state` ([`State`][state]) | ||
— info passed around about the current state | ||
###### Returns | ||
JSX child (`JsxChild`, optional). | ||
You can also add more results to `state.esm` and `state.comments`. | ||
### `Options` | ||
Configuration (TypeScript type). | ||
##### Fields | ||
###### `space` | ||
Which space the document is in ([`Space`][space], 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. | ||
###### `handlers` | ||
Object mapping node types to functions handling the corresponding nodes | ||
(`Record<string, Handle>`, optional). | ||
Merged into the defaults. | ||
See [`Handle`][handle]. | ||
### `Space` | ||
Namespace (TypeScript type). | ||
###### Type | ||
```ts | ||
type Space = 'html' | 'svg' | ||
``` | ||
### `State` | ||
Info passed around about the current state (TypeScript type). | ||
###### Fields | ||
* `schema` ([`Schema`][schema]) | ||
— current schema | ||
* `comments` (`Array<EstreeComment>`) | ||
— list of estree comments | ||
* `esm` (`Array<EstreeNode>`) | ||
— list of top-level estree nodes | ||
* `handle` (`(node: HastNode) => EstreeJsxChild | void`) | ||
— transform a hast node to estree | ||
* `handle` (`(node: HastParent) => EstreeJsxChild | void`) | ||
— transform children of a hast parent to estree | ||
* `patch` (`(from: HastNode, to: EstreeNode) => void`) | ||
— take positional info from `from` (use `inherit` if you also want data) | ||
* `inherit` (`(from: HastNode, to: EstreeNode) => void`) | ||
— take positional info and data from `from` (use `patch` if you don’t want | ||
data) | ||
* `createJsxAttributeName` (`(name: string) => EstreeJsxAttributeName`) | ||
— create a JSX attribute name | ||
* `createJsxElementName` (`(name: string) => EstreeJsxElementName`) | ||
— create a JSX attribute name | ||
## Types | ||
This package is fully typed with [TypeScript][]. | ||
It exports the additional types `Options`, `Space`, and `Handle`. | ||
It exports the additional types [`Handle`][handle], [`Options`][options], | ||
[`Space`][space], and [`State`][state]. | ||
@@ -206,3 +287,3 @@ ## Compatibility | ||
versions of Node.js. | ||
As of now, that is Node.js 12.20+, 14.14+, 16.0+, and 18.0+. | ||
As of now, that is Node.js 14.14+ and 16.0+. | ||
Our projects sometimes work with older versions, but this is not guaranteed. | ||
@@ -290,2 +371,4 @@ | ||
[hast-node]: https://github.com/syntax-tree/hast#nodes | ||
[estree]: https://github.com/estree/estree | ||
@@ -301,2 +384,16 @@ | ||
[schema]: https://github.com/wooorm/property-information#api | ||
[mdx]: https://mdxjs.com | ||
[defaulthandlers]: #defaulthandlers | ||
[toestree]: #toestreetree-options | ||
[space]: #space-1 | ||
[options]: #options | ||
[handle]: #handle | ||
[state]: #state |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
57492
25
1324
391
+ Addedstyle-to-object@0.4.4(transitive)
- Removedstyle-to-object@0.3.0(transitive)
Updatedstyle-to-object@^0.4.0