diff --git a/.editorconfig b/.editorconfig index c6c8b36..0f17867 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,9 +1,9 @@ root = true [*] -indent_style = space -indent_size = 2 -end_of_line = lf charset = utf-8 -trim_trailing_whitespace = true +end_of_line = lf +indent_size = 2 +indent_style = space insert_final_newline = true +trim_trailing_whitespace = true diff --git a/.github/workflows/bb.yml b/.github/workflows/bb.yml index 0198fc3..3dbfce5 100644 --- a/.github/workflows/bb.yml +++ b/.github/workflows/bb.yml @@ -1,9 +1,3 @@ -name: bb -on: - issues: - types: [opened, reopened, edited, closed, labeled, unlabeled] - pull_request_target: - types: [opened, reopened, edited, closed, labeled, unlabeled] jobs: main: runs-on: ubuntu-latest @@ -11,3 +5,9 @@ jobs: - uses: unifiedjs/beep-boop-beta@main with: repo-token: ${{secrets.GITHUB_TOKEN}} +name: bb +on: + issues: + types: [closed, edited, labeled, opened, reopened, unlabeled] + pull_request_target: + types: [closed, edited, labeled, opened, reopened, unlabeled] diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fe284ad..ade3921 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,21 +1,21 @@ -name: main -on: - - pull_request - - push jobs: main: name: ${{matrix.node}} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: dcodeIO/setup-node-nvm@master + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: ${{matrix.node}} - run: npm install - run: npm test - - uses: codecov/codecov-action@v1 + - uses: codecov/codecov-action@v5 strategy: matrix: node: - - lts/erbium + - lts/hydrogen - node +name: main +on: + - pull_request + - push diff --git a/.gitignore b/.gitignore index affeb21..908c1f3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,13 @@ -.DS_Store +*.d.ts *.log +*.map +*.tsbuildinfo +.DS_Store coverage/ node_modules/ test/jsx-*.js yarn.lock -**/*.d.ts -!lib/jsx-classic.d.ts -!lib/jsx-automatic.d.ts +!/lib/automatic-runtime-html.d.ts +!/lib/automatic-runtime-svg.d.ts +!/lib/jsx-automatic.d.ts +!/lib/jsx-classic.d.ts diff --git a/.npmrc b/.npmrc index 43c97e7..3757b30 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,2 @@ +ignore-scripts=true package-lock=false diff --git a/.prettierignore b/.prettierignore index cebe81f..8d01c59 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,2 +1,2 @@ -coverage/ *.md +coverage/ diff --git a/html.js b/html.js deleted file mode 100644 index 2abe80c..0000000 --- a/html.js +++ /dev/null @@ -1,6 +0,0 @@ -/** - * @typedef {import('./lib/index.js').Child} Child Acceptable child value - * @typedef {import('./lib/index.js').Properties} Properties Acceptable properties value. - */ - -export {h} from './lib/html.js' diff --git a/html/jsx-runtime.js b/html/jsx-runtime.js deleted file mode 100644 index 4777df1..0000000 --- a/html/jsx-runtime.js +++ /dev/null @@ -1,5 +0,0 @@ -/** - * @typedef {import('../lib/runtime.js').JSXProps}} JSXProps - */ - -export * from '../lib/runtime-html.js' diff --git a/index.js b/index.js index c5b0522..61c9def 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,7 @@ /** - * @typedef {import('./lib/index.js').Child} Child Acceptable child value - * @typedef {import('./lib/index.js').Properties} Properties Acceptable properties value. + * @typedef {import('./lib/create-h.js').Child} Child + * @typedef {import('./lib/create-h.js').Properties} Properties + * @typedef {import('./lib/create-h.js').Result} Result */ export {h, s} from './lib/index.js' diff --git a/jsx-runtime.js b/jsx-runtime.js deleted file mode 100644 index 0bf51b2..0000000 --- a/jsx-runtime.js +++ /dev/null @@ -1,5 +0,0 @@ -/** - * @typedef {import('./lib/runtime.js').JSXProps}} JSXProps - */ - -export * from './lib/runtime-html.js' diff --git a/lib/automatic-runtime-html.d.ts b/lib/automatic-runtime-html.d.ts new file mode 100644 index 0000000..cdaa7b4 --- /dev/null +++ b/lib/automatic-runtime-html.d.ts @@ -0,0 +1,36 @@ +/* eslint-disable @typescript-eslint/naming-convention */ + +import type {Element, Root} from 'hast' +import type {Child} from './create-h.js' +import type {JSXProps} from './create-automatic-runtime.js' + +export * from './jsx-automatic.js' + +export const Fragment: null + +export const jsxDEV: { + ( + type: null, + properties: {children?: Child}, + key?: string | null | undefined + ): Root + (type: string, properties: JSXProps, key?: string | null | undefined): Element +} + +export const jsxs: { + ( + type: null, + properties: {children?: Child}, + key?: string | null | undefined + ): Root + (type: string, properties: JSXProps, key?: string | null | undefined): Element +} + +export const jsx: { + ( + type: null, + properties: {children?: Child}, + key?: string | null | undefined + ): Root + (type: string, properties: JSXProps, key?: string | null | undefined): Element +} diff --git a/lib/automatic-runtime-html.js b/lib/automatic-runtime-html.js new file mode 100644 index 0000000..d03c053 --- /dev/null +++ b/lib/automatic-runtime-html.js @@ -0,0 +1,9 @@ +// Note: types exposed from `automatic-runtime-html.d.ts` because TS has bugs +// when generating types. +import {createAutomaticRuntime} from './create-automatic-runtime.js' +import {h} from './index.js' + +// Export `JSX` as a global for TypeScript. +export * from './jsx-automatic.js' + +export const {Fragment, jsxDEV, jsxs, jsx} = createAutomaticRuntime(h) diff --git a/lib/automatic-runtime-svg.d.ts b/lib/automatic-runtime-svg.d.ts new file mode 100644 index 0000000..cdaa7b4 --- /dev/null +++ b/lib/automatic-runtime-svg.d.ts @@ -0,0 +1,36 @@ +/* eslint-disable @typescript-eslint/naming-convention */ + +import type {Element, Root} from 'hast' +import type {Child} from './create-h.js' +import type {JSXProps} from './create-automatic-runtime.js' + +export * from './jsx-automatic.js' + +export const Fragment: null + +export const jsxDEV: { + ( + type: null, + properties: {children?: Child}, + key?: string | null | undefined + ): Root + (type: string, properties: JSXProps, key?: string | null | undefined): Element +} + +export const jsxs: { + ( + type: null, + properties: {children?: Child}, + key?: string | null | undefined + ): Root + (type: string, properties: JSXProps, key?: string | null | undefined): Element +} + +export const jsx: { + ( + type: null, + properties: {children?: Child}, + key?: string | null | undefined + ): Root + (type: string, properties: JSXProps, key?: string | null | undefined): Element +} diff --git a/lib/automatic-runtime-svg.js b/lib/automatic-runtime-svg.js new file mode 100644 index 0000000..968ba7f --- /dev/null +++ b/lib/automatic-runtime-svg.js @@ -0,0 +1,7 @@ +import {createAutomaticRuntime} from './create-automatic-runtime.js' +import {s} from './index.js' + +// Export `JSX` as a global for TypeScript. +export * from './jsx-automatic.js' + +export const {Fragment, jsxDEV, jsxs, jsx} = createAutomaticRuntime(s) diff --git a/lib/core.js b/lib/core.js deleted file mode 100644 index d1d22f5..0000000 --- a/lib/core.js +++ /dev/null @@ -1,287 +0,0 @@ -/** - * @typedef {import('hast').Root} Root - * @typedef {import('hast').Element} Element - * @typedef {import('hast').Properties} Properties - * @typedef {Root['children'][number]} Child - * @typedef {Child|Root} Node - * @typedef {import('property-information').Info} Info - * @typedef {import('property-information').Schema} Schema - * - * @typedef {Root|Element} HResult - * @typedef {string|number} HStyleValue - * @typedef {Object.} HStyle - * @typedef {string|number|boolean|null|undefined} HPrimitiveValue - * @typedef {Array.} HArrayValue - * @typedef {HPrimitiveValue|HArrayValue} HPropertyValue - * @typedef {{[property: string]: HPropertyValue|HStyle}} HProperties - * - * @typedef {string|number|null|undefined} HPrimitiveChild - * @typedef {Array.} HArrayChild - * @typedef {Node|HPrimitiveChild|HArrayChild} HChild - */ - -import {find, normalize} from 'property-information' -import {parseSelector} from 'hast-util-parse-selector' -import {parse as spaces} from 'space-separated-tokens' -import {parse as commas} from 'comma-separated-tokens' - -const buttonTypes = new Set(['menu', 'submit', 'reset', 'button']) - -const own = {}.hasOwnProperty - -/** - * @param {Schema} schema - * @param {string} defaultTagName - * @param {Array.} [caseSensitive] - */ -export function core(schema, defaultTagName, caseSensitive) { - const adjust = caseSensitive && createAdjustMap(caseSensitive) - - const h = - /** - * @type {{ - * (): Root - * (selector: null|undefined, ...children: HChild[]): Root - * (selector: string, properties?: HProperties, ...children: HChild[]): Element - * (selector: string, ...children: HChild[]): Element - * }} - */ - ( - /** - * Hyperscript compatible DSL for creating virtual hast trees. - * - * @param {string|null} [selector] - * @param {HProperties|HChild} [properties] - * @param {HChild[]} children - * @returns {HResult} - */ - function (selector, properties, ...children) { - let index = -1 - /** @type {HResult} */ - let node - - if (selector === undefined || selector === null) { - node = {type: 'root', children: []} - // @ts-expect-error Properties are not supported for roots. - children.unshift(properties) - } else { - node = parseSelector(selector, defaultTagName) - // Normalize the name. - node.tagName = node.tagName.toLowerCase() - if (adjust && own.call(adjust, node.tagName)) { - node.tagName = adjust[node.tagName] - } - - // Handle props. - if (isProperties(properties, node.tagName)) { - /** @type {string} */ - let key - - for (key in properties) { - if (own.call(properties, key)) { - // @ts-expect-error `node.properties` is set. - addProperty(schema, node.properties, key, properties[key]) - } - } - } else { - children.unshift(properties) - } - } - - // Handle children. - while (++index < children.length) { - addChild(node.children, children[index]) - } - - if (node.type === 'element' && node.tagName === 'template') { - node.content = {type: 'root', children: node.children} - node.children = [] - } - - return node - } - ) - - return h -} - -/** - * @param {HProperties|HChild} value - * @param {string} name - * @returns {value is HProperties} - */ -function isProperties(value, name) { - if ( - value === null || - value === undefined || - typeof value !== 'object' || - Array.isArray(value) - ) { - return false - } - - if (name === 'input' || !value.type || typeof value.type !== 'string') { - return true - } - - if ('children' in value && Array.isArray(value.children)) { - return false - } - - if (name === 'button') { - return buttonTypes.has(value.type.toLowerCase()) - } - - return !('value' in value) -} - -/** - * @param {Schema} schema - * @param {Properties} properties - * @param {string} key - * @param {HStyle|HPropertyValue} value - * @returns {void} - */ -function addProperty(schema, properties, key, value) { - const info = find(schema, key) - let index = -1 - /** @type {HPropertyValue} */ - let result - - // Ignore nullish and NaN values. - if (value === undefined || value === null) return - - if (typeof value === 'number') { - // Ignore NaN. - if (Number.isNaN(value)) return - - result = value - } - // Booleans. - else if (typeof value === 'boolean') { - result = value - } - // Handle list values. - else if (typeof value === 'string') { - if (info.spaceSeparated) { - result = spaces(value) - } else if (info.commaSeparated) { - result = commas(value) - } else if (info.commaOrSpaceSeparated) { - result = spaces(commas(value).join(' ')) - } else { - result = parsePrimitive(info, info.property, value) - } - } else if (Array.isArray(value)) { - result = value.concat() - } else { - result = info.property === 'style' ? style(value) : String(value) - } - - if (Array.isArray(result)) { - /** @type {Array.} */ - const finalResult = [] - - while (++index < result.length) { - // @ts-expect-error Assume no booleans in array. - finalResult[index] = parsePrimitive(info, info.property, result[index]) - } - - result = finalResult - } - - // Class names (which can be added both on the `selector` and here). - if (info.property === 'className' && Array.isArray(properties.className)) { - // @ts-expect-error Assume no booleans in `className`. - result = properties.className.concat(result) - } - - properties[info.property] = result -} - -/** - * @param {Array.} nodes - * @param {HChild} value - * @returns {void} - */ -function addChild(nodes, value) { - let index = -1 - - if (value === undefined || value === null) { - // Empty. - } else if (typeof value === 'string' || typeof value === 'number') { - nodes.push({type: 'text', value: String(value)}) - } else if (Array.isArray(value)) { - while (++index < value.length) { - addChild(nodes, value[index]) - } - } else if (typeof value === 'object' && 'type' in value) { - if (value.type === 'root') { - addChild(nodes, value.children) - } else { - nodes.push(value) - } - } else { - throw new Error('Expected node, nodes, or string, got `' + value + '`') - } -} - -/** - * Parse a single primitives. - * - * @param {Info} info - * @param {string} name - * @param {HPrimitiveValue} value - * @returns {HPrimitiveValue} - */ -function parsePrimitive(info, name, value) { - if (typeof value === 'string') { - if (info.number && value && !Number.isNaN(Number(value))) { - return Number(value) - } - - if ( - (info.boolean || info.overloadedBoolean) && - (value === '' || normalize(value) === normalize(name)) - ) { - return true - } - } - - return value -} - -/** - * @param {HStyle} value - * @returns {string} - */ -function style(value) { - /** @type {Array.} */ - const result = [] - /** @type {string} */ - let key - - for (key in value) { - if (own.call(value, key)) { - result.push([key, value[key]].join(': ')) - } - } - - return result.join('; ') -} - -/** - * @param {Array.} values - * @returns {Object.} - */ -function createAdjustMap(values) { - /** @type {Object.} */ - const result = {} - let index = -1 - - while (++index < values.length) { - result[values[index].toLowerCase()] = values[index] - } - - return result -} diff --git a/lib/create-automatic-runtime.js b/lib/create-automatic-runtime.js new file mode 100644 index 0000000..ee728dd --- /dev/null +++ b/lib/create-automatic-runtime.js @@ -0,0 +1,52 @@ +/** + * @import {Element, Root} from 'hast' + * @import {Child, Properties, PropertyValue, Result, Style, createH as CreateH} from './create-h.js' + */ + +/** + * @typedef {Record} JSXProps + */ + +// Make VS code see references to above symbols. +'' + +/** + * Create an automatic runtime. + * + * @param {ReturnType} f + * `h` function. + * @returns + * Automatic JSX runtime. + */ +export function createAutomaticRuntime(f) { + /** + * @overload + * @param {null} type + * @param {{children?: Child}} properties + * @param {string | null | undefined} [key] + * @returns {Root} + * + * @overload + * @param {string} type + * @param {JSXProps} properties + * @param {string | null | undefined} [key] + * @returns {Element} + * + * @param {string | null} type + * Element name or `null` to get a root. + * @param {Properties & {children?: Child}} properties + * Properties. + * @returns {Result} + * Result. + */ + function jsx(type, properties) { + const {children, ...properties_} = properties + const result = + // @ts-ignore: `children` is fine: TS has a recursion problem which + // sometimes generates broken types. + type === null ? f(null, children) : f(type, properties_, children) + return result + } + + return {Fragment: null, jsxDEV: jsx, jsxs: jsx, jsx} +} diff --git a/lib/create-h.js b/lib/create-h.js new file mode 100644 index 0000000..4e90f33 --- /dev/null +++ b/lib/create-h.js @@ -0,0 +1,365 @@ +/** + * @import {Element, Nodes, RootContent, Root} from 'hast' + * @import {Info, Schema} from 'property-information' + */ + +/** + * @typedef {Array} ArrayChildNested + * List of children (deep). + */ + +/** + * @typedef {Array} ArrayChild + * List of children. + */ + +/** + * @typedef {Array} ArrayValue + * List of property values for space- or comma separated values (such as `className`). + */ + +/** + * @typedef {ArrayChild | Nodes | PrimitiveChild} Child + * Acceptable child value. + */ + +/** + * @typedef {number | string | null | undefined} PrimitiveChild + * Primitive children, either ignored (nullish), or turned into text nodes. + */ + +/** + * @typedef {boolean | number | string | null | undefined} PrimitiveValue + * Primitive property value. + */ + +/** + * @typedef {Record} Properties + * Acceptable value for element properties. + */ + +/** + * @typedef {ArrayValue | PrimitiveValue} PropertyValue + * Primitive value or list value. + */ + +/** + * @typedef {Element | Root} Result + * Result from a `h` (or `s`) call. + */ + +/** + * @typedef {number | string} StyleValue + * Value for a CSS style field. + */ + +/** + * @typedef {Record} Style + * Supported value of a `style` prop. + */ + +import {parse as parseCommas} from 'comma-separated-tokens' +import {parseSelector} from 'hast-util-parse-selector' +import {find, normalize} from 'property-information' +import {parse as parseSpaces} from 'space-separated-tokens' + +/** + * @param {Schema} schema + * Schema to use. + * @param {string} defaultTagName + * Default tag name. + * @param {ReadonlyArray | undefined} [caseSensitive] + * Case-sensitive tag names (default: `undefined`). + * @returns + * `h`. + */ +export function createH(schema, defaultTagName, caseSensitive) { + const adjust = caseSensitive ? createAdjustMap(caseSensitive) : undefined + + /** + * Hyperscript compatible DSL for creating virtual hast trees. + * + * @overload + * @param {null | undefined} [selector] + * @param {...Child} children + * @returns {Root} + * + * @overload + * @param {string} selector + * @param {Properties} properties + * @param {...Child} children + * @returns {Element} + * + * @overload + * @param {string} selector + * @param {...Child} children + * @returns {Element} + * + * @param {string | null | undefined} [selector] + * Selector. + * @param {Child | Properties | null | undefined} [properties] + * Properties (or first child) (default: `undefined`). + * @param {...Child} children + * Children. + * @returns {Result} + * Result. + */ + function h(selector, properties, ...children) { + /** @type {Result} */ + let node + + if (selector === null || selector === undefined) { + node = {type: 'root', children: []} + // Properties are not supported for roots. + const child = /** @type {Child} */ (properties) + children.unshift(child) + } else { + node = parseSelector(selector, defaultTagName) + // Normalize the name. + const lower = node.tagName.toLowerCase() + const adjusted = adjust ? adjust.get(lower) : undefined + node.tagName = adjusted || lower + + // Handle properties. + if (isChild(properties)) { + children.unshift(properties) + } else { + for (const [key, value] of Object.entries(properties)) { + addProperty(schema, node.properties, key, value) + } + } + } + + // Handle children. + for (const child of children) { + addChild(node.children, child) + } + + if (node.type === 'element' && node.tagName === 'template') { + node.content = {type: 'root', children: node.children} + node.children = [] + } + + return node + } + + return h +} + +/** + * Check if something is properties or a child. + * + * @param {Child | Properties} value + * Value to check. + * @returns {value is Child} + * Whether `value` is definitely a child. + */ +function isChild(value) { + // Never properties if not an object. + if (value === null || typeof value !== 'object' || Array.isArray(value)) { + return true + } + + // Never node without `type`; that’s the main discriminator. + if (typeof value.type !== 'string') return false + + // Slower check: never property value if object or array with + // non-number/strings. + const record = /** @type {Record} */ (value) + const keys = Object.keys(value) + + for (const key of keys) { + const value = record[key] + + if (value && typeof value === 'object') { + if (!Array.isArray(value)) return true + + const list = /** @type {ReadonlyArray} */ (value) + + for (const item of list) { + if (typeof item !== 'number' && typeof item !== 'string') { + return true + } + } + } + } + + // Also see empty `children` as a node. + if ('children' in value && Array.isArray(value.children)) { + return true + } + + // Default to properties, someone can always pass an empty object, + // put `data: {}` in a node, + // or wrap it in an array. + return false +} + +/** + * @param {Schema} schema + * Schema. + * @param {Properties} properties + * Properties object. + * @param {string} key + * Property name. + * @param {PropertyValue | Style} value + * Property value. + * @returns {undefined} + * Nothing. + */ +function addProperty(schema, properties, key, value) { + const info = find(schema, key) + /** @type {PropertyValue} */ + let result + + // Ignore nullish and NaN values. + if (value === null || value === undefined) return + + if (typeof value === 'number') { + // Ignore NaN. + if (Number.isNaN(value)) return + + result = value + } + // Booleans. + else if (typeof value === 'boolean') { + result = value + } + // Handle list values. + else if (typeof value === 'string') { + if (info.spaceSeparated) { + result = parseSpaces(value) + } else if (info.commaSeparated) { + result = parseCommas(value) + } else if (info.commaOrSpaceSeparated) { + result = parseSpaces(parseCommas(value).join(' ')) + } else { + result = parsePrimitive(info, info.property, value) + } + } else if (Array.isArray(value)) { + result = [...value] + } else { + result = info.property === 'style' ? style(value) : String(value) + } + + if (Array.isArray(result)) { + /** @type {Array} */ + const finalResult = [] + + for (const item of result) { + // Assume no booleans in array. + finalResult.push( + /** @type {number | string} */ ( + parsePrimitive(info, info.property, item) + ) + ) + } + + result = finalResult + } + + // Class names (which can be added both on the `selector` and here). + if (info.property === 'className' && Array.isArray(properties.className)) { + // Assume no booleans in `className`. + result = properties.className.concat( + /** @type {Array | number | string} */ (result) + ) + } + + properties[info.property] = result +} + +/** + * @param {Array} nodes + * Children. + * @param {Child} value + * Child. + * @returns {undefined} + * Nothing. + */ +function addChild(nodes, value) { + if (value === null || value === undefined) { + // Empty. + } else if (typeof value === 'number' || typeof value === 'string') { + nodes.push({type: 'text', value: String(value)}) + } else if (Array.isArray(value)) { + for (const child of value) { + addChild(nodes, child) + } + } else if (typeof value === 'object' && 'type' in value) { + if (value.type === 'root') { + addChild(nodes, value.children) + } else { + nodes.push(value) + } + } else { + throw new Error('Expected node, nodes, or string, got `' + value + '`') + } +} + +/** + * Parse a single primitives. + * + * @param {Info} info + * Property information. + * @param {string} name + * Property name. + * @param {PrimitiveValue} value + * Property value. + * @returns {PrimitiveValue} + * Property value. + */ +function parsePrimitive(info, name, value) { + if (typeof value === 'string') { + if (info.number && value && !Number.isNaN(Number(value))) { + return Number(value) + } + + if ( + (info.boolean || info.overloadedBoolean) && + (value === '' || normalize(value) === normalize(name)) + ) { + return true + } + } + + return value +} + +/** + * Serialize a `style` object as a string. + * + * @param {Style} styles + * Style object. + * @returns {string} + * CSS string. + */ +function style(styles) { + /** @type {Array} */ + const result = [] + + for (const [key, value] of Object.entries(styles)) { + result.push([key, value].join(': ')) + } + + return result.join('; ') +} + +/** + * Create a map to adjust casing. + * + * @param {ReadonlyArray} values + * List of properly cased keys. + * @returns {Map} + * Map of lowercase keys to uppercase keys. + */ +function createAdjustMap(values) { + /** @type {Map} */ + const result = new Map() + + for (const value of values) { + result.set(value.toLowerCase(), value) + } + + return result +} diff --git a/lib/html.js b/lib/html.js deleted file mode 100644 index 162f5cb..0000000 --- a/lib/html.js +++ /dev/null @@ -1,14 +0,0 @@ -/** - * @typedef {import('./core.js').HChild} Child Acceptable child value - * @typedef {import('./core.js').HProperties} Properties Acceptable properties value. - * - * @typedef {import('./jsx-classic').Element} h.JSX.Element - * @typedef {import('./jsx-classic').IntrinsicAttributes} h.JSX.IntrinsicAttributes - * @typedef {import('./jsx-classic').IntrinsicElements} h.JSX.IntrinsicElements - * @typedef {import('./jsx-classic').ElementChildrenAttribute} h.JSX.ElementChildrenAttribute - */ - -import {html} from 'property-information' -import {core} from './core.js' - -export const h = core(html, 'div') diff --git a/lib/index.js b/lib/index.js index 5e87b79..8d32178 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,7 +1,27 @@ +// Register the JSX namespace on `h`. /** - * @typedef {import('./core.js').HChild} Child Acceptable child value - * @typedef {import('./core.js').HProperties} Properties Acceptable properties value. + * @typedef {import('./jsx-classic.js').Element} h.JSX.Element + * @typedef {import('./jsx-classic.js').ElementChildrenAttribute} h.JSX.ElementChildrenAttribute + * @typedef {import('./jsx-classic.js').IntrinsicAttributes} h.JSX.IntrinsicAttributes + * @typedef {import('./jsx-classic.js').IntrinsicElements} h.JSX.IntrinsicElements */ -export {h} from './html.js' -export {s} from './svg.js' +// Register the JSX namespace on `s`. +/** + * @typedef {import('./jsx-classic.js').Element} s.JSX.Element + * @typedef {import('./jsx-classic.js').ElementChildrenAttribute} s.JSX.ElementChildrenAttribute + * @typedef {import('./jsx-classic.js').IntrinsicAttributes} s.JSX.IntrinsicAttributes + * @typedef {import('./jsx-classic.js').IntrinsicElements} s.JSX.IntrinsicElements + */ + +import {html, svg} from 'property-information' +import {createH} from './create-h.js' +import {svgCaseSensitiveTagNames} from './svg-case-sensitive-tag-names.js' + +// Note: this explicit type is needed, otherwise TS creates broken types. +/** @type {ReturnType} */ +export const h = createH(html, 'div') + +// Note: this explicit type is needed, otherwise TS creates broken types. +/** @type {ReturnType} */ +export const s = createH(svg, 'g', svgCaseSensitiveTagNames) diff --git a/lib/jsx-automatic.d.ts b/lib/jsx-automatic.d.ts index cf7a71c..24a9732 100644 --- a/lib/jsx-automatic.d.ts +++ b/lib/jsx-automatic.d.ts @@ -1,42 +1,43 @@ -import {HProperties, HChild, HResult} from './core.js' +import type {Child, Properties, Result} from './create-h.js' export namespace JSX { /** - * This defines the return value of JSX syntax. + * Define the return value of JSX syntax. */ - type Element = HResult + type Element = Result /** - * This disallows the use of functional components. + * Key of this interface defines as what prop children are passed. + */ + interface ElementChildrenAttribute { + /** + * Only the key matters, not the value. + */ + children?: never + } + + /** + * Disallow the use of functional components. */ type IntrinsicAttributes = never /** - * This defines the prop types for known elements. + * Define the prop types for known elements. * - * For `hastscript` this defines any string may be used in combination with `hast` `Properties`. + * For `hastscript` this defines any string may be used in combination with + * `hast` `Properties`. * * This **must** be an interface. */ - // eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style - interface IntrinsicElements { - [name: string]: - | HProperties - | { - /** - * The prop that matches `ElementChildrenAttribute` key defines the type of JSX children, defines the children type. - */ - children?: HChild - } - } - - /** - * The key of this interface defines as what prop children are passed. - */ - interface ElementChildrenAttribute { - /** - * Only the key matters, not the value. - */ - children?: never - } + type IntrinsicElements = Record< + string, + | Properties + | { + /** + * The prop that matches `ElementChildrenAttribute` key defines the + * type of JSX children, defines the children type. + */ + children?: Child + } + > } diff --git a/lib/jsx-classic.d.ts b/lib/jsx-classic.d.ts index 1b720d7..be07a62 100644 --- a/lib/jsx-classic.d.ts +++ b/lib/jsx-classic.d.ts @@ -1,46 +1,47 @@ -import {HProperties, HChild, HResult} from './core.js' +import type {Child, Properties, Result} from './create-h.js' /** - * This unique symbol is declared to specify the key on which JSX children are passed, without conflicting - * with the Attributes type. + * This unique symbol is declared to specify the key on which JSX children are + * passed, without conflicting with the `Attributes` type. */ declare const children: unique symbol /** - * This defines the return value of JSX syntax. + * Define the return value of JSX syntax. */ -export type Element = HResult +export type Element = Result /** - * This disallows the use of functional components. + * Key of this interface defines as what prop children are passed. + */ +export interface ElementChildrenAttribute { + /** + * Only the key matters, not the value. + */ + [children]?: never +} + +/** + * Disallow the use of functional components. */ export type IntrinsicAttributes = never /** - * This defines the prop types for known elements. + * Define the prop types for known elements. * - * For `hastscript` this defines any string may be used in combination with `hast` `Properties`. + * For `hastscript` this defines any string may be used in combination with + * `hast` `Properties`. * * This **must** be an interface. */ -// eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style -export interface IntrinsicElements { - [name: string]: - | HProperties - | { - /** - * The prop that matches `ElementChildrenAttribute` key defines the type of JSX children, defines the children type. - */ - [children]?: HChild - } -} - -/** - * The key of this interface defines as what prop children are passed. - */ -export interface ElementChildrenAttribute { - /** - * Only the key matters, not the value. - */ - [children]?: never -} +export type IntrinsicElements = Record< + string, + | Properties + | { + /** + * The prop that matches `ElementChildrenAttribute` key defines the + * type of JSX children, defines the children type. + */ + [children]?: Child + } +> diff --git a/lib/runtime-html.js b/lib/runtime-html.js deleted file mode 100644 index 06ffb38..0000000 --- a/lib/runtime-html.js +++ /dev/null @@ -1,6 +0,0 @@ -// Export `JSX` as a global for TypeScript. -import {runtime} from './runtime.js' -import {h} from './html.js' - -export * from './jsx-automatic.js' -export const {Fragment, jsx, jsxs} = runtime(h) diff --git a/lib/runtime-svg.js b/lib/runtime-svg.js deleted file mode 100644 index 13117ba..0000000 --- a/lib/runtime-svg.js +++ /dev/null @@ -1,6 +0,0 @@ -// Export `JSX` as a global for TypeScript. -import {runtime} from './runtime.js' -import {s} from './svg.js' - -export * from './jsx-automatic.js' -export const {Fragment, jsx, jsxs} = runtime(s) diff --git a/lib/runtime.js b/lib/runtime.js deleted file mode 100644 index 7062781..0000000 --- a/lib/runtime.js +++ /dev/null @@ -1,38 +0,0 @@ -/** - * @typedef {import('./core.js').Element} Element - * @typedef {import('./core.js').Root} Root - * @typedef {import('./core.js').HResult} HResult - * @typedef {import('./core.js').HChild} HChild - * @typedef {import('./core.js').HProperties} HProperties - * @typedef {import('./core.js').HPropertyValue} HPropertyValue - * @typedef {import('./core.js').HStyle} HStyle - * @typedef {import('./core.js').core} Core - * - * @typedef {{[x: string]: HPropertyValue|HStyle|HChild}} JSXProps - */ - -/** - * @param {ReturnType} f - */ -export function runtime(f) { - const jsx = - /** - * @type {{ - * (type: null|undefined, props: {children?: HChild}, key?: string): Root - * (type: string, props: JSXProps, key?: string): Element - * }} - */ - ( - /** - * @param {string|null} type - * @param {HProperties & {children?: HChild}} props - * @returns {HResult} - */ - function (type, props) { - const {children, ...properties} = props - return type === null ? f(type, children) : f(type, properties, children) - } - ) - - return {Fragment: null, jsx, jsxs: jsx} -} diff --git a/lib/svg-case-sensitive-tag-names.js b/lib/svg-case-sensitive-tag-names.js index 575ceef..3e27ea3 100644 --- a/lib/svg-case-sensitive-tag-names.js +++ b/lib/svg-case-sensitive-tag-names.js @@ -1,3 +1,8 @@ +/** + * List of case-sensitive SVG tag names. + * + * @type {ReadonlyArray} + */ export const svgCaseSensitiveTagNames = [ 'altGlyph', 'altGlyphDef', diff --git a/lib/svg.js b/lib/svg.js deleted file mode 100644 index b04a847..0000000 --- a/lib/svg.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * @typedef {import('./core.js').HChild} Child Acceptable child value - * @typedef {import('./core.js').HProperties} Properties Acceptable properties value. - * - * @typedef {import('./jsx-classic').Element} s.JSX.Element - * @typedef {import('./jsx-classic').IntrinsicAttributes} s.JSX.IntrinsicAttributes - * @typedef {import('./jsx-classic').IntrinsicElements} s.JSX.IntrinsicElements - * @typedef {import('./jsx-classic').ElementChildrenAttribute} s.JSX.ElementChildrenAttribute - */ - -import {svg} from 'property-information' -import {core} from './core.js' -import {svgCaseSensitiveTagNames} from './svg-case-sensitive-tag-names.js' - -export const s = core(svg, 'g', svgCaseSensitiveTagNames) diff --git a/license b/license index 8d8660d..bc8f165 100644 --- a/license +++ b/license @@ -1,6 +1,6 @@ (The MIT License) -Copyright (c) 2016 Titus Wormer +Copyright (c) Titus Wormer Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/package.json b/package.json index ba7f317..e86c69a 100644 --- a/package.json +++ b/package.json @@ -1,116 +1,126 @@ { - "name": "hastscript", - "version": "7.0.2", - "description": "hast utility to create trees", - "license": "MIT", - "keywords": [ - "unist", - "hast", - "hast-util", - "util", - "utility", - "html", - "rehype", - "vdom", - "virtual", - "dom", - "hyperscript", - "dsl" - ], - "repository": "syntax-tree/hastscript", - "bugs": "https://github.com/syntax-tree/hastscript/issues", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - }, "author": "Titus Wormer (https://wooorm.com)", + "bugs": "https://github.com/syntax-tree/hastscript/issues", "contributors": [ "Titus Wormer (https://wooorm.com)" ], - "sideEffects": false, - "type": "module", - "main": "index.js", - "types": "index.d.ts", - "exports": { - ".": "./index.js", - "./index.js": "./index.js", - "./html.js": "./html.js", - "./svg.js": "./svg.js", - "./jsx-runtime": "./jsx-runtime.js", - "./html/jsx-runtime": "./html/jsx-runtime.js", - "./svg/jsx-runtime": "./svg/jsx-runtime.js" - }, - "files": [ - "lib/", - "html/", - "svg/", - "html.d.ts", - "html.js", - "svg.d.ts", - "svg.js", - "jsx-runtime.d.ts", - "jsx-runtime.js", - "index.d.ts", - "index.js" - ], "dependencies": { - "@types/hast": "^2.0.0", + "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", - "hast-util-parse-selector": "^3.0.0", - "property-information": "^6.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" }, + "description": "hast utility to create trees", + "devDependencies#": "note: some bug with `typescript` 5.5 being broken", "devDependencies": { - "@babel/core": "^7.0.0", - "@babel/plugin-syntax-jsx": "^7.0.0", - "@babel/plugin-transform-react-jsx": "^7.0.0", - "@types/babel__core": "^7.0.0", - "@types/tape": "^4.0.0", - "acorn": "^8.0.0", + "@types/node": "^22.0.0", "acorn-jsx": "^5.0.0", - "astring": "^1.0.0", - "c8": "^7.0.0", - "estree-util-build-jsx": "^2.0.0", - "prettier": "^2.0.0", - "remark-cli": "^9.0.0", - "remark-preset-wooorm": "^8.0.0", - "rimraf": "^3.0.0", + "c8": "^10.0.0", + "esast-util-from-js": "^2.0.0", + "estree-util-build-jsx": "^3.0.0", + "estree-util-to-js": "^2.0.0", + "prettier": "^3.0.0", + "remark-cli": "^12.0.0", + "remark-preset-wooorm": "^11.0.0", "svg-tag-names": "^3.0.0", - "tape": "^5.0.0", - "tsd": "^0.17.0", + "tsd": "^0.31.0", "type-coverage": "^2.0.0", - "typescript": "^4.0.0", - "unist-builder": "^3.0.0", - "xo": "^0.42.0" + "typescript": "^5.0.0", + "xo": "^0.60.0" }, - "scripts": { - "prepack": "npm run build && npm run format", - "build": "rimraf \"{script/**,test/**,}*.d.ts\" \"lib/{core,html,index,runtime-html,runtime-svg,runtime,svg-case-sensitive-tag-names,svg}.d.ts\" && tsc && tsd && type-coverage", - "generate": "node script/generate-jsx && node script/build", - "format": "remark . -qfo && prettier . -w --loglevel warn && xo --fix", - "test-api": "node test/index.js", - "test-coverage": "c8 --check-coverage --branches 100 --functions 100 --lines 100 --statements 100 --reporter lcov node test/index.js", - "test": "npm run build && npm run generate && npm run format && npm run test-coverage" + "exports": { + "./jsx-dev-runtime": "./lib/automatic-runtime-html.js", + "./jsx-runtime": "./lib/automatic-runtime-html.js", + "./svg/jsx-dev-runtime": "./lib/automatic-runtime-svg.js", + "./svg/jsx-runtime": "./lib/automatic-runtime-svg.js", + ".": "./index.js" + }, + "files": [ + "index.d.ts.map", + "index.d.ts", + "index.js", + "lib/" + ], + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" }, + "keywords": [ + "dom", + "dsl", + "hast-util", + "hast", + "html", + "hyperscript", + "rehype", + "unist", + "utility", + "util", + "vdom", + "virtual" + ], + "license": "MIT", + "name": "hastscript", "prettier": { - "tabWidth": 2, - "useTabs": false, - "singleQuote": true, "bracketSpacing": false, "semi": false, - "trailingComma": "none" - }, - "xo": { - "prettier": true + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "none", + "useTabs": false }, "remarkConfig": { "plugins": [ - "preset-wooorm" + "remark-preset-wooorm" ] }, + "repository": "syntax-tree/hastscript", + "scripts": { + "build": "tsc --build --clean && tsc --build && tsd && type-coverage", + "format": "remark --frail --output --quiet -- . && prettier --log-level warn --write -- . && xo --fix", + "generate": "node --conditions development script/generate-jsx.js && node --conditions development script/build.js", + "test-api": "node --conditions development test/index.js", + "test-coverage": "c8 --100 --reporter lcov -- npm run test-api", + "test": "npm run generate && npm run build && npm run format && npm run test-coverage" + }, + "sideEffects": false, "typeCoverage": { "atLeast": 100, - "detail": true, + "ignoreFiles#": "needed `any`s :'(", + "ignoreFiles": [ + "test/jsx-build-jsx-automatic-development.js" + ], "strict": true + }, + "type": "module", + "version": "9.0.1", + "xo": { + "overrides": [ + { + "files": [ + "**/*.ts" + ], + "rules": { + "@typescript-eslint/array-type": [ + "error", + { + "default": "generic" + } + ], + "@typescript-eslint/ban-types": [ + "error", + { + "extendDefaults": true + } + ], + "@typescript-eslint/consistent-type-definitions": [ + "error", + "interface" + ] + } + } + ], + "prettier": true } } diff --git a/readme.md b/readme.md index 027966a..6c5cd94 100644 --- a/readme.md +++ b/readme.md @@ -1,30 +1,72 @@ # hastscript -[![Build][build-badge]][build] -[![Coverage][coverage-badge]][coverage] -[![Downloads][downloads-badge]][downloads] -[![Size][size-badge]][size] -[![Sponsors][sponsors-badge]][collective] -[![Backers][backers-badge]][collective] -[![Chat][chat-badge]][chat] +[![Build][badge-build-image]][badge-build-url] +[![Coverage][badge-coverage-image]][badge-coverage-url] +[![Downloads][badge-downloads-image]][badge-downloads-url] +[![Size][badge-size-image]][badge-size-url] + +[hast][github-hast] utility to create trees with ease. + +## Contents + +* [What is this?](#what-is-this) +* [When should I use this?](#when-should-i-use-this) +* [Install](#install) +* [Use](#use) +* [API](#api) + * [`h(selector?[, properties][, …children])`](#hselector-properties-children) + * [`s(selector?[, properties][, …children])`](#sselector-properties-children) + * [`Child`](#child) + * [`Properties`](#properties-1) + * [`Result`](#result) +* [Syntax tree](#syntax-tree) +* [JSX](#jsx) +* [Compatibility](#compatibility) +* [Security](#security) +* [Related](#related) +* [Contribute](#contribute) +* [License](#license) + +## What is this? + +This package is a hyperscript interface (like `createElement` from React and +`h` from Vue and such) to help with creating hast trees. + +## When should I use this? + +You can use this utility in your project when you generate hast syntax trees +with code. +It helps because it replaces most of the repetition otherwise needed in a syntax +tree with function calls. +It also helps as it improves the attributes you pass by turning them into the +form that is required by hast. + +You can instead use [`unist-builder`][github-unist-builder] +when creating any unist nodes and +[`xastscript`][github-xastscript] when creating xast (XML) nodes. -[**hast**][hast] utility to create [*trees*][tree] in HTML or SVG. +## Install -Similar to [`hyperscript`][hyperscript], [`virtual-dom/h`][virtual-hyperscript], -[`React.createElement`][react], and [Vue’s `createElement`][vue], -but for [**hast**][hast]. +This package is [ESM only][github-gist-esm]. +In Node.js (version 16+), +install with [npm][npmjs-install]: -Use [`unist-builder`][u] to create any [**unist**][unist] tree. +```sh +npm install hastscript +``` -## Install +In Deno with [`esm.sh`][esmsh]: -This package is [ESM only](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c): -Node 12+ is needed to use it and it must be `import`ed instead of `require`d. +```js +import {h} from 'https://esm.sh/hastscript@9' +``` -[npm][]: +In browsers with [`esm.sh`][esmsh]: -```sh -npm install hastscript +```html + ``` ## Use @@ -32,7 +74,6 @@ npm install hastscript ```js import {h, s} from 'hastscript' -// Children as an array: console.log( h('.foo#some-id', [ h('span', 'some text'), @@ -44,20 +85,8 @@ console.log( ]) ) -// Children as arguments: console.log( - h( - 'form', - {method: 'POST'}, - h('input', {type: 'text', name: 'foo'}), - h('input', {type: 'text', name: 'bar'}), - h('input', {type: 'submit', value: 'send'}) - ) -) - -// SVG: -console.log( - s('svg', {xmlns: 'http://www.w3.org/2000/svg', viewbox: '0 0 500 500'}, [ + s('svg', {viewbox: '0 0 500 500', xmlns: 'http://www.w3.org/2000/svg'}, [ s('title', 'SVG `` element'), s('circle', {cx: 120, cy: 120, r: 100}) ]) @@ -92,35 +121,10 @@ Yields: } ] } -{ - type: 'element', - tagName: 'form', - properties: {method: 'POST'}, - children: [ - { - type: 'element', - tagName: 'input', - properties: {type: 'text', name: 'foo'}, - children: [] - }, - { - type: 'element', - tagName: 'input', - properties: {type: 'text', name: 'bar'}, - children: [] - }, - { - type: 'element', - tagName: 'input', - properties: {type: 'submit', value: 'send'}, - children: [] - } - ] -} { type: 'element', tagName: 'svg', - properties: {xmlns: 'http://www.w3.org/2000/svg', viewBox: '0 0 500 500'}, + properties: {viewBox: '0 0 500 500', xmlns: 'http://www.w3.org/2000/svg'}, children: [ { type: 'element', @@ -140,70 +144,154 @@ Yields: ## API -This package exports the following identifiers: `h` and `s`. +This package exports the identifiers [`h`][api-h] and [`s`][api-s]. There is no default export. +It exports the additional [TypeScript][] types +[`Child`][api-child], +[`Properties`][api-properties], +and +[`Result`][api-result]. -### `h(selector?[, properties][, …children])` +The export map supports the automatic JSX runtime. +You can pass `hastscript` or `hastscript/svg` to your build tool +(TypeScript, Babel, SWC) +with an `importSource` option or similar. -### `s(selector?[, properties][, …children])` +### `h(selector?[, properties][, …children])` -Create virtual [**hast**][hast] [*trees*][tree] for HTML or SVG. +Create virtual **[hast][github-hast]** trees for HTML. ##### Signatures -* `h(): root` -* `h(null[, …children]): root` -* `h(name[, properties][, …children]): element` - -(and the same for `s`). +* `h(): root` +* `h(null[, …children]): root` +* `h(selector[, properties][, …children]): element` ##### Parameters ###### `selector` -Simple CSS selector (`string`, optional). -Can contain a tag name (`foo`), IDs (`#bar`), and classes (`.baz`). -If the selector is a string but there is no tag name in it, `h` defaults to -build a `div` element, and `s` to a `g` element. -`selector` is parsed by [`hast-util-parse-selector`][parse-selector]. -When string, builds an [`Element`][element]. -When nullish, builds a [`Root`][root] instead. +Simple CSS selector +(`string`, optional). +When string, builds an [`Element`][github-hast-element]. +When nullish, builds a [`Root`][github-hast-root] instead. +The selector can contain a tag name (`foo`), +IDs (`#bar`), +and classes (`.baz`). +If the selector is a string but there is no tag name in it then `h` defaults to +build a `div` element and `s` to a `g` element. +`selector` is parsed by +[`hast-util-parse-selector`][github-hast-util-parse-selector]. ###### `properties` -Map of properties (`Object.<*>`, optional). -Keys should match either the HTML attribute name, or the DOM property name, but -are case-insensitive. -Cannot be given when building a [`Root`][root]. +Properties of the element +([`Properties`][api-properties], optional). ###### `children` -(Lists of) children (`string`, `number`, `Node`, `Array.`, optional). -When strings or numbers are encountered, they are mapped to [`Text`][text] -nodes. -If [`Root`][root] nodes are given, their children are used instead. +Children of the node ([`Child`][api-child] or `Array`, optional). ##### Returns -[`Element`][element] or [`Root`][root]. +Created tree ([`Result`][api-result]). + +[`Element`][github-hast-element] when a `selector` is passed, +otherwise [`Root`][github-hast-root]. + +### `s(selector?[, properties][, …children])` + +Create virtual **[hast][github-hast]** trees for SVG. + +Signatures, parameters, and return value are the same as `h` above. +Importantly, +the `selector` and `properties` parameters are interpreted as SVG. + +### `Child` + +(Lists of) children (TypeScript type). + +When strings or numbers are encountered, +they are turned into [`Text`][github-hast-text] +nodes. +[`Root`][github-hast-root] nodes are treated as “fragments”, +meaning that their children are used instead. + +###### Type + +```ts +type Child = + | Array + | Node + | number + | string + | null + | undefined +``` + +### `Properties` + +Map of properties (TypeScript type). +Keys should match either the HTML attribute name or the DOM property name, +but are case-insensitive. + +###### Type + +```ts +type Properties = Record< + string, + | boolean + | number + | string + | null + | undefined + // For comma- and space-separated values such as `className`: + | Array + // Accepts value for `style` prop as object. + | Record +> +``` + +### `Result` + +Result from a `h` (or `s`) call (TypeScript type). + +###### Type + +```ts +type Result = Element | Root +``` + +## Syntax tree + +The syntax tree is [hast][github-hast]. ## JSX -`hastscript` can be used with JSX. -Either use the automatic runtime set to `hastscript/html`, `hastscript/svg`, -or `hastscript` (shortcut for HTML). +This package can be used with JSX. +You should use the automatic JSX runtime set to `hastscript` or +`hastscript/svg`. + +> 👉 **Note** +> while `h` supports dots (`.`) for classes or number signs (`#`) +> for IDs in `selector`, +> those are not supported in JSX. -Or import `h` or `s` yourself and define it as the pragma (plus set the fragment -to `null`). +> 🪦 **Legacy**: +> you can also use the classic JSX runtime, +> but this is not recommended. +> To do so, +> import `h` (or `s`) yourself and define it as the pragma +> (plus set the fragment to `null`). -The example above can then be written like so, using inline pragmas, so -that SVG can be used too: +The Use example above can then be written like so, +using inline pragmas, +so that SVG can be used too: `example-html.jsx`: -```jsx +```js /** @jsxImportSource hastscript */ - console.log(
some text @@ -213,19 +301,11 @@ console.log(
) - -console.log( -
- - - -
-) ``` `example-svg.jsx`: -```jsx +```js /** @jsxImportSource hastscript/svg */ console.log( @@ -235,59 +315,33 @@ console.log( ) ``` -Because JSX does not allow dots (`.`) or number signs (`#`) in tag names, you -have to pass class names and IDs in as attributes. - -You can use [`estree-util-build-jsx`][build-jsx] to compile JSX away. - -You could also use [bublé][], but it’s not ideal (`jsxFragment` is currently -only available on the API, not the CLI, and it only allows a single pragma). +## Compatibility -For [Babel][], use [`@babel/plugin-transform-react-jsx`][babel-jsx] and either -pass `pragma: 'h'` and `pragmaFrag: 'null'`, or pass `importSource: -'hastscript'`. -This is less ideal because it allows a single pragma. +Projects maintained by the unified collective are compatible with maintained +versions of Node.js. -Babel also lets you configure this in a script: - -```jsx -/** @jsx s @jsxFrag null */ -import {s} from 'hastscript' - -console.log() -``` - -This is useful because it allows using *both* `html` and `svg`, although in -different files. +When we cut a new major release, +we drop support for unmaintained versions of Node. +This means we try to keep the current release line, +`hastscript@9`, +compatible with Node.js 16. ## Security -Use of `hastscript` can open you up to a [cross-site scripting (XSS)][xss] -attack as values are injected into the syntax tree. -The following example shows how a script is injected that runs when loaded in a -browser. - -```js -const tree = {type: 'root', children: []} - -tree.children.push(h('script', 'alert(1)')) -``` - -Yields: - -```html - -``` +Use of `hastscript` can open you up to a +[cross-site scripting (XSS)][wikipedia-xss] +when you pass user-provided input to it because values are injected into the +syntax tree. The following example shows how an image is injected that fails loading and therefore runs code in a browser. ```js -const tree = {type: 'root', children: []} +const tree = h() // Somehow someone injected these properties instead of an expected `src` and // `alt`: -const otherProps = {src: 'x', onError: 'alert(2)'} +const otherProps = {onError: 'alert(1)', src: 'x'} tree.children.push(h('img', {src: 'default.png', ...otherProps})) ``` @@ -295,20 +349,20 @@ tree.children.push(h('img', {src: 'default.png', ...otherProps})) Yields: ```html - + ``` The following example shows how code can run in a browser because someone stored an object in a database instead of the expected string. ```js -const tree = {type: 'root', children: []} +const tree = h() // Somehow this isn’t the expected `'wooorm'`. const username = { type: 'element', tagName: 'script', - children: [{type: 'text', value: 'alert(3)'}] + children: [{type: 'text', value: 'alert(2)'}] } tree.children.push(h('span.handle', username)) @@ -317,115 +371,107 @@ tree.children.push(h('span.handle', username)) Yields: ```html - + ``` -Either do not use user input in `hastscript` or use -[`hast-util-santize`][sanitize]. +Either do not use user-provided input in `hastscript` or use +[`hast-util-santize`][github-hast-util-sanitize]. ## Related -* [`unist-builder`](https://github.com/syntax-tree/unist-builder) - — Create any unist tree -* [`xastscript`](https://github.com/syntax-tree/xastscript) - — Create a xast tree -* [`hast-to-hyperscript`](https://github.com/syntax-tree/hast-to-hyperscript) - — Convert a Node to React, Virtual DOM, Hyperscript, and more -* [`hast-util-from-dom`](https://github.com/syntax-tree/hast-util-from-dom) - — Transform a DOM tree to hast -* [`hast-util-select`](https://github.com/syntax-tree/hast-util-select) - — `querySelector`, `querySelectorAll`, and `matches` -* [`hast-util-to-html`](https://github.com/syntax-tree/hast-util-to-html) - — Stringify nodes to HTML -* [`hast-util-to-dom`](https://github.com/syntax-tree/hast-util-to-dom) - — Transform to a DOM tree +* [`unist-builder`][github-unist-builder] + — create unist trees +* [`xastscript`][github-xastscript] + — create xast trees +* [`hast-to-hyperscript`](https://github.com/syntax-tree/hast-to-hyperscript) + — turn hast into React, Preact, Vue, etc +* [`hast-util-to-html`](https://github.com/syntax-tree/hast-util-to-html) + — turn hast into HTML +* [`hast-util-to-dom`](https://github.com/syntax-tree/hast-util-to-dom) + — turn hast into DOM trees +* [`estree-util-build-jsx`](https://github.com/syntax-tree/estree-util-build-jsx) + — compile JSX away ## Contribute -See [`contributing.md` in `syntax-tree/.github`][contributing] for ways to get -started. -See [`support.md`][support] for ways to get help. +See +[`contributing.md`][health-contributing] +in +[`syntax-tree/.github`][health] +for ways to get started. +See [`support.md`][health-support] for ways to get help. -This project has a [code of conduct][coc]. -By interacting with this repository, organization, or community you agree to -abide by its terms. +This project has a [code of conduct][health-coc]. +By interacting with this repository, +organization, +or community you agree to abide by its terms. ## License -[MIT][license] © [Titus Wormer][author] +[MIT][file-license] © [Titus Wormer][wooorm] -[build-badge]: https://github.com/syntax-tree/hastscript/workflows/main/badge.svg - -[build]: https://github.com/syntax-tree/hastscript/actions - -[coverage-badge]: https://img.shields.io/codecov/c/github/syntax-tree/hastscript.svg - -[coverage]: https://codecov.io/github/syntax-tree/hastscript - -[downloads-badge]: https://img.shields.io/npm/dm/hastscript.svg - -[downloads]: https://www.npmjs.com/package/hastscript +[api-child]: #child -[size-badge]: https://img.shields.io/bundlephobia/minzip/hastscript.svg +[api-h]: #hselector-properties-children -[size]: https://bundlephobia.com/result?p=hastscript +[api-properties]: #properties-1 -[sponsors-badge]: https://opencollective.com/unified/sponsors/badge.svg +[api-result]: #result -[backers-badge]: https://opencollective.com/unified/backers/badge.svg +[api-s]: #sselector-properties-children -[collective]: https://opencollective.com/unified +[badge-build-image]: https://github.com/syntax-tree/hastscript/workflows/main/badge.svg -[chat-badge]: https://img.shields.io/badge/chat-discussions-success.svg +[badge-build-url]: https://github.com/syntax-tree/hastscript/actions -[chat]: https://github.com/syntax-tree/unist/discussions +[badge-coverage-image]: https://img.shields.io/codecov/c/github/syntax-tree/hastscript.svg -[npm]: https://docs.npmjs.com/cli/install +[badge-coverage-url]: https://codecov.io/github/syntax-tree/hastscript -[license]: license +[badge-downloads-image]: https://img.shields.io/npm/dm/hastscript.svg -[author]: https://wooorm.com +[badge-downloads-url]: https://www.npmjs.com/package/hastscript -[contributing]: https://github.com/syntax-tree/.github/blob/HEAD/contributing.md +[badge-size-image]: https://img.shields.io/bundlejs/size/hastscript -[support]: https://github.com/syntax-tree/.github/blob/HEAD/support.md +[badge-size-url]: https://bundlejs.com/?q=hastscript -[coc]: https://github.com/syntax-tree/.github/blob/HEAD/code-of-conduct.md +[esmsh]: https://esm.sh -[hyperscript]: https://github.com/dominictarr/hyperscript +[file-license]: license -[virtual-hyperscript]: https://github.com/Matt-Esch/virtual-dom/tree/HEAD/virtual-hyperscript +[github-gist-esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c -[react]: https://reactjs.org/docs/glossary.html#react-elements +[github-hast]: https://github.com/syntax-tree/hast -[vue]: https://vuejs.org/v2/guide/render-function.html#createElement-Arguments +[github-hast-element]: https://github.com/syntax-tree/hast#element -[unist]: https://github.com/syntax-tree/unist +[github-hast-root]: https://github.com/syntax-tree/hast#root -[tree]: https://github.com/syntax-tree/unist#tree +[github-hast-text]: https://github.com/syntax-tree/hast#text -[hast]: https://github.com/syntax-tree/hast +[github-hast-util-parse-selector]: https://github.com/syntax-tree/hast-util-parse-selector -[element]: https://github.com/syntax-tree/hast#element +[github-hast-util-sanitize]: https://github.com/syntax-tree/hast-util-sanitize -[root]: https://github.com/syntax-tree/xast#root +[github-unist-builder]: https://github.com/syntax-tree/unist-builder -[text]: https://github.com/syntax-tree/hast#text +[github-xastscript]: https://github.com/syntax-tree/xastscript -[u]: https://github.com/syntax-tree/unist-builder +[health]: https://github.com/syntax-tree/.github -[build-jsx]: https://github.com/wooorm/estree-util-build-jsx +[health-coc]: https://github.com/syntax-tree/.github/blob/main/code-of-conduct.md -[bublé]: https://github.com/Rich-Harris/buble +[health-contributing]: https://github.com/syntax-tree/.github/blob/main/contributing.md -[babel]: https://github.com/babel/babel +[health-support]: https://github.com/syntax-tree/.github/blob/main/support.md -[babel-jsx]: https://github.com/babel/babel/tree/main/packages/babel-plugin-transform-react-jsx +[npmjs-install]: https://docs.npmjs.com/cli/install -[parse-selector]: https://github.com/syntax-tree/hast-util-parse-selector +[typescript]: https://www.typescriptlang.org -[xss]: https://en.wikipedia.org/wiki/Cross-site_scripting +[wikipedia-xss]: https://en.wikipedia.org/wiki/Cross-site_scripting -[sanitize]: https://github.com/syntax-tree/hast-util-sanitize +[wooorm]: https://wooorm.com diff --git a/script/build.js b/script/build.js index 9dd2f12..b949f0f 100644 --- a/script/build.js +++ b/script/build.js @@ -1,12 +1,23 @@ -import fs from 'node:fs' -import path from 'node:path' +import fs from 'node:fs/promises' import {svgTagNames} from 'svg-tag-names' -const casing = svgTagNames.filter((d) => d !== d.toLowerCase()) +/** @type {Array} */ +const irregular = [] -fs.writeFileSync( - path.join('lib', 'svg-case-sensitive-tag-names.js'), - 'export const svgCaseSensitiveTagNames = ' + - JSON.stringify(casing, null, 2) + - '\n' +for (const name of svgTagNames) { + if (name !== name.toLowerCase()) irregular.push(name) +} + +await fs.writeFile( + new URL('../lib/svg-case-sensitive-tag-names.js', import.meta.url), + [ + '/**', + ' * List of case-sensitive SVG tag names.', + ' *', + ' * @type {ReadonlyArray}', + ' */', + 'export const svgCaseSensitiveTagNames = ' + + JSON.stringify(irregular, undefined, 2), + '' + ].join('\n') ) diff --git a/script/generate-jsx.js b/script/generate-jsx.js index 3aa56a4..2a28c7d 100644 --- a/script/generate-jsx.js +++ b/script/generate-jsx.js @@ -1,62 +1,52 @@ -import fs from 'node:fs' -import path from 'node:path' -import babel from '@babel/core' -import {Parser} from 'acorn' +import fs from 'node:fs/promises' import acornJsx from 'acorn-jsx' -import {generate} from 'astring' import {buildJsx} from 'estree-util-build-jsx' +import {fromJs} from 'esast-util-from-js' +import {toJs} from 'estree-util-to-js' -const doc = String(fs.readFileSync(path.join('test', 'jsx.jsx'))) - -fs.writeFileSync( - path.join('test', 'jsx-build-jsx-classic.js'), - generate( - buildJsx( - // @ts-expect-error Acorn nodes are assignable to ESTree nodes. - Parser.extend(acornJsx()).parse( - doc.replace(/'name'/, "'jsx (estree-util-build-jsx, classic)'"), - {sourceType: 'module', ecmaVersion: 2021} - ), - {pragma: 'h', pragmaFrag: 'null'} - ) - ) +const document = await fs.readFile( + new URL('../test/jsx.jsx', import.meta.url), + 'utf8' ) -fs.writeFileSync( - path.join('test', 'jsx-build-jsx-automatic.js'), - generate( - buildJsx( - // @ts-expect-error Acorn nodes are assignable to ESTree nodes. - Parser.extend(acornJsx()).parse( - doc.replace(/'name'/, "'jsx (estree-util-build-jsx, automatic)'"), - {sourceType: 'module', ecmaVersion: 2021} - ), - {runtime: 'automatic', importSource: '.'} - ) - ).replace(/\/jsx-runtime(?=["'])/g, './lib/runtime-html.js') +const treeAutomatic = fromJs( + document.replace(/'name'/, "'jsx (estree-util-build-jsx, automatic)'"), + {module: true, plugins: [acornJsx()]} ) -fs.writeFileSync( - path.join('test', 'jsx-babel-classic.js'), - // @ts-expect-error Result always given. - babel.transform(doc.replace(/'name'/, "'jsx (babel, classic)'"), { - plugins: [ - ['@babel/plugin-transform-react-jsx', {pragma: 'h', pragmaFrag: 'null'}] - ] - }).code +const treeAutomaticDevelopment = fromJs( + document.replace( + /'name'/, + "'jsx (estree-util-build-jsx, automatic, development)'" + ), + {module: true, plugins: [acornJsx()]} ) -fs.writeFileSync( - path.join('test', 'jsx-babel-automatic.js'), - // @ts-expect-error Result always given. - babel - .transformSync(doc.replace(/'name'/, "'jsx (babel, automatic)'"), { - plugins: [ - [ - '@babel/plugin-transform-react-jsx', - {runtime: 'automatic', importSource: '.'} - ] - ] - }) - .code.replace(/\/jsx-runtime(?=["'])/g, './lib/runtime-html.js') +const treeClassic = fromJs( + document.replace(/'name'/, "'jsx (estree-util-build-jsx, classic)'"), + {module: true, plugins: [acornJsx()]} +) + +buildJsx(treeAutomatic, {importSource: 'hastscript', runtime: 'automatic'}) +buildJsx(treeAutomaticDevelopment, { + development: true, + importSource: 'hastscript', + runtime: 'automatic' +}) +buildJsx(treeClassic, {pragmaFrag: 'null', pragma: 'h'}) + +await fs.writeFile( + new URL('../test/jsx-build-jsx-automatic.js', import.meta.url), + toJs(treeAutomatic).value +) + +await fs.writeFile( + new URL('../test/jsx-build-jsx-automatic-development.js', import.meta.url), + // There’s a problem with `this` that TS doesn’t like. + '// @ts-nocheck\n\n' + toJs(treeAutomaticDevelopment).value +) + +await fs.writeFile( + new URL('../test/jsx-build-jsx-classic.js', import.meta.url), + toJs(treeClassic).value ) diff --git a/svg.js b/svg.js deleted file mode 100644 index 0576ff1..0000000 --- a/svg.js +++ /dev/null @@ -1,6 +0,0 @@ -/** - * @typedef {import('./lib/index.js').Child} Child Acceptable child value - * @typedef {import('./lib/index.js').Properties} Properties Acceptable properties value. - */ - -export {s} from './lib/svg.js' diff --git a/svg/jsx-runtime.js b/svg/jsx-runtime.js deleted file mode 100644 index f34e22b..0000000 --- a/svg/jsx-runtime.js +++ /dev/null @@ -1,5 +0,0 @@ -/** - * @typedef {import('../lib/runtime.js').JSXProps}} JSXProps - */ - -export * from '../lib/runtime-svg.js' diff --git a/test-d/automatic-h.tsx b/test-d/automatic-h.tsx index a45c8df..ca0085c 100644 --- a/test-d/automatic-h.tsx +++ b/test-d/automatic-h.tsx @@ -1,22 +1,13 @@ /* @jsxRuntime automatic */ -/* @jsxImportSource .. */ +/* @jsxImportSource hastscript */ -import {expectType, expectError} from 'tsd' -import {Root, Element} from 'hast' -import {h} from '../index.js' -import {Fragment, jsx, jsxs} from '../jsx-runtime.js' +import {h} from 'hastscript' +import type {Element, Root} from 'hast' +import {expectType} from 'tsd' type Result = Element | Root // JSX automatic runtime. -expectType(jsx(Fragment, {})) -expectType(jsx(Fragment, {children: h('h')})) -expectType(jsx('a', {})) -expectType(jsx('a', {children: 'a'})) -expectType(jsx('a', {children: h('h')})) -expectType(jsxs('a', {children: ['a', 'b']})) -expectType(jsxs('a', {children: [h('x'), h('y')]})) - expectType(<>) expectType() expectType() @@ -45,11 +36,15 @@ expectType({[, ]}) expectType({[, ]}) expectType({[]}) -expectError() +// @ts-expect-error: not a valid property value. +const a = // This is where the automatic runtime differs from the classic runtime. // The automatic runtime the children prop to define JSX children, whereas it’s used as an attribute in the classic runtime. + expectType(} />) -declare function Bar(props?: Record): Element -expectError() +declare function Bar(properties?: Record): Element + +// @ts-expect-error: components are not supported. +const b = diff --git a/test-d/automatic-s.tsx b/test-d/automatic-s.tsx index a7ccf46..cea80ee 100644 --- a/test-d/automatic-s.tsx +++ b/test-d/automatic-s.tsx @@ -1,9 +1,9 @@ /* @jsxRuntime automatic */ -/* @jsxImportSource ../svg */ +/* @jsxImportSource hastscript/svg */ -import {expectType, expectError} from 'tsd' -import {Root, Element} from 'hast' -import {s} from '../index.js' +import {s} from 'hastscript' +import type {Element, Root} from 'hast' +import {expectType} from 'tsd' type Result = Element | Root @@ -35,11 +35,15 @@ expectType({[, ]}) expectType({[, ]}) expectType({[]}) -expectError() +// @ts-expect-error: not a valid property value. +const a = // This is where the automatic runtime differs from the classic runtime. // The automatic runtime the children prop to define JSX children, whereas it’s used as an attribute in the classic runtime. + expectType(} />) -declare function Bar(props?: Record): Element -expectError() +declare function Bar(properties?: Record): Element + +// @ts-expect-error: components are not supported. +const b = diff --git a/test-d/classic-h.tsx b/test-d/classic-h.tsx index 6b143c0..0eca873 100644 --- a/test-d/classic-h.tsx +++ b/test-d/classic-h.tsx @@ -1,8 +1,9 @@ -/* @jsx h */ /* @jsxFrag null */ -import {expectType, expectError} from 'tsd' -import {Root, Element} from 'hast' -import {h} from '../index.js' +/* @jsx h */ + +import {h} from 'hastscript' +import type {Element, Root} from 'hast' +import {expectType} from 'tsd' type Result = Element | Root @@ -34,12 +35,16 @@ expectType({[, ]}) expectType({[, ]}) expectType({[]}) -expectError() +// @ts-expect-error: not a valid property value. +const a = + +// @ts-expect-error: This is where the classic runtime differs from the +// automatic runtime. +// The automatic runtime the children prop to define JSX children, whereas +// it’s used as an attribute in the classic runtime. +const b = } /> -// This is where the classic runtime differs from the automatic runtime. -// The automatic runtime the children prop to define JSX children, whereas it’s -// used as an attribute in the classic runtime. -expectError(} />) +declare function Bar(properties?: Record): Element -declare function Bar(props?: Record): Element -expectError() +// @ts-expect-error: components are not supported. +const c = diff --git a/test-d/classic-s.tsx b/test-d/classic-s.tsx index 9431f7b..5919e16 100644 --- a/test-d/classic-s.tsx +++ b/test-d/classic-s.tsx @@ -1,8 +1,9 @@ -/* @jsx s */ /* @jsxFrag null */ -import {expectType, expectError} from 'tsd' -import {Root, Element} from 'hast' -import {s} from '../index.js' +/* @jsx s */ + +import {s} from 'hastscript' +import type {Element, Root} from 'hast' +import {expectType} from 'tsd' type Result = Element | Root @@ -34,12 +35,16 @@ expectType({[, ]}) expectType({[, ]}) expectType({[]}) -expectError() +// @ts-expect-error: not a valid property value. +const a = + +// @ts-expect-error: This is where the classic runtime differs from the +// automatic runtime. +// The automatic runtime the children prop to define JSX children, whereas +// it’s used as an attribute in the classic runtime. +const b = } /> -// This is where the classic runtime differs from the automatic runtime. -// The automatic runtime the children prop to define JSX children, whereas it’s -// used as an attribute in the classic runtime. -expectError(} />) +declare function Bar(properties?: Record): Element -declare function Bar(props?: Record): Element -expectError() +// @ts-expect-error: components are not supported. +const c = diff --git a/test-d/files.ts b/test-d/files.ts index 5d6c415..d73d50e 100644 --- a/test-d/files.ts +++ b/test-d/files.ts @@ -1,10 +1,6 @@ +import {h, s} from 'hastscript' +import type {Root} from 'hast' import {expectType} from 'tsd' -import {Root} from 'hast' -import {h as hFromRoot} from '../html.js' -import {s as sFromRoot} from '../svg.js' -import {h as hFromIndex, s as sFromIndex} from '../index.js' -expectType(hFromRoot()) -expectType(hFromIndex()) -expectType(sFromRoot()) -expectType(sFromIndex()) +expectType(h()) +expectType(s()) diff --git a/test-d/index.ts b/test-d/index.ts index c55abf7..d43bb0b 100644 --- a/test-d/index.ts +++ b/test-d/index.ts @@ -1,17 +1,20 @@ -import {expectType, expectError} from 'tsd' -import {Root, Element} from 'hast' -import {h, s} from '../index.js' -import {h as hFromRoot} from '../html.js' -import {s as sFromRoot} from '../svg.js' -import {Fragment, jsx, jsxs} from '../jsx-runtime.js' +import {Fragment, jsxs, jsx} from 'hastscript/jsx-runtime' +import {h, s} from 'hastscript' +import type {Element, Root} from 'hast' +import {expectType} from 'tsd' -// Ensure files are loadable in TS. -expectType(hFromRoot()) -expectType(sFromRoot()) +expectType(jsx(Fragment, {})) +expectType(jsx(Fragment, {children: h('h')})) +expectType(jsx('a', {})) +expectType(jsx('a', {children: 'a'})) +expectType(jsx('a', {children: h('h')})) +expectType(jsxs('a', {children: ['a', 'b']})) +expectType(jsxs('a', {children: [h('x'), h('y')]})) expectType(h()) expectType(s()) -expectError(h(true)) +// @ts-expect-error: not a tag name. +h(true) expectType(h(null)) expectType(h(undefined)) expectType(h('')) @@ -20,9 +23,11 @@ expectType(h('', null)) expectType(h('', undefined)) expectType(h('', 1)) expectType(h('', 'a')) -expectError(h('', true)) +// @ts-expect-error: not a child. +h('', true) expectType(h('', [1, 'a', null])) -expectError(h('', [true])) +// @ts-expect-error: not a child. +h('', [true]) expectType(h('', {})) expectType(h('', {}, [1, 'a', null])) @@ -33,22 +38,16 @@ expectType(h('', {p: true})) expectType(h('', {p: false})) expectType(h('', {p: 'a'})) expectType(h('', {p: [1]})) -expectError(h('', {p: [true]})) +// @ts-expect-error: not a property value. +h('', {p: [true]}) expectType(h('', {p: ['a']})) expectType(h('', {p: {x: 1}})) // Style -expectError(h('', {p: {x: true}})) +// @ts-expect-error: not a property value. +h('', {p: {x: true}}) expectType( - s('svg', {xmlns: 'http://www.w3.org/2000/svg', viewbox: '0 0 500 500'}, [ + s('svg', {viewbox: '0 0 500 500', xmlns: 'http://www.w3.org/2000/svg'}, [ s('title', 'SVG `(jsx(Fragment, {})) -expectType(jsx(Fragment, {children: h('x')})) -expectType(jsx('a', {})) -expectType(jsx('a', {children: 'a'})) -expectType(jsx('a', {children: h('x')})) -expectType(jsxs('a', {children: ['a', 'b']})) -expectType(jsxs('a', {children: [h('x'), h('y')]})) diff --git a/test/core.js b/test/core.js index 07dc9f7..4bd8ae9 100644 --- a/test/core.js +++ b/test/core.js @@ -1,730 +1,707 @@ -import test from 'tape' -import {h, s} from '../index.js' -import {h as hFromRoot} from '../html.js' -import {s as sFromRoot} from '../svg.js' - -test('hastscript', (t) => { - t.equal(h, hFromRoot, '`h` should be exposed from `/html.js`') - t.equal(s, sFromRoot, '`s` should be exposed from `/svg.js`') - - t.equal(typeof h, 'function', 'should expose a function') - - t.test('selector', (t) => { - t.deepEqual( - h(), - {type: 'root', children: []}, - 'should create a `root` node without arguments' - ) +import assert from 'node:assert/strict' +import test from 'node:test' +import {h, s} from 'hastscript' - t.deepEqual( - h(''), - { - type: 'element', - tagName: 'div', - properties: {}, - children: [] - }, - 'should create a `div` element w/ an empty string name' - ) +test('core', async function (t) { + await t.test('should expose the public api (`/`)', async function () { + assert.deepEqual(Object.keys(await import('hastscript')).sort(), ['h', 's']) + }) - t.deepEqual( - h('.bar', {class: 'baz'}), - { + await t.test( + 'should expose the public api (`/jsx-runtime`)', + async function () { + assert.deepEqual( + Object.keys(await import('hastscript/jsx-runtime')).sort(), + ['Fragment', 'jsx', 'jsxDEV', 'jsxs'] + ) + } + ) + + await t.test( + 'should expose the public api (`/jsx-dev-runtime`)', + async function () { + assert.deepEqual( + Object.keys(await import('hastscript/jsx-dev-runtime')).sort(), + ['Fragment', 'jsx', 'jsxDEV', 'jsxs'] + ) + } + ) + + await t.test( + 'should expose the public api (`/svg/jsx-runtime`)', + async function () { + assert.deepEqual( + Object.keys(await import('hastscript/svg/jsx-runtime')).sort(), + ['Fragment', 'jsx', 'jsxDEV', 'jsxs'] + ) + } + ) + + await t.test( + 'should expose the public api (`/svg/jsx-dev-runtime`)', + async function () { + assert.deepEqual( + Object.keys(await import('hastscript/svg/jsx-dev-runtime')).sort(), + ['Fragment', 'jsx', 'jsxDEV', 'jsxs'] + ) + } + ) +}) + +test('selector', async function (t) { + await t.test( + 'should create a `root` node without arguments', + async function () { + assert.deepEqual(h(), {type: 'root', children: []}) + } + ) + + await t.test( + 'should create a `div` element w/ an empty string name', + async function () { + assert.deepEqual(h(''), { type: 'element', tagName: 'div', - properties: {className: ['bar', 'baz']}, + properties: {}, children: [] - }, - 'should append to the selector’s classes' - ) + }) + } + ) + + await t.test('should append to the selector’s classes', async function () { + assert.deepEqual(h('.bar', {class: 'baz'}), { + type: 'element', + tagName: 'div', + properties: {className: ['bar', 'baz']}, + children: [] + }) + }) - t.deepEqual( - h('#id'), - { + await t.test( + 'should create a `div` element when given an id selector', + async function () { + assert.deepEqual(h('#id'), { type: 'element', tagName: 'div', properties: {id: 'id'}, children: [] - }, - 'should create a `div` element when given an id selector' - ) - - t.deepEqual( - h('#a#b'), - { + }) + } + ) + + await t.test( + 'should create an element with the last ID when given multiple in a selector', + async function () { + assert.deepEqual(h('#a#b'), { type: 'element', tagName: 'div', properties: {id: 'b'}, children: [] - }, - 'should create an element with the last ID when given multiple in a selector' - ) - - t.deepEqual( - h('.foo'), - { + }) + } + ) + + await t.test( + 'should create a `div` element when given a class selector', + async function () { + assert.deepEqual(h('.foo'), { type: 'element', tagName: 'div', properties: {className: ['foo']}, children: [] - }, - 'should create a `div` element when given a class selector' - ) - - t.deepEqual( - h('foo'), - { + }) + } + ) + + await t.test( + 'should create a `foo` element when given a tag selector', + async function () { + assert.deepEqual(h('foo'), { type: 'element', tagName: 'foo', properties: {}, children: [] - }, - 'should create a `foo` element when given a tag selector' - ) - - t.deepEqual( - h('foo#bar'), - { + }) + } + ) + + await t.test( + 'should create a `foo` element with an ID when given a both as a selector', + async function () { + assert.deepEqual(h('foo#bar'), { type: 'element', tagName: 'foo', properties: {id: 'bar'}, children: [] - }, - 'should create a `foo` element with an ID when given a both as a selector' - ) - - t.deepEqual( - h('foo.bar'), - { + }) + } + ) + + await t.test( + 'should create a `foo` element with a class when given a both as a selector', + async function () { + assert.deepEqual(h('foo.bar'), { type: 'element', tagName: 'foo', properties: {className: ['bar']}, children: [] - }, - 'should create a `foo` element with a class when given a both as a selector' - ) + }) + } + ) + + await t.test('should support multiple classes', async function () { + assert.deepEqual(h('.foo.bar'), { + type: 'element', + tagName: 'div', + properties: {className: ['foo', 'bar']}, + children: [] + }) + }) +}) - t.deepEqual( - h('.foo.bar'), - { +test('property names', async function (t) { + await t.test( + 'should support correctly cased property names', + async function () { + assert.deepEqual(h('', {className: 'foo'}), { type: 'element', tagName: 'div', - properties: {className: ['foo', 'bar']}, + properties: {className: ['foo']}, children: [] - }, - 'should support multiple classes' - ) - - t.end() + }) + } + ) + + await t.test('should map attributes to property names', async function () { + assert.deepEqual(h('', {class: 'foo'}), { + type: 'element', + tagName: 'div', + properties: {className: ['foo']}, + children: [] + }) }) - t.test('properties', (t) => { - t.test('known property names', (t) => { - t.deepEqual( - h('', {className: 'foo'}), - { - type: 'element', - tagName: 'div', - properties: {className: ['foo']}, - children: [] - }, - 'should support correctly cased property names' - ) - - t.deepEqual( - h('', {class: 'foo'}), - { - type: 'element', - tagName: 'div', - properties: {className: ['foo']}, - children: [] - }, - 'should map attributes to property names' - ) - - t.deepEqual( - h('', {CLASS: 'foo'}), - { - type: 'element', - tagName: 'div', - properties: {className: ['foo']}, - children: [] - }, - 'should map attribute-like values to property names' - ) - - t.deepEqual( - h('', {'class-name': 'foo'}), - { - type: 'element', - tagName: 'div', - properties: {'class-name': 'foo'}, - children: [] - }, - 'should *not* map property-like values to property names' - ) + await t.test( + 'should map attribute-like values to property names', + async function () { + assert.deepEqual(h('', {CLASS: 'foo'}), { + type: 'element', + tagName: 'div', + properties: {className: ['foo']}, + children: [] + }) + } + ) + + await t.test( + 'should *not* map property-like values to property names', + async function () { + assert.deepEqual(h('', {'class-name': 'foo'}), { + type: 'element', + tagName: 'div', + properties: {'class-name': 'foo'}, + children: [] + }) + } + ) +}) - t.end() +test('property names (unknown)', async function (t) { + await t.test('should keep lower-cased unknown names', async function () { + assert.deepEqual(h('', {allowbigscreen: true}), { + type: 'element', + tagName: 'div', + properties: {allowbigscreen: true}, + children: [] }) + }) - t.test('unknown property names', (t) => { - t.deepEqual( - h('', {allowbigscreen: true}), - { - type: 'element', - tagName: 'div', - properties: {allowbigscreen: true}, - children: [] - }, - 'should keep lower-cased unknown names' - ) - - t.deepEqual( - h('', {allowBigScreen: true}), - { - type: 'element', - tagName: 'div', - properties: {allowBigScreen: true}, - children: [] - }, - 'should keep camel-cased unknown names' - ) - - t.deepEqual( - h('', {'allow_big-screen': true}), - { - type: 'element', - tagName: 'div', - properties: {'allow_big-screen': true}, - children: [] - }, - 'should keep weirdly cased unknown names' - ) - - t.end() + await t.test('should keep camel-cased unknown names', async function () { + assert.deepEqual(h('', {allowBigScreen: true}), { + type: 'element', + tagName: 'div', + properties: {allowBigScreen: true}, + children: [] }) + }) - t.test('other namespaces', (t) => { - t.deepEqual( - h('', {'aria-valuenow': 1}), - { - type: 'element', - tagName: 'div', - properties: {ariaValueNow: 1}, - children: [] - }, - 'should support aria attribute names' - ) - - t.deepEqual( - h('', {ariaValueNow: 1}), - { - type: 'element', - tagName: 'div', - properties: {ariaValueNow: 1}, - children: [] - }, - 'should support aria property names' - ) - - t.deepEqual( - s('', {'color-interpolation-filters': 'sRGB'}), - { - type: 'element', - tagName: 'g', - properties: {colorInterpolationFilters: 'sRGB'}, - children: [] - }, - 'should support svg attribute names' - ) - - t.deepEqual( - s('', {colorInterpolationFilters: 'sRGB'}), - { - type: 'element', - tagName: 'g', - properties: {colorInterpolationFilters: 'sRGB'}, - children: [] - }, - 'should support svg property names' - ) - - t.deepEqual( - s('', {'xml:space': 'preserve'}), - { - type: 'element', - tagName: 'g', - properties: {xmlSpace: 'preserve'}, - children: [] - }, - 'should support xml attribute names' - ) - - t.deepEqual( - s('', {xmlSpace: 'preserve'}), - { - type: 'element', - tagName: 'g', - properties: {xmlSpace: 'preserve'}, - children: [] - }, - 'should support xml property names' - ) - - t.deepEqual( - s('', {'xmlns:xlink': 'http://www.w3.org/1999/xlink'}), - { - type: 'element', - tagName: 'g', - properties: {xmlnsXLink: 'http://www.w3.org/1999/xlink'}, - children: [] - }, - 'should support xmlns attribute names' - ) - - t.deepEqual( - s('', {xmlnsXLink: 'http://www.w3.org/1999/xlink'}), - { - type: 'element', - tagName: 'g', - properties: {xmlnsXLink: 'http://www.w3.org/1999/xlink'}, - children: [] - }, - 'should support xmlns property names' - ) - - t.deepEqual( - s('', {'xlink:arcrole': 'http://www.example.com'}), - { - type: 'element', - tagName: 'g', - properties: {xLinkArcRole: 'http://www.example.com'}, - children: [] - }, - 'should support xlink attribute names' - ) - - t.deepEqual( - s('', {xLinkArcRole: 'http://www.example.com'}), - { - type: 'element', - tagName: 'g', - properties: {xLinkArcRole: 'http://www.example.com'}, - children: [] - }, - 'should support xlink property names' - ) - - t.end() + await t.test('should keep weirdly cased unknown names', async function () { + assert.deepEqual(h('', {'allow_big-screen': true}), { + type: 'element', + tagName: 'div', + properties: {'allow_big-screen': true}, + children: [] }) + }) +}) - t.test('data property names', (t) => { - t.deepEqual( - h('', {'data-foo': true}), - { - type: 'element', - tagName: 'div', - properties: {dataFoo: true}, - children: [] - }, - 'should support data attribute names' - ) - - t.deepEqual( - h('', {'data-123': true}), - { - type: 'element', - tagName: 'div', - properties: {data123: true}, - children: [] - }, - 'should support numeric-first data attribute names' - ) - - t.deepEqual( - h('', {dataFooBar: true}), - { - type: 'element', - tagName: 'div', - properties: {dataFooBar: true}, - children: [] - }, - 'should support data property names' - ) - - t.deepEqual( - h('', {data123: true}), - { - type: 'element', - tagName: 'div', - properties: {data123: true}, - children: [] - }, - 'should support numeric-first data property names' - ) - - t.deepEqual( - h('', {'data-foo.bar': true}), - { - type: 'element', - tagName: 'div', - properties: {'dataFoo.bar': true}, - children: [] - }, - 'should support data attribute names with uncommon characters' - ) - - t.deepEqual( - h('', {'dataFoo.bar': true}), - { - type: 'element', - tagName: 'div', - properties: {'dataFoo.bar': true}, - children: [] - }, - 'should support data property names with uncommon characters' - ) - - t.deepEqual( - h('', {'data-foo!bar': true}), - { - type: 'element', - tagName: 'div', - properties: {'data-foo!bar': true}, - children: [] - }, - 'should keep invalid data attribute names' - ) - - t.deepEqual( - h('', {'dataFoo!bar': true}), - { - type: 'element', - tagName: 'div', - properties: {'dataFoo!bar': true}, - children: [] - }, - 'should keep invalid data property names' - ) - - t.end() +test('property names (other)', async function (t) { + await t.test('should support aria attribute names', async function () { + assert.deepEqual(h('', {'aria-valuenow': 1}), { + type: 'element', + tagName: 'div', + properties: {ariaValueNow: 1}, + children: [] }) + }) - t.test('unknown property values', (t) => { - t.deepEqual( - h('', {foo: 'bar'}), - { - type: 'element', - tagName: 'div', - properties: {foo: 'bar'}, - children: [] - }, - 'should support unknown `string` values' - ) - - t.deepEqual( - h('', {foo: 3}), - { - type: 'element', - tagName: 'div', - properties: {foo: 3}, - children: [] - }, - 'should support unknown `number` values' - ) - - t.deepEqual( - h('', {foo: true}), - { - type: 'element', - tagName: 'div', - properties: {foo: true}, - children: [] - }, - 'should support unknown `boolean` values' - ) - - t.deepEqual( - h('', {list: ['bar', 'baz']}), - { - type: 'element', - tagName: 'div', - properties: {list: ['bar', 'baz']}, - children: [] - }, - 'should support unknown `Array` values' - ) - - t.deepEqual( - h('', {foo: null}), - { - type: 'element', - tagName: 'div', - properties: {}, - children: [] - }, - 'should ignore properties with a value of `null`' - ) - - t.deepEqual( - h('', {foo: undefined}), - { - type: 'element', - tagName: 'div', - properties: {}, - children: [] - }, - 'should ignore properties with a value of `undefined`' - ) + await t.test('should support aria property names', async function () { + assert.deepEqual(h('', {ariaValueNow: 1}), { + type: 'element', + tagName: 'div', + properties: {ariaValueNow: 1}, + children: [] + }) + }) - t.deepEqual( - h('', {foo: Number.NaN}), - { - type: 'element', - tagName: 'div', - properties: {}, - children: [] - }, - 'should ignore properties with a value of `NaN`' - ) + await t.test('should support svg attribute names', async function () { + assert.deepEqual(s('', {'color-interpolation-filters': 'sRGB'}), { + type: 'element', + tagName: 'g', + properties: {colorInterpolationFilters: 'sRGB'}, + children: [] + }) + }) - t.end() + await t.test('should support svg property names', async function () { + assert.deepEqual(s('', {colorInterpolationFilters: 'sRGB'}), { + type: 'element', + tagName: 'g', + properties: {colorInterpolationFilters: 'sRGB'}, + children: [] }) + }) - t.test('known booleans', (t) => { - t.deepEqual( - h('', {allowFullScreen: ''}), - { - type: 'element', - tagName: 'div', - properties: {allowFullScreen: true}, - children: [] - }, - 'should cast valid known `boolean` values' - ) + await t.test('should support xml attribute names', async function () { + assert.deepEqual(s('', {'xml:space': 'preserve'}), { + type: 'element', + tagName: 'g', + properties: {xmlSpace: 'preserve'}, + children: [] + }) + }) - t.deepEqual( - h('', {allowFullScreen: 'yup'}), - { - type: 'element', - tagName: 'div', - properties: {allowFullScreen: 'yup'}, - children: [] - }, - 'should not cast invalid known `boolean` values' - ) + await t.test('should support xml property names', async function () { + assert.deepEqual(s('', {xmlSpace: 'preserve'}), { + type: 'element', + tagName: 'g', + properties: {xmlSpace: 'preserve'}, + children: [] + }) + }) - t.deepEqual( - h('img', {title: 'title'}), - { - type: 'element', - tagName: 'img', - properties: {title: 'title'}, - children: [] - }, - 'should not cast unknown boolean-like values' - ) + await t.test('should support xmlns attribute names', async function () { + assert.deepEqual(s('', {'xmlns:xlink': 'http://www.w3.org/1999/xlink'}), { + type: 'element', + tagName: 'g', + properties: {xmlnsXLink: 'http://www.w3.org/1999/xlink'}, + children: [] + }) + }) - t.end() + await t.test('should support xmlns property names', async function () { + assert.deepEqual(s('', {xmlnsXLink: 'http://www.w3.org/1999/xlink'}), { + type: 'element', + tagName: 'g', + properties: {xmlnsXLink: 'http://www.w3.org/1999/xlink'}, + children: [] }) + }) - t.test('known overloaded booleans', (t) => { - t.deepEqual( - h('', {download: ''}), - { - type: 'element', - tagName: 'div', - properties: {download: true}, - children: [] - }, - 'should cast known empty overloaded `boolean` values' - ) + await t.test('should support xlink attribute names', async function () { + assert.deepEqual(s('', {'xlink:arcrole': 'http://www.example.com'}), { + type: 'element', + tagName: 'g', + properties: {xLinkArcRole: 'http://www.example.com'}, + children: [] + }) + }) - t.deepEqual( - h('', {download: 'downLOAD'}), - { - type: 'element', - tagName: 'div', - properties: {download: true}, - children: [] - }, - 'should cast known named overloaded `boolean` values' - ) + await t.test('should support xlink property names', async function () { + assert.deepEqual(s('', {xLinkArcRole: 'http://www.example.com'}), { + type: 'element', + tagName: 'g', + properties: {xLinkArcRole: 'http://www.example.com'}, + children: [] + }) + }) +}) - t.deepEqual( - h('', {download: 'example.ogg'}), - { - type: 'element', - tagName: 'div', - properties: {download: 'example.ogg'}, - children: [] - }, - 'should not cast overloaded `boolean` values for different values' - ) +test('data property names', async function (t) { + await t.test('should support data attribute names', async function () { + assert.deepEqual(h('', {'data-foo': true}), { + type: 'element', + tagName: 'div', + properties: {dataFoo: true}, + children: [] + }) + }) - t.end() + await t.test( + 'should support numeric-first data attribute names', + async function () { + assert.deepEqual(h('', {'data-123': true}), { + type: 'element', + tagName: 'div', + properties: {data123: true}, + children: [] + }) + } + ) + + await t.test('should support data property names', async function () { + assert.deepEqual(h('', {dataFooBar: true}), { + type: 'element', + tagName: 'div', + properties: {dataFooBar: true}, + children: [] }) + }) - t.test('known numbers', (t) => { - t.deepEqual( - h('textarea', {cols: '3'}), - { - type: 'element', - tagName: 'textarea', - properties: {cols: 3}, - children: [] - }, - 'should cast valid known `numeric` values' - ) + await t.test( + 'should support numeric-first data property names', + async function () { + assert.deepEqual(h('', {data123: true}), { + type: 'element', + tagName: 'div', + properties: {data123: true}, + children: [] + }) + } + ) + + await t.test( + 'should support data attribute names with uncommon characters', + async function () { + assert.deepEqual(h('', {'data-foo.bar': true}), { + type: 'element', + tagName: 'div', + properties: {'dataFoo.bar': true}, + children: [] + }) + } + ) + + await t.test( + 'should support data property names with uncommon characters', + async function () { + assert.deepEqual(h('', {'dataFoo.bar': true}), { + type: 'element', + tagName: 'div', + properties: {'dataFoo.bar': true}, + children: [] + }) + } + ) + + await t.test('should keep invalid data attribute names', async function () { + assert.deepEqual(h('', {'data-foo!bar': true}), { + type: 'element', + tagName: 'div', + properties: {'data-foo!bar': true}, + children: [] + }) + }) - t.deepEqual( - h('textarea', {cols: 'one'}), - { - type: 'element', - tagName: 'textarea', - properties: {cols: 'one'}, - children: [] - }, - 'should not cast invalid known `numeric` values' - ) + await t.test('should keep invalid data property names', async function () { + assert.deepEqual(h('', {'dataFoo!bar': true}), { + type: 'element', + tagName: 'div', + properties: {'dataFoo!bar': true}, + children: [] + }) + }) +}) - t.deepEqual( - h('meter', {low: '40', high: '90'}), - { - type: 'element', - tagName: 'meter', - properties: {low: 40, high: 90}, - children: [] - }, - 'should cast known `numeric` values' - ) +test('property values (unknown)', async function (t) { + await t.test('should support unknown `string` values', async function () { + assert.deepEqual(h('', {foo: 'bar'}), { + type: 'element', + tagName: 'div', + properties: {foo: 'bar'}, + children: [] + }) + }) - t.end() + await t.test('should support unknown `number` values', async function () { + assert.deepEqual(h('', {foo: 3}), { + type: 'element', + tagName: 'div', + properties: {foo: 3}, + children: [] }) + }) - t.test('known lists', (t) => { - t.deepEqual( - h('', {class: 'foo bar baz'}), - { - type: 'element', - tagName: 'div', - properties: {className: ['foo', 'bar', 'baz']}, - children: [] - }, - 'should cast know space-separated `array` values' - ) + await t.test('should support unknown `boolean` values', async function () { + assert.deepEqual(h('', {foo: true}), { + type: 'element', + tagName: 'div', + properties: {foo: true}, + children: [] + }) + }) - t.deepEqual( - h('input', {type: 'file', accept: 'video/*, image/*'}), - { - type: 'element', - tagName: 'input', - properties: {type: 'file', accept: ['video/*', 'image/*']}, - children: [] - }, - 'should cast know comma-separated `array` values' - ) + await t.test('should support unknown `Array` values', async function () { + assert.deepEqual(h('', {list: ['bar', 'baz']}), { + type: 'element', + tagName: 'div', + properties: {list: ['bar', 'baz']}, + children: [] + }) + }) - t.deepEqual( - h('a', {coords: ['0', '0', '82', '126']}), - { - type: 'element', - tagName: 'a', - properties: {coords: [0, 0, 82, 126]}, - children: [] - }, - 'should cast a list of known `numeric` values' - ) + await t.test( + 'should ignore properties with a value of `null`', + async function () { + assert.deepEqual(h('', {foo: null}), { + type: 'element', + tagName: 'div', + properties: {}, + children: [] + }) + } + ) + + await t.test( + 'should ignore properties with a value of `undefined`', + async function () { + assert.deepEqual(h('', {foo: undefined}), { + type: 'element', + tagName: 'div', + properties: {}, + children: [] + }) + } + ) + + await t.test( + 'should ignore properties with a value of `NaN`', + async function () { + assert.deepEqual(h('', {foo: Number.NaN}), { + type: 'element', + tagName: 'div', + properties: {}, + children: [] + }) + } + ) +}) - t.end() +test('boolean properties', async function (t) { + await t.test('should cast valid known `boolean` values', async function () { + assert.deepEqual(h('', {allowFullScreen: ''}), { + type: 'element', + tagName: 'div', + properties: {allowFullScreen: true}, + children: [] }) + }) - t.test('style', (t) => { - t.deepEqual( - h('', {style: {color: 'red', '-webkit-border-radius': '3px'}}), - { - type: 'element', - tagName: 'div', - properties: { - style: 'color: red; -webkit-border-radius: 3px' - }, - children: [] - }, - 'should support `style` as an object' - ) + await t.test( + 'should not cast invalid known `boolean` values', + async function () { + assert.deepEqual(h('', {allowFullScreen: 'yup'}), { + type: 'element', + tagName: 'div', + properties: {allowFullScreen: 'yup'}, + children: [] + }) + } + ) + + await t.test( + 'should not cast unknown boolean-like values', + async function () { + assert.deepEqual(h('img', {title: 'title'}), { + type: 'element', + tagName: 'img', + properties: {title: 'title'}, + children: [] + }) + } + ) +}) - t.deepEqual( - h('', {style: 'color:/*red*/purple; -webkit-border-radius: 3px'}), - { - type: 'element', - tagName: 'div', - properties: { - style: 'color:/*red*/purple; -webkit-border-radius: 3px' - }, - children: [] - }, - 'should support `style` as a string' - ) +test('overloaded boolean properties', async function (t) { + await t.test( + 'should cast known empty overloaded `boolean` values', + async function () { + assert.deepEqual(h('', {download: ''}), { + type: 'element', + tagName: 'div', + properties: {download: true}, + children: [] + }) + } + ) + + await t.test( + 'should cast known named overloaded `boolean` values', + async function () { + assert.deepEqual(h('', {download: 'downLOAD'}), { + type: 'element', + tagName: 'div', + properties: {download: true}, + children: [] + }) + } + ) + + await t.test( + 'should not cast overloaded `boolean` values for different values', + async function () { + assert.deepEqual(h('', {download: 'example.ogg'}), { + type: 'element', + tagName: 'div', + properties: {download: 'example.ogg'}, + children: [] + }) + } + ) +}) - t.end() +test('number properties', async function (t) { + await t.test('should cast valid known `numeric` values', async function () { + assert.deepEqual(h('textarea', {cols: '3'}), { + type: 'element', + tagName: 'textarea', + properties: {cols: 3}, + children: [] }) - - t.end() }) - t.test('children', (t) => { - t.deepEqual( - h('div', {}, []), - { + await t.test( + 'should not cast invalid known `numeric` values', + async function () { + assert.deepEqual(h('textarea', {cols: 'one'}), { type: 'element', - tagName: 'div', - properties: {}, + tagName: 'textarea', + properties: {cols: 'one'}, children: [] - }, - 'should ignore no children' - ) + }) + } + ) + + await t.test('should cast known `numeric` values', async function () { + assert.deepEqual(h('meter', {low: '40', high: '90'}), { + type: 'element', + tagName: 'meter', + properties: {low: 40, high: 90}, + children: [] + }) + }) +}) - t.deepEqual( - h('div', {}, 'foo'), - { +test('list properties', async function (t) { + await t.test( + 'should cast know space-separated `array` values', + async function () { + assert.deepEqual(h('', {class: 'foo bar baz'}), { type: 'element', tagName: 'div', - properties: {}, - children: [{type: 'text', value: 'foo'}] - }, - 'should support `string` for a `Text`' - ) + properties: {className: ['foo', 'bar', 'baz']}, + children: [] + }) + } + ) + + await t.test( + 'should cast know comma-separated `array` values', + async function () { + assert.deepEqual(h('input', {type: 'file', accept: 'video/*, image/*'}), { + type: 'element', + tagName: 'input', + properties: {type: 'file', accept: ['video/*', 'image/*']}, + children: [] + }) + } + ) + + await t.test( + 'should cast a list of known `numeric` values', + async function () { + assert.deepEqual(h('a', {coords: ['0', '0', '82', '126']}), { + type: 'element', + tagName: 'a', + properties: {coords: [0, 0, 82, 126]}, + children: [] + }) + } + ) +}) - t.deepEqual( - h('div', {}, {type: 'text', value: 'foo'}), +test('style property', async function (t) { + await t.test('should support `style` as an object', async function () { + assert.deepEqual( + h('', {style: {color: 'red', '-webkit-border-radius': '3px'}}), { type: 'element', tagName: 'div', - properties: {}, - children: [{type: 'text', value: 'foo'}] - }, - 'should support a node' + properties: {style: 'color: red; -webkit-border-radius: 3px'}, + children: [] + } ) + }) - t.deepEqual( - h('div', {}, h('span', {}, 'foo')), + await t.test('should support `style` as a string', async function () { + assert.deepEqual( + h('', {style: 'color:/*red*/purple; -webkit-border-radius: 3px'}), { type: 'element', tagName: 'div', - properties: {}, - children: [ - { - type: 'element', - tagName: 'span', - properties: {}, - children: [{type: 'text', value: 'foo'}] - } - ] - }, - 'should support a node created by `h`' + properties: {style: 'color:/*red*/purple; -webkit-border-radius: 3px'}, + children: [] + } ) + }) +}) + +test('children', async function (t) { + await t.test('should ignore no children', async function () { + assert.deepEqual(h('div', {}, []), { + type: 'element', + tagName: 'div', + properties: {}, + children: [] + }) + }) + + await t.test('should support `string` for a `Text`', async function () { + assert.deepEqual(h('div', {}, 'foo'), { + type: 'element', + tagName: 'div', + properties: {}, + children: [{type: 'text', value: 'foo'}] + }) + }) + + await t.test('should support a node', async function () { + assert.deepEqual(h('div', {}, {type: 'text', value: 'foo'}), { + type: 'element', + tagName: 'div', + properties: {}, + children: [{type: 'text', value: 'foo'}] + }) + }) + + await t.test('should support a node created by `h`', async function () { + assert.deepEqual(h('div', {}, h('span', {}, 'foo')), { + type: 'element', + tagName: 'div', + properties: {}, + children: [ + { + type: 'element', + tagName: 'span', + properties: {}, + children: [{type: 'text', value: 'foo'}] + } + ] + }) + }) - t.deepEqual( + await t.test('should support nodes', async function () { + assert.deepEqual( h('div', {}, [ {type: 'text', value: 'foo'}, {type: 'text', value: 'bar'} @@ -737,11 +714,12 @@ test('hastscript', (t) => { {type: 'text', value: 'foo'}, {type: 'text', value: 'bar'} ] - }, - 'should support nodes' + } ) + }) - t.deepEqual( + await t.test('should support nodes created by `h`', async function () { + assert.deepEqual( h('div', {}, [h('span', {}, 'foo'), h('strong', {}, 'bar')]), { type: 'element', @@ -761,13 +739,14 @@ test('hastscript', (t) => { children: [{type: 'text', value: 'bar'}] } ] - }, - 'should support nodes created by `h`' + } ) + }) - t.deepEqual( - h('div', {}, ['foo', 'bar']), - { + await t.test( + 'should support `Array` for a `Text`s', + async function () { + assert.deepEqual(h('div', {}, ['foo', 'bar']), { type: 'element', tagName: 'div', properties: {}, @@ -775,157 +754,140 @@ test('hastscript', (t) => { {type: 'text', value: 'foo'}, {type: 'text', value: 'bar'} ] - }, - 'should support `Array.` for a `Text`s' - ) - - t.deepEqual( - h('strong', 'foo'), - { - type: 'element', - tagName: 'strong', - properties: {}, - children: [{type: 'text', value: 'foo'}] - }, - 'should allow omitting `properties` for a `string`' - ) - - t.deepEqual( - h('strong', h('span', 'foo')), - { - type: 'element', - tagName: 'strong', - properties: {}, - children: [ - { - type: 'element', - tagName: 'span', - properties: {}, - children: [{type: 'text', value: 'foo'}] - } - ] - }, - 'should allow omitting `properties` for a node' - ) - - t.deepEqual( - h('strong', ['foo', 'bar']), - { - type: 'element', - tagName: 'strong', - properties: {}, - children: [ - {type: 'text', value: 'foo'}, - {type: 'text', value: 'bar'} - ] - }, - 'should allow omitting `properties` for an array' - ) - - t.deepEqual( - h('input', {type: 'text', value: 'foo'}), - { - type: 'element', - tagName: 'input', - properties: {type: 'text', value: 'foo'}, - children: [] - }, - 'should *not* allow omitting `properties` for an `input[type=text][value]`, as those are void and clash' - ) - - t.deepEqual( - h('a', {type: 'text/html'}), - { - type: 'element', - tagName: 'a', - properties: {type: 'text/html'}, - children: [] - }, - 'should *not* allow omitting `properties` for a `[type]`, without `value` or `children`' - ) - - t.deepEqual( - h('foo', {type: 'text/html', children: {bar: 'baz'}}), - { - type: 'element', - tagName: 'foo', - properties: {type: 'text/html', children: '[object Object]'}, - children: [] - }, - 'should *not* allow omitting `properties` when `children` is not set to an array' - ) + }) + } + ) + + await t.test('should disambiguate non-object as a child', async function () { + assert.deepEqual(h('x', 'y'), { + type: 'element', + tagName: 'x', + properties: {}, + children: [{type: 'text', value: 'y'}] + }) + }) - t.deepEqual( - h('button', {type: 'submit', value: 'Send'}), - { - type: 'element', - tagName: 'button', - properties: {type: 'submit', value: 'Send'}, - children: [] - }, - 'should *not* allow omitting `properties` when a button has a valid type' - ) + await t.test('should disambiguate `array` as a child', async function () { + assert.deepEqual(h('x', ['y']), { + type: 'element', + tagName: 'x', + properties: {}, + children: [{type: 'text', value: 'y'}] + }) + }) - t.deepEqual( - h('button', {type: 'BUTTON', value: 'Send'}), - { - type: 'element', - tagName: 'button', - properties: {type: 'BUTTON', value: 'Send'}, - children: [] - }, - 'should *not* allow omitting `properties` when a button has a valid non-lowercase type' - ) + await t.test( + 'should not disambiguate an object w/o `type` as a child', + async function () { + assert.deepEqual( + // @ts-expect-error: incorrect properties. + h('x', { + a: 'y', + b: 1, + c: true, + d: ['z'], + e: {f: true} + }), + { + type: 'element', + tagName: 'x', + properties: { + a: 'y', + b: 1, + c: true, + d: ['z'], + e: '[object Object]' + }, + children: [] + } + ) + } + ) + + await t.test( + 'should disambiguate an object w/ a `type` and an array of non-primitives as a child', + async function () { + assert.deepEqual( + // @ts-expect-error: unknown node. + h('x', {type: 'y', key: [{value: 1}]}), + { + type: 'element', + tagName: 'x', + properties: {}, + children: [{type: 'y', key: [{value: 1}]}] + } + ) + } + ) - t.deepEqual( - h('button', {type: 'menu', value: 'Send'}), - { + await t.test( + 'should not disambiguate an object w/ a `type` and an array of primitives as a child', + async function () { + assert.deepEqual(h('x', {type: 'y', key: [1]}), { type: 'element', - tagName: 'button', - properties: {type: 'menu', value: 'Send'}, + tagName: 'x', + properties: {type: 'y', key: [1]}, children: [] - }, - 'should *not* allow omitting `properties` when a button has a valid type' - ) - - t.deepEqual( - h('button', {type: 'text', value: 'Send'}), - { + }) + } + ) + + await t.test( + 'should disambiguate an object w/ a `type` and an `object` as a child', + async function () { + assert.deepEqual(h('x', {type: 'y', data: {bar: 'baz'}}), { type: 'element', - tagName: 'button', + tagName: 'x', properties: {}, - children: [{type: 'text', value: 'Send'}] - }, - 'should allow omitting `properties` when a button has an invalid type' - ) - - t.deepEqual( - h('section', {id: 'test'}, h('p', 'first'), h('p', 'second')), - { + children: [{type: 'y', data: {bar: 'baz'}}] + }) + } + ) + + await t.test( + 'should disambiguate an object w/ a `type` and an empty `children` array is a child', + async function () { + assert.deepEqual(h('x', {type: 'y', children: []}), { type: 'element', - tagName: 'section', - properties: {id: 'test'}, - children: [ - { - type: 'element', - tagName: 'p', - properties: {}, - children: [{type: 'text', value: 'first'}] - }, - { - type: 'element', - tagName: 'p', - properties: {}, - children: [{type: 'text', value: 'second'}] - } - ] - }, - 'should allow passing multiple child nodes as arguments' - ) + tagName: 'x', + properties: {}, + children: [{type: 'y', children: []}] + }) + } + ) + + await t.test( + 'should allow passing multiple child nodes as arguments', + async function () { + assert.deepEqual( + h('section', {id: 'test'}, h('p', 'first'), h('p', 'second')), + { + type: 'element', + tagName: 'section', + properties: {id: 'test'}, + children: [ + { + type: 'element', + tagName: 'p', + properties: {}, + children: [{type: 'text', value: 'first'}] + }, + { + type: 'element', + tagName: 'p', + properties: {}, + children: [{type: 'text', value: 'second'}] + } + ] + } + ) + } + ) - t.deepEqual( - h('section', h('p', 'first'), h('p', 'second')), - { + await t.test( + 'should allow passing multiple child nodes as arguments when there is no properties argument present', + async function () { + assert.deepEqual(h('section', h('p', 'first'), h('p', 'second')), { type: 'element', tagName: 'section', properties: {}, @@ -943,48 +905,41 @@ test('hastscript', (t) => { children: [{type: 'text', value: 'second'}] } ] - }, - 'should allow passing multiple child nodes as arguments when there is no properties argument present' - ) - - t.throws( - () => { - // @ts-expect-error runtime. - h('foo', {}, true) - }, - /Expected node, nodes, or string, got `true`/, - 'should throw when given an invalid value' - ) - - t.end() + }) + } + ) + + await t.test('should throw when given an invalid value', async function () { + assert.throws(function () { + // @ts-expect-error: check how the runtime handles a boolean instead of a child. + h('foo', {}, true) + }, /Expected node, nodes, or string, got `true`/) }) +}) - t.test('