Skip to content

Commit

Permalink
Update @types/hast, utilities
Browse files Browse the repository at this point in the history
  • Loading branch information
wooorm committed Aug 4, 2023
1 parent 771379a commit ed479a5
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 50 deletions.
84 changes: 49 additions & 35 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
/**
* @typedef {import('property-information').Schema} Schema
* @typedef {import('hast').Content} Content
* @typedef {import('hast').Nodes} Nodes
* @typedef {import('hast').Parents} Parents
* @typedef {import('hast').Element} Element
* @typedef {import('hast').Root} Root
* @typedef {import('./components.js').Components} Components
*/

/**
* @typedef {Content | Root} Node
* @typedef {Extract<Node, import('unist').Parent>} Parent
*/

/**
* @typedef {unknown} Fragment
* Represent the children, typically a symbol.
Expand Down Expand Up @@ -91,7 +86,7 @@
*
* @callback Create
* Create something in development or production.
* @param {Node} node
* @param {Nodes} node
* hast node.
* @param {unknown} type
* Fragment symbol or tag name.
Expand All @@ -106,6 +101,8 @@
* Info passed around.
* @property {string | undefined} filePath
* File path.
* @property {Array<Parents>} ancestors
* Stack of parents.
* @property {Partial<Components>} components
* Components to swap.
* @property {boolean} passKeys
Expand Down Expand Up @@ -232,7 +229,7 @@ const tableElements = new Set(['table', 'thead', 'tbody', 'tfoot', 'tr'])
* Transform a hast tree to preact, react, solid, svelte, vue, etc.,
* with an automatic JSX runtime.
*
* @param {Node} tree
* @param {Nodes} tree
* Tree to transform.
* @param {Options} options
* Configuration (required).
Expand Down Expand Up @@ -272,6 +269,7 @@ export function toJsxRuntime(tree, options) {
/** @type {State} */
const state = {
Fragment: options.Fragment,
ancestors: [],
schema: options.space === 'svg' ? svg : html,
passKeys: options.passKeys !== false,
passNode: options.passNode || false,
Expand Down Expand Up @@ -303,7 +301,7 @@ export function toJsxRuntime(tree, options) {
*
* @param {State} state
* Info passed around.
* @param {Node} node
* @param {Nodes} node
* Current node.
* @param {string | undefined} key
* Key.
Expand All @@ -324,13 +322,19 @@ function one(state, node, key) {
state.schema = schema
}

state.ancestors.push(node)

let children = createChildren(state, node)
const props = createProperties(state, node)
const props = createProperties(state, state.ancestors)
let type = state.Fragment

state.ancestors.pop()

if (node.type === 'element') {
if (children && tableElements.has(node.tagName)) {
children = children.filter((child) => !whitespace(child))
children = children.filter(
(child) => typeof child !== 'string' || !whitespace(child)
)
}

if (own.call(state.components, node.tagName)) {
Expand Down Expand Up @@ -408,8 +412,8 @@ function developmentCreate(filePath, jsxDEV) {
isStaticChildren,
{
fileName: filePath,
lineNumber: point.line === null ? undefined : point.line,
columnNumber: point.column === null ? undefined : point.column - 1
lineNumber: point ? point.line : undefined,
columnNumber: point ? point.column - 1 : undefined
},
undefined
)
Expand All @@ -421,7 +425,7 @@ function developmentCreate(filePath, jsxDEV) {
*
* @param {State} state
* Info passed around.
* @param {Parent} node
* @param {Parents} node
* Current element.
* @returns {Array<Child>}
* Children.
Expand Down Expand Up @@ -458,12 +462,13 @@ function createChildren(state, node) {
*
* @param {State} state
* Info passed around.
* @param {Parent} node
* Current element.
* @param {Array<Parents>} ancestors
* Stack of parents.
* @returns {Props}
* Props for runtime.
*/
function createProperties(state, node) {
function createProperties(state, ancestors) {
const node = ancestors[ancestors.length - 1]
/** @type {Props} */
const props = {}
/** @type {string} */
Expand All @@ -472,7 +477,12 @@ function createProperties(state, node) {
if ('properties' in node && node.properties) {
for (prop in node.properties) {
if (prop !== 'children' && own.call(node.properties, prop)) {
const result = createProperty(state, node, prop, node.properties[prop])
const result = createProperty(
state,
ancestors,
prop,
node.properties[prop]
)

if (result) {
props[result[0]] = result[1]
Expand All @@ -489,16 +499,16 @@ function createProperties(state, node) {
*
* @param {State} state
* Info passed around.
* @param {Element} node
* Current element.
* @param {Array<Parents>} ancestors
* Stack of parents.
* @param {string} prop
* Key.
* @param {Array<string | number> | string | number | boolean | null | undefined} value
* hast property value.
* @returns {Field | void}
* Field for runtime, optional.
*/
function createProperty(state, node, prop, value) {
function createProperty(state, ancestors, prop, value) {
const info = find(state.schema, prop)

// Ignore nullish and `NaN` values.
Expand All @@ -519,7 +529,9 @@ function createProperty(state, node, prop, value) {
// React only accepts `style` as object.
if (info.property === 'style') {
let styleObject =
typeof value === 'object' ? value : parseStyle(state, node, String(value))
typeof value === 'object'
? value
: parseStyle(state, ancestors, String(value))

if (state.stylePropertyNameCase === 'css') {
styleObject = transformStyleToCssCasing(styleObject)
Expand All @@ -541,31 +553,33 @@ function createProperty(state, node, prop, value) {
*
* @param {State} state
* Info passed around.
* @param {Element} node
* Current element.
* @param {Array<Nodes>} ancestors
* Stack of nodes.
* @param {string} value
* CSS declarations.
* @returns {Style}
* Properties.
* @throws
* Throws `VFileMessage` when CSS cannot be parsed.
*/
function parseStyle(state, node, value) {
function parseStyle(state, ancestors, value) {
/** @type {Style} */
const result = {}

try {
styleToObject(value, replacer)
} catch (error_) {
const error = /** @type {Error} */ (error_)
const cleanMessage = error.message.replace(/^undefined:\d+:\d+: /, '')

const message = new VFileMessage(
'Cannot parse style attribute: ' + cleanMessage,
node,
'hast-util-to-jsx-runtime:style'
)
message.file = state.filePath || null
const cause = /** @type {Error} */ (error_)

const message = new VFileMessage('Cannot parse `style` attribute', {
ancestors,
cause,
source: 'hast-util-to-jsx-runtime',
ruleId: 'style'
})
message.file = state.filePath || undefined
message.url =
'https://github.com/syntax-tree/hast-util-to-jsx-runtime#cannot-parse-style-attribute'

throw message
}
Expand Down
15 changes: 8 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,23 +36,23 @@
"index.js"
],
"dependencies": {
"@types/hast": "^2.0.0",
"@types/unist": "^2.0.0",
"@types/hast": "^3.0.0",
"@types/unist": "^3.0.0",
"comma-separated-tokens": "^2.0.0",
"hast-util-whitespace": "^2.0.0",
"hast-util-whitespace": "^3.0.0",
"property-information": "^6.0.0",
"space-separated-tokens": "^2.0.0",
"style-to-object": "^0.4.1",
"unist-util-position": "^4.0.0",
"vfile-message": "^3.0.0"
"style-to-object": "^0.4.0",
"unist-util-position": "^5.0.0",
"vfile-message": "^4.0.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"c8": "^8.0.0",
"esbuild": "^0.18.0",
"hastscript": "^7.0.0",
"hastscript": "^8.0.0",
"prettier": "^3.0.0",
"react": "^18.0.0",
"react-dom": "^18.0.0",
Expand Down Expand Up @@ -84,6 +84,7 @@
"#": "`n` is wrong",
"rules": {
"n/file-extension-in-import": "off",
"unicorn/prefer-at": "off",
"unicorn/prefer-string-replace-all": "off"
}
},
Expand Down
51 changes: 49 additions & 2 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,17 +113,64 @@ There is no default export.
Transform a hast tree to preact, react, solid, svelte, vue, etc., with an
automatic JSX runtime.

###### Parameters
##### Parameters

* `tree` ([`Node`][node])
— tree to transform
* `options` ([`Options`][api-options], required)
— configuration

###### Returns
##### Returns

Result from your configured JSX runtime (`JSX.Element`).

##### Throws

The following errors are thrown:

###### ``Expected `Fragment` in options``

This error is thrown when either `options` is not passed at all or
when `options.Fragment` is `undefined`.

The automatic JSX runtime needs a symbol for a fragment to work.

To solve the error, make sure you are passing the correct fragment symbol from
your framework.

###### `` Expected `jsxDEV` in options when `development: true` ``

This error is thrown when `options.development` is turned on (`true`), but when
`options.jsxDEV` is not a function.

The automatic JSX runtime, in development, needs this function.

To solve the error, make sure you are importing the correct runtime functions
(for example, `'react/jsx-dev-runtime'`), and pass `jsxDEV`.

###### ``Expected `jsx` in production options``

###### ``Expected `jsxs` in production options``

These errors are thrown when `options.development` is *not* turned on (`false`
or not defined), and when `options.jsx` or `options.jsxs` are not functions.

The automatic JSX runtime, in production, needs these functions.

To solve the error, make sure you are importing the correct runtime functions
(for example, `'react/jsx-runtime'`), and pass `jsx` and `jsxs`.

###### ``Cannot parse `style` attribute``

This error is thrown when a `style` attribute is found on an element, which
cannot be parsed as CSS.

Most frameworks don’t accept `style` as a string, so we need to parse it as
CSS, and pass it as an object.
But when broken CSS is used, such as `style="color:red; /*"`, we crash.

To solve the error, make sure authors write valid CSS.

### `Options`

Configuration (TypeScript type).
Expand Down
10 changes: 4 additions & 6 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,15 +106,13 @@ test('core', () => {
)

assert.equal(
// @ts-expect-error: Types are out of date, `name` is no longer used.
// type-coverage:ignore-next-line
toJsxRuntime({type: 'doctype'}, production).type,
production.Fragment,
'should support a doctype (1)'
)

assert.equal(
// @ts-expect-error: Types are out of date, `name` is no longer used.
renderToStaticMarkup(toJsxRuntime({type: 'doctype'}, production)),
'',
'should support a doctype (2)'
Expand Down Expand Up @@ -262,7 +260,7 @@ test('properties', () => {
() => {
toJsxRuntime(h('div', {style: 'color:red; /*'}), production)
},
/^1:1-1:1: Cannot parse style attribute: End of comment missing$/,
/Cannot parse `style` attribute/,
'should crash on invalid style strings (default)'
)

Expand All @@ -279,7 +277,7 @@ test('properties', () => {
production
)
},
/^3:2-3:123: Cannot parse style attribute: End of comment missing$/,
/^3:2-3:123: Cannot parse `style` attribute/,
'Cannot parse style attribute: End of comment missing'
)

Expand All @@ -295,7 +293,7 @@ test('properties', () => {
{...production, filePath: 'example.html'}
)
},
/^1:1-1:1: Cannot parse style attribute: End of comment missing$/,
/^1:1: Cannot parse `style` attribut/,
'should crash on invalid style strings (w/ file path, w/o positional info)'
)

Expand All @@ -312,7 +310,7 @@ test('properties', () => {
{...production, filePath: 'example.html'}
)
},
/^3:2-3:123: Cannot parse style attribute: End of comment missing$/,
/^3:2-3:123: Cannot parse `style` attribute/,
'should crash on invalid style strings (w/ file path, w/ positional info)'
)

Expand Down

0 comments on commit ed479a5

Please sign in to comment.