Skip to content

Commit 982a3c0

Browse files
committed
Add utilities to state
This commit adds several useful utilities to the passed `state`. It also refactors the code so that each handler is in its own file.
1 parent 58b61c9 commit 982a3c0

File tree

12 files changed

+1022
-861
lines changed

12 files changed

+1022
-861
lines changed

index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
2-
* @typedef {import('./lib/index.js').Handle} Handle
3-
* @typedef {import('./lib/index.js').Space} Space
2+
* @typedef {import('./lib/state.js').Handle} Handle
3+
* @typedef {import('./lib/state.js').Space} Space
44
* @typedef {import('./lib/index.js').Options} Options
55
*/
66

lib/handlers/comment.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* @typedef {import('estree').Comment} Comment
3+
* @typedef {import('estree-jsx').JSXExpressionContainer} JsxExpressionContainer
4+
* @typedef {import('estree-jsx').JSXEmptyExpression} JsxEmptyExpression
5+
* @typedef {import('../state.js').State} State
6+
*/
7+
8+
/**
9+
* Turn a hast comment into an estree node.
10+
*
11+
* @param {import('hast').Comment} node
12+
* hast node to transform.
13+
* @param {State} state
14+
* Info passed around about the current state.
15+
* @returns {JsxExpressionContainer}
16+
* estree expression.
17+
*/
18+
export function comment(node, state) {
19+
/** @type {Comment} */
20+
const result = {type: 'Block', value: node.value}
21+
state.inherit(node, result)
22+
state.comments.push(result)
23+
24+
/** @type {JsxEmptyExpression} */
25+
const expression = {
26+
type: 'JSXEmptyExpression',
27+
// @ts-expect-error: `comments` is custom.
28+
comments: [Object.assign({}, result, {leading: false, trailing: true})]
29+
}
30+
state.patch(node, expression)
31+
32+
/** @type {JsxExpressionContainer} */
33+
const container = {type: 'JSXExpressionContainer', expression}
34+
state.patch(node, container)
35+
return container
36+
}

lib/handlers/element.js

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
/**
2+
* @typedef {import('hast').Element} Element
3+
* @typedef {import('estree').Property} Property
4+
* @typedef {import('estree-jsx').JSXElement} JsxElement
5+
* @typedef {import('estree-jsx').JSXSpreadAttribute} JsxSpreadAttribute
6+
* @typedef {import('estree-jsx').JSXAttribute} JsxAttribute
7+
* @typedef {import('../state.js').State} State
8+
*/
9+
10+
import {stringify as commas} from 'comma-separated-tokens'
11+
import {svg, find, hastToReact} from 'property-information'
12+
import {stringify as spaces} from 'space-separated-tokens'
13+
import {
14+
start as identifierStart,
15+
cont as identifierCont
16+
} from 'estree-util-is-identifier-name'
17+
// @ts-expect-error: `style-to-object` doesn’t support actual ESM + TS correctly.
18+
import styleToObject from 'style-to-object'
19+
20+
/** @type {(value: string, iterator?: (property: string, value: string, declaration: unknown) => void) => Record<string, string>} */
21+
const style = styleToObject
22+
23+
const own = {}.hasOwnProperty
24+
25+
/**
26+
* Turn a hast element into an estree node.
27+
*
28+
* @param {Element} node
29+
* hast node to transform.
30+
* @param {State} state
31+
* Info passed around about the current state.
32+
* @returns {JsxElement}
33+
* estree expression.
34+
*/
35+
// eslint-disable-next-line complexity
36+
export function element(node, state) {
37+
const parentSchema = state.schema
38+
let schema = parentSchema
39+
const props = node.properties || {}
40+
41+
if (parentSchema.space === 'html' && node.tagName.toLowerCase() === 'svg') {
42+
schema = svg
43+
state.schema = schema
44+
}
45+
46+
const children = state.all(node)
47+
48+
/** @type {Array<JsxAttribute | JsxSpreadAttribute>} */
49+
const attributes = []
50+
/** @type {string} */
51+
let prop
52+
53+
for (prop in props) {
54+
if (own.call(props, prop)) {
55+
let value = props[prop]
56+
const info = find(schema, prop)
57+
/** @type {JsxAttribute['value']} */
58+
let attributeValue
59+
60+
// Ignore nullish and `NaN` values.
61+
// Ignore `false` and falsey known booleans.
62+
if (
63+
value === undefined ||
64+
value === null ||
65+
(typeof value === 'number' && Number.isNaN(value)) ||
66+
value === false ||
67+
(!value && info.boolean)
68+
) {
69+
continue
70+
}
71+
72+
prop = info.space
73+
? hastToReact[info.property] || info.property
74+
: info.attribute
75+
76+
if (Array.isArray(value)) {
77+
// Accept `array`.
78+
// Most props are space-separated.
79+
value = info.commaSeparated ? commas(value) : spaces(value)
80+
}
81+
82+
if (prop === 'style') {
83+
/** @type {Record<string, string>} */
84+
// @ts-expect-error Assume `value` is an object otherwise.
85+
const styleValue =
86+
typeof value === 'string' ? parseStyle(value, node.tagName) : value
87+
88+
/** @type {Array<Property>} */
89+
const cssProperties = []
90+
/** @type {string} */
91+
let cssProp
92+
93+
for (cssProp in styleValue) {
94+
// eslint-disable-next-line max-depth
95+
if (own.call(styleValue, cssProp)) {
96+
cssProperties.push({
97+
type: 'Property',
98+
method: false,
99+
shorthand: false,
100+
computed: false,
101+
key: {type: 'Identifier', name: cssProp},
102+
value: {type: 'Literal', value: String(styleValue[cssProp])},
103+
kind: 'init'
104+
})
105+
}
106+
}
107+
108+
attributeValue = {
109+
type: 'JSXExpressionContainer',
110+
expression: {type: 'ObjectExpression', properties: cssProperties}
111+
}
112+
} else if (value === true) {
113+
attributeValue = null
114+
} else {
115+
attributeValue = {type: 'Literal', value: String(value)}
116+
}
117+
118+
if (jsxIdentifierName(prop)) {
119+
attributes.push({
120+
type: 'JSXAttribute',
121+
name: {type: 'JSXIdentifier', name: prop},
122+
value: attributeValue
123+
})
124+
} else {
125+
attributes.push({
126+
type: 'JSXSpreadAttribute',
127+
argument: {
128+
type: 'ObjectExpression',
129+
properties: [
130+
{
131+
type: 'Property',
132+
method: false,
133+
shorthand: false,
134+
computed: false,
135+
key: {type: 'Literal', value: String(prop)},
136+
// @ts-expect-error No need to worry about `style` (which has a
137+
// `JSXExpressionContainer` value) because that’s a valid identifier.
138+
value: attributeValue || {type: 'Literal', value: true},
139+
kind: 'init'
140+
}
141+
]
142+
}
143+
})
144+
}
145+
}
146+
}
147+
148+
// Restore parent schema.
149+
state.schema = parentSchema
150+
151+
/** @type {JsxElement} */
152+
const result = {
153+
type: 'JSXElement',
154+
openingElement: {
155+
type: 'JSXOpeningElement',
156+
attributes,
157+
name: state.createJsxElementName(node.tagName),
158+
selfClosing: children.length === 0
159+
},
160+
closingElement:
161+
children.length > 0
162+
? {
163+
type: 'JSXClosingElement',
164+
name: state.createJsxElementName(node.tagName)
165+
}
166+
: null,
167+
children
168+
}
169+
state.inherit(node, result)
170+
return result
171+
}
172+
173+
/**
174+
* Parse CSS rules as a declaration.
175+
*
176+
* @param {string} value
177+
* CSS text.
178+
* @param {string} tagName
179+
* Element name.
180+
* @returns {Record<string, string>}
181+
* Props.
182+
*/
183+
function parseStyle(value, tagName) {
184+
/** @type {Record<string, string>} */
185+
const result = {}
186+
187+
try {
188+
style(value, iterator)
189+
} catch (error) {
190+
const exception = /** @type {Error} */ (error)
191+
exception.message =
192+
tagName + '[style]' + exception.message.slice('undefined'.length)
193+
throw error
194+
}
195+
196+
return result
197+
198+
/**
199+
* Add `name`, as a CSS prop, to `result`.
200+
*
201+
* @param {string} name
202+
* Key.
203+
* @param {string} value
204+
* Value.
205+
* @returns {void}
206+
* Nothing.
207+
*/
208+
function iterator(name, value) {
209+
if (name.slice(0, 4) === '-ms-') name = 'ms-' + name.slice(4)
210+
result[name.replace(/-([a-z])/g, styleReplacer)] = value
211+
}
212+
}
213+
214+
/**
215+
* Uppercase `$1`.
216+
*
217+
* @param {string} _
218+
* Whatever.
219+
* @param {string} $1
220+
* an ASCII alphabetic.
221+
* @returns {string}
222+
* Uppercased `$1`.
223+
*/
224+
function styleReplacer(_, $1) {
225+
return $1.toUpperCase()
226+
}
227+
228+
/**
229+
* Checks if the given string is a valid identifier name.
230+
*
231+
* Allows dashes, so it’s actually JSX identifier names.
232+
*
233+
* @param {string} name
234+
* Whatever.
235+
* @returns {boolean}
236+
* Whether `name` is a valid JSX identifier.
237+
*/
238+
function jsxIdentifierName(name) {
239+
let index = -1
240+
241+
while (++index < name.length) {
242+
if (!(index ? cont : identifierStart)(name.charCodeAt(index))) return false
243+
}
244+
245+
// `false` if `name` is empty.
246+
return index > 0
247+
248+
/**
249+
* @param {number} code
250+
* @returns {boolean}
251+
*/
252+
function cont(code) {
253+
return identifierCont(code) || code === 45 /* `-` */
254+
}
255+
}

lib/handlers/index.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import {comment} from './comment.js'
2+
import {element} from './element.js'
3+
import {mdxExpression} from './mdx-expression.js'
4+
import {mdxJsxElement} from './mdx-jsx-element.js'
5+
import {mdxjsEsm} from './mdxjs-esm.js'
6+
import {text} from './text.js'
7+
import {root} from './root.js'
8+
9+
export const handlers = {
10+
comment,
11+
doctype: ignore,
12+
element,
13+
mdxFlowExpression: mdxExpression,
14+
mdxTextExpression: mdxExpression,
15+
mdxJsxFlowElement: mdxJsxElement,
16+
mdxJsxTextElement: mdxJsxElement,
17+
mdxjsEsm,
18+
text,
19+
root
20+
}
21+
22+
/**
23+
* Handle a node that is ignored.
24+
*
25+
* @returns {void}
26+
* Nothing.
27+
*/
28+
function ignore() {}

lib/handlers/mdx-expression.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/**
2+
* @typedef {import('mdast-util-mdx-expression').MdxFlowExpression} MdxFlowExpression
3+
* @typedef {import('mdast-util-mdx-expression').MdxTextExpression} MdxTextExpression
4+
* @typedef {import('estree').Expression} Expression
5+
* @typedef {import('estree-jsx').JSXEmptyExpression} JsxEmptyExpression
6+
* @typedef {import('estree-jsx').JSXExpressionContainer} JsxExpressionContainer
7+
* @typedef {import('../state.js').State} State
8+
*/
9+
10+
import {attachComments} from 'estree-util-attach-comments'
11+
12+
/**
13+
* Turn an MDX expression node into an estree node.
14+
*
15+
* @param {MdxFlowExpression | MdxTextExpression} node
16+
* hast node to transform.
17+
* @param {State} state
18+
* Info passed around about the current state.
19+
* @returns {JsxExpressionContainer}
20+
* estree expression.
21+
*/
22+
export function mdxExpression(node, state) {
23+
const estree = node.data && node.data.estree
24+
const comments = (estree && estree.comments) || []
25+
/** @type {Expression | JsxEmptyExpression | undefined} */
26+
let expression
27+
28+
if (estree) {
29+
state.comments.push(...comments)
30+
attachComments(estree, estree.comments)
31+
expression =
32+
(estree.body[0] &&
33+
estree.body[0].type === 'ExpressionStatement' &&
34+
estree.body[0].expression) ||
35+
undefined
36+
}
37+
38+
if (!expression) {
39+
expression = {type: 'JSXEmptyExpression'}
40+
state.patch(node, expression)
41+
}
42+
43+
/** @type {JsxExpressionContainer} */
44+
const result = {type: 'JSXExpressionContainer', expression}
45+
state.inherit(node, result)
46+
return result
47+
}

0 commit comments

Comments
 (0)