diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ee318ca..fb63387 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,5 +17,5 @@ jobs: strategy: matrix: node: - - lts/hydrogen + - lts/gallium - node diff --git a/.gitignore b/.gitignore index affeb21..586ce3d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,6 @@ coverage/ node_modules/ test/jsx-*.js yarn.lock -**/*.d.ts +*.d.ts !lib/jsx-classic.d.ts !lib/jsx-automatic.d.ts diff --git a/.npmrc b/.npmrc index 9951b11..3757b30 100644 --- a/.npmrc +++ b/.npmrc @@ -1,2 +1,2 @@ -package-lock=false ignore-scripts=true +package-lock=false diff --git a/example.js b/example.js deleted file mode 100644 index 7f79c4f..0000000 --- a/example.js +++ /dev/null @@ -1,21 +0,0 @@ -import {h, s} from './index.js' - -// Children as an array: -console.log( - h('.foo#some-id', [ - h('span', 'some text'), - h('input', {type: 'text', value: 'foo'}), - h('a.alpha', {class: 'bravo charlie', download: 'download'}, [ - 'delta', - 'echo' - ]) - ]) -) - -// SVG: -console.log( - s('svg', {xmlns: 'http://www.w3.org/2000/svg', viewbox: '0 0 500 500'}, [ - s('title', 'SVG `` element'), - s('circle', {cx: 120, cy: 120, r: 100}) - ]) -) diff --git a/html.js b/html.js deleted file mode 100644 index 8cfe365..0000000 --- a/html.js +++ /dev/null @@ -1,6 +0,0 @@ -/** - * @typedef {import('./lib/index.js').Child} Child - * @typedef {import('./lib/index.js').Properties} Properties - */ - -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/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.js b/lib/automatic-runtime-html.js new file mode 100644 index 0000000..03bf92a --- /dev/null +++ b/lib/automatic-runtime-html.js @@ -0,0 +1,7 @@ +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, jsx, jsxDEV, jsxs} = createAutomaticRuntime(h) diff --git a/lib/automatic-runtime-svg.js b/lib/automatic-runtime-svg.js new file mode 100644 index 0000000..2e381a2 --- /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, jsx, jsxDEV, jsxs} = createAutomaticRuntime(s) diff --git a/lib/create-automatic-runtime.js b/lib/create-automatic-runtime.js new file mode 100644 index 0000000..9dffaa6 --- /dev/null +++ b/lib/create-automatic-runtime.js @@ -0,0 +1,57 @@ +/** + * @typedef {import('hast').Element} Element + * @typedef {import('hast').Root} Root + * + * @typedef {import('./create-h.js').Child} Child + * @typedef {import('./create-h.js').Properties} Properties + * @typedef {import('./create-h.js').PropertyValue} PropertyValue + * @typedef {import('./create-h.js').Result} Result + * @typedef {import('./create-h.js').Style} Style + * @typedef {import('./create-h.js').createH} CreateH + * + * @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}} props + * @param {string} [key] + * @returns {Root} + * + * @overload + * @param {string} type + * @param {JSXProps} props + * @param {string} [key] + * @returns {Element} + * + * @param {string | null} type + * Element name or `null` to get a root. + * @param {Properties & {children?: Child}} props + * Properties. + * @returns {Result} + * Result. + */ + function jsx(type, props) { + const {children, ...properties} = props + 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, jsx, jsxDEV: jsx, jsxs: jsx} +} diff --git a/lib/core.js b/lib/create-h.js similarity index 54% rename from lib/core.js rename to lib/create-h.js index 02a0585..928676f 100644 --- a/lib/core.js +++ b/lib/create-h.js @@ -1,128 +1,148 @@ /** - * @typedef {import('hast').Root} Root - * @typedef {import('hast').Content} Content * @typedef {import('hast').Element} Element - * @typedef {import('hast').Properties} Properties + * @typedef {import('hast').Nodes} Nodes + * @typedef {import('hast').Root} Root + * @typedef {import('hast').RootContent} RootContent + * * @typedef {import('property-information').Info} Info * @typedef {import('property-information').Schema} Schema */ /** - * @typedef {Content | Root} Node - * Any concrete `hast` node. - * @typedef {Root | Element} HResult + * @typedef {Element | Root} Result * Result from a `h` (or `s`) call. * - * @typedef {string | number} HStyleValue + * @typedef {number | string} StyleValue * Value for a CSS style field. - * @typedef {Record} HStyle + * @typedef {Record} Style * Supported value of a `style` prop. - * @typedef {string | number | boolean | null | undefined} HPrimitiveValue + * @typedef {boolean | number | string | null | undefined} PrimitiveValue * Primitive property value. - * @typedef {Array} HArrayValue + * @typedef {Array} ArrayValue * List of property values for space- or comma separated values (such as `className`). - * @typedef {HPrimitiveValue | HArrayValue} HPropertyValue + * @typedef {ArrayValue | PrimitiveValue} PropertyValue * Primitive value or list value. - * @typedef {{[property: string]: HPropertyValue | HStyle}} HProperties + * @typedef {{[property: string]: PropertyValue | Style}} Properties * Acceptable value for element properties. * - * @typedef {string | number | null | undefined} HPrimitiveChild + * @typedef {number | string | null | undefined} PrimitiveChild * Primitive children, either ignored (nullish), or turned into text nodes. - * @typedef {Array} HArrayChild + * @typedef {Array} ArrayChild * List of children. - * @typedef {Node | HPrimitiveChild | HArrayChild} HChild + * @typedef {Array} ArrayChildNested + * List of children (deep). + * @typedef {ArrayChild | Nodes | PrimitiveChild} Child * Acceptable child value. */ -import {find, normalize} from 'property-information' +import {parse as commas} from 'comma-separated-tokens' import {parseSelector} from 'hast-util-parse-selector' +import {find, normalize} from 'property-information' 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 buttonTypes = new Set(['button', 'menu', 'reset', 'submit']) const own = {}.hasOwnProperty /** * @param {Schema} schema + * Schema to use. * @param {string} defaultTagName - * @param {Array} [caseSensitive] + * Default tag name. + * @param {Array | undefined} [caseSensitive] + * Case-sensitive tag names (default: `undefined`). + * @returns + * `h`. */ -export function core(schema, defaultTagName, caseSensitive) { +export function createH(schema, defaultTagName, caseSensitive) { const adjust = caseSensitive && createAdjustMap(caseSensitive) - const h = - /** - * @type {{ - * (): Root - * (selector: null | undefined, ...children: Array): Root - * (selector: string, properties?: HProperties, ...children: Array): Element - * (selector: string, ...children: Array): Element - * }} - */ - ( - /** - * Hyperscript compatible DSL for creating virtual hast trees. - * - * @param {string | null} [selector] - * @param {HProperties | HChild} [properties] - * @param {Array} 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] - } + /** + * 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) { + let index = -1 + /** @type {Result} */ + let node + + if (selector === undefined || selector === null) { + 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. + 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 props. + if (isProperties(properties, node.tagName)) { + /** @type {string} */ + let key + + for (key in properties) { + if (own.call(properties, key)) { + addProperty(schema, node.properties, key, properties[key]) } } + } else { + children.unshift(properties) + } + } - // Handle children. - while (++index < children.length) { - addChild(node.children, children[index]) - } + // 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 = [] - } + if (node.type === 'element' && node.tagName === 'template') { + node.content = {type: 'root', children: node.children} + node.children = [] + } - return node - } - ) + return node + } return h } /** - * @param {HProperties | HChild} value + * Check if something is properties or a child. + * + * @param {Child | Properties} value + * Value to check. * @param {string} name - * @returns {value is HProperties} + * Tag name. + * @returns {value is Properties} + * Whether `value` is a properties object. */ function isProperties(value, name) { if ( @@ -151,15 +171,20 @@ function isProperties(value, name) { /** * @param {Schema} schema + * Schema. * @param {Properties} properties + * Properties object. * @param {string} key - * @param {HStyle | HPropertyValue} value - * @returns {void} + * Property name. + * @param {PropertyValue | Style} value + * Property value. + * @returns {undefined} + * Nothing. */ function addProperty(schema, properties, key, value) { const info = find(schema, key) let index = -1 - /** @type {HPropertyValue} */ + /** @type {PropertyValue} */ let result // Ignore nullish and NaN values. @@ -193,12 +218,15 @@ function addProperty(schema, properties, key, value) { } if (Array.isArray(result)) { - /** @type {Array} */ + /** @type {Array} */ const finalResult = [] while (++index < result.length) { - // @ts-expect-error Assume no booleans in array. - finalResult[index] = parsePrimitive(info, info.property, result[index]) + // Assume no booleans in array. + const value = /** @type {number | string} */ ( + parsePrimitive(info, info.property, result[index]) + ) + finalResult[index] = value } result = finalResult @@ -206,17 +234,21 @@ function addProperty(schema, properties, key, value) { // 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) + // Assume no booleans in `className`. + const value = /** @type {number | string} */ (result) + result = properties.className.concat(value) } properties[info.property] = result } /** - * @param {Array} nodes - * @param {HChild} value - * @returns {void} + * @param {Array} nodes + * Children. + * @param {Child} value + * Child. + * @returns {undefined} + * Nothing. */ function addChild(nodes, value) { let index = -1 @@ -244,9 +276,13 @@ function addChild(nodes, value) { * Parse a single primitives. * * @param {Info} info + * Property information. * @param {string} name - * @param {HPrimitiveValue} value - * @returns {HPrimitiveValue} + * Property name. + * @param {PrimitiveValue} value + * Property value. + * @returns {PrimitiveValue} + * Property value. */ function parsePrimitive(info, name, value) { if (typeof value === 'string') { @@ -268,7 +304,7 @@ function parsePrimitive(info, name, value) { /** * Serialize a `style` object as a string. * - * @param {HStyle} value + * @param {Style} value * Style object. * @returns {string} * CSS string. diff --git a/lib/html.js b/lib/html.js deleted file mode 100644 index cdc0917..0000000 --- a/lib/html.js +++ /dev/null @@ -1,18 +0,0 @@ -/** - * @typedef {import('./core.js').HChild} Child - * Acceptable child value. - * @typedef {import('./core.js').HProperties} Properties - * Acceptable value for element properties. - * @typedef {import('./core.js').HResult} Result - * Result from a `h` (or `s`) call. - * - * @typedef {import('./jsx-classic.js').Element} h.JSX.Element - * @typedef {import('./jsx-classic.js').IntrinsicAttributes} h.JSX.IntrinsicAttributes - * @typedef {import('./jsx-classic.js').IntrinsicElements} h.JSX.IntrinsicElements - * @typedef {import('./jsx-classic.js').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 4e14b52..5c736f7 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,11 +1,36 @@ /** - * @typedef {import('./core.js').HChild} Child + * @typedef {import('./create-h.js').Child} Child * Acceptable child value. - * @typedef {import('./core.js').HProperties} Properties + * @typedef {import('./create-h.js').Properties} Properties * Acceptable value for element properties. - * @typedef {import('./core.js').HResult} Result + * @typedef {import('./create-h.js').Result} Result * Result from a `h` (or `s`) call. */ -export {h} from './html.js' -export {s} from './svg.js' +// Register the JSX namespace on `h`. +/** + * @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 + */ + +// 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 4b8d37d..14c0c18 100644 --- a/lib/jsx-automatic.d.ts +++ b/lib/jsx-automatic.d.ts @@ -1,43 +1,43 @@ -import type {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, @typescript-eslint/consistent-type-definitions interface IntrinsicElements { [name: string]: - | HProperties + | Properties | { /** - * The prop that matches `ElementChildrenAttribute` key defines the type of JSX children, defines the children type. + * The prop that matches `ElementChildrenAttribute` key defines the + * type of JSX children, defines the children type. */ - children?: HChild + children?: Child } } - - /** - * The key of this interface defines as what prop children are passed. - */ - // eslint-disable-next-line @typescript-eslint/consistent-type-definitions - interface ElementChildrenAttribute { - /** - * Only the key matters, not the value. - */ - children?: never - } } diff --git a/lib/jsx-classic.d.ts b/lib/jsx-classic.d.ts index 69ca32f..d2cc7c4 100644 --- a/lib/jsx-classic.d.ts +++ b/lib/jsx-classic.d.ts @@ -1,47 +1,47 @@ -import type {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, @typescript-eslint/consistent-type-definitions export interface IntrinsicElements { [name: string]: - | HProperties + | Properties | { /** - * The prop that matches `ElementChildrenAttribute` key defines the type of JSX children, defines the children type. + * The prop that matches `ElementChildrenAttribute` key defines the + * type of JSX children, defines the children type. */ - [children]?: HChild + [children]?: Child } } - -/** - * The key of this interface defines as what prop children are passed. - */ -// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -export interface ElementChildrenAttribute { - /** - * Only the key matters, not the value. - */ - [children]?: never -} diff --git a/lib/runtime-html.js b/lib/runtime-html.js deleted file mode 100644 index 636e2d9..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, jsxDEV} = runtime(h) diff --git a/lib/runtime-svg.js b/lib/runtime-svg.js deleted file mode 100644 index b60c533..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, jsxDEV} = runtime(s) diff --git a/lib/runtime.js b/lib/runtime.js deleted file mode 100644 index ce1f489..0000000 --- a/lib/runtime.js +++ /dev/null @@ -1,40 +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 {Record} JSXProps - */ - -/** - * Create an automatic runtime. - * - * @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, jsxDEV: jsx} -} diff --git a/lib/svg.js b/lib/svg.js deleted file mode 100644 index 3c68309..0000000 --- a/lib/svg.js +++ /dev/null @@ -1,19 +0,0 @@ -/** - * @typedef {import('./core.js').HChild} Child - * Acceptable child value. - * @typedef {import('./core.js').HProperties} Properties - * Acceptable value for element properties. - * @typedef {import('./core.js').HResult} Result - * Result from a `h` (or `s`) call. - * - * @typedef {import('./jsx-classic.js').Element} s.JSX.Element - * @typedef {import('./jsx-classic.js').IntrinsicAttributes} s.JSX.IntrinsicAttributes - * @typedef {import('./jsx-classic.js').IntrinsicElements} s.JSX.IntrinsicElements - * @typedef {import('./jsx-classic.js').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/package.json b/package.json index b338b5c..c351501 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hastscript", - "version": "7.2.0", + "version": "8.0.0", "description": "hast utility to create trees", "license": "MIT", "keywords": [ @@ -33,81 +33,85 @@ "types": "index.d.ts", "exports": { ".": "./index.js", - "./index.js": "./index.js", - "./html.js": "./html.js", - "./svg.js": "./svg.js", - "./jsx-runtime": "./jsx-runtime.js", - "./jsx-dev-runtime": "./jsx-runtime.js", - "./html/jsx-runtime": "./html/jsx-runtime.js", - "./html/jsx-dev-runtime": "./html/jsx-runtime.js", - "./svg/jsx-runtime": "./svg/jsx-runtime.js", - "./svg/jsx-dev-runtime": "./svg/jsx-runtime.js" + "./jsx-runtime": "./lib/automatic-runtime-html.js", + "./jsx-dev-runtime": "./lib/automatic-runtime-html.js", + "./svg/jsx-runtime": "./lib/automatic-runtime-svg.js", + "./svg/jsx-dev-runtime": "./lib/automatic-runtime-svg.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", + "hast-util-parse-selector": "^4.0.0", "property-information": "^6.0.0", "space-separated-tokens": "^2.0.0" }, "devDependencies": { - "@types/node": "^18.0.0", + "@types/node": "^20.0.0", "acorn-jsx": "^5.0.0", - "c8": "^7.0.0", - "esast-util-from-js": "^1.0.0", - "estree-util-build-jsx": "^2.0.0", - "estree-util-to-js": "^1.0.0", - "prettier": "^2.0.0", + "c8": "^8.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": "^11.0.0", "remark-preset-wooorm": "^9.0.0", "svg-tag-names": "^3.0.0", - "tsd": "^0.25.0", + "tsd": "^0.28.0", "type-coverage": "^2.0.0", - "typescript": "^4.0.0", - "unist-builder": "^3.0.0", - "xo": "^0.53.0" + "typescript": "^5.0.0", + "unist-builder": "^4.0.0", + "xo": "^0.55.0" }, "scripts": { "prepack": "npm run build && npm run format", "build": "tsc --build --clean && tsc --build && tsd && type-coverage", "generate": "node script/generate-jsx.js && node script/build.js", - "format": "remark . -qfo && prettier . -w --loglevel warn && xo --fix", + "format": "remark . -qfo && prettier . -w --log-level warn && xo --fix", "test-api": "node --conditions development test/index.js", - "test-coverage": "c8 --check-coverage --100 --reporter lcov npm run test-api", - "test": "npm run build && npm run generate && npm run format && npm run test-coverage" + "test-coverage": "c8 --100 --reporter lcov npm run test-api", + "test": "npm run generate && npm run build && npm run format && npm run test-coverage" }, "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" ] }, "typeCoverage": { "atLeast": 100, "detail": true, + "ignoreCatch": true, + "#": "needed `any`s :'(", + "ignoreFiles": [ + "test/jsx-build-jsx-automatic-development.js" + ], "strict": true + }, + "xo": { + "overrides": [ + { + "files": "**/*.ts", + "rules": { + "@typescript-eslint/consistent-indexed-object-style": "off", + "@typescript-eslint/consistent-type-definitions": "off" + } + } + ], + "prettier": true, + "rules": { + "n/file-extension-in-import": "off" + } } } diff --git a/readme.md b/readme.md index f79d343..be9e685 100644 --- a/readme.md +++ b/readme.md @@ -51,7 +51,7 @@ You can instead use [`unist-builder`][u] when creating any unist nodes and ## Install This package is [ESM only][esm]. -In Node.js (version 14.14+ or 16.0+), install with [npm][]: +In Node.js (version 16+), install with [npm][]: ```sh npm install hastscript @@ -60,14 +60,14 @@ npm install hastscript In Deno with [`esm.sh`][esmsh]: ```js -import {h} from 'https://esm.sh/hastscript@7' +import {h} from 'https://esm.sh/hastscript@8' ``` In browsers with [`esm.sh`][esmsh]: ```html ``` @@ -146,12 +146,12 @@ Yields: ## API -This package exports the identifiers `h` and `s`. +This package exports the identifiers [`h`][api-h] and [`s`][api-s]. There is no default export. The export map supports the automatic JSX runtime. -You can pass `hastscript` (or `hastscript/html`) or `hastscript/svg` to your -build tool (TypeScript, Babel, SWC) with an `importSource` option or similar. +You can pass `hastscript` or `hastscript/svg` to your build tool (TypeScript, +Babel, SWC) with an `importSource` option or similar. ### `h(selector?[, properties][, …children])` @@ -177,15 +177,16 @@ When nullish, builds a [`Root`][root] instead. ###### `properties` -Properties of the element ([`Properties`][properties], optional). +Properties of the element ([`Properties`][api-properties], optional). ###### `children` -Children of the element ([`Child`][child] or `Array`, optional). +Children of the node ([`Child`][api-child] or `Array`, optional). ##### Returns -Created tree ([`Result`][result]). +Created tree ([`Result`][api-result]). + [`Element`][element] when a `selector` is passed, otherwise [`Root`][root]. ### `s(selector?[, properties][, …children])` @@ -199,6 +200,7 @@ SVG. ### `Child` (Lists of) children (TypeScript type). + When strings or numbers are encountered, they are turned into [`Text`][text] nodes. [`Root`][root] nodes are treated as “fragments”, meaning that their children @@ -208,12 +210,12 @@ are used instead. ```ts type Child = - | string + | Array + | Node | number + | string | null | undefined - | Node - | Array ``` ### `Properties` @@ -227,15 +229,15 @@ are case-insensitive. ```ts type Properties = Record< string, - | string - | number | boolean + | number + | string | null | undefined // For comma- and space-separated values such as `className`: - | Array + | Array // Accepts value for `style` prop as object. - | Record + | Record > ``` @@ -246,7 +248,7 @@ Result from a `h` (or `s`) call (TypeScript type). ###### Type ```ts -type Result = Root | Element +type Result = Element | Root ``` ## Syntax tree @@ -256,8 +258,8 @@ The syntax tree is [hast][]. ## JSX This package can be used with JSX. -You should use the automatic JSX runtime set to `hastscript` (also available as -the more explicit name `hastscript/html`) or `hastscript/svg`. +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. @@ -300,14 +302,19 @@ console.log( ## Types This package is fully typed with [TypeScript][]. -It exports the additional types `Child`, `Properties`, and `Result`. +It exports the additional types [`Child`][api-child], +[`Properties`][api-properties], and +[`Result`][api-result]. ## Compatibility -Projects maintained by the unified collective are compatible with all maintained +Projects maintained by the unified collective are compatible with maintained versions of Node.js. -As of now, that is Node.js 14.14+ and 16.0+. -Our projects sometimes work with older versions, but this is not guaranteed. + +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@^8`, +compatible with Node.js 16. ## Security @@ -323,7 +330,7 @@ const tree = h() // Somehow someone injected these properties instead of an expected `src` and // `alt`: -const otherProps = {src: 'x', onError: 'alert(2)'} +const otherProps = {src: 'x', onError: 'alert(1)'} tree.children.push(h('img', {src: 'default.png', ...otherProps})) ``` @@ -331,7 +338,7 @@ 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 @@ -344,7 +351,7 @@ const tree = h() 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)) @@ -353,7 +360,7 @@ tree.children.push(h('span.handle', username)) Yields: ```html - + ``` Either do not use user-provided input in `hastscript` or use @@ -377,7 +384,7 @@ Either do not use user-provided input in `hastscript` or use ## Contribute See [`contributing.md`][contributing] in [`syntax-tree/.github`][health] for -started. +ways to get started. See [`support.md`][support] for ways to get help. This project has a [code of conduct][coc]. @@ -402,9 +409,9 @@ abide by its terms. [downloads]: https://www.npmjs.com/package/hastscript -[size-badge]: https://img.shields.io/bundlephobia/minzip/hastscript.svg +[size-badge]: https://img.shields.io/badge/dynamic/json?label=minzipped%20size&query=$.size.compressedSize&url=https://deno.bundlejs.com/?q=hastscript -[size]: https://bundlephobia.com/result?p=hastscript +[size]: https://bundlejs.com/?q=hastscript [sponsors-badge]: https://opencollective.com/unified/sponsors/badge.svg @@ -454,8 +461,12 @@ abide by its terms. [hast-util-sanitize]: https://github.com/syntax-tree/hast-util-sanitize -[child]: #child +[api-h]: #hselector-properties-children + +[api-s]: #sselector-properties-children + +[api-child]: #child -[properties]: #properties-1 +[api-properties]: #properties-1 -[result]: #result +[api-result]: #result diff --git a/script/build.js b/script/build.js index d83433f..9d888ab 100644 --- a/script/build.js +++ b/script/build.js @@ -1,11 +1,13 @@ import fs from 'node:fs/promises' import {svgTagNames} from 'svg-tag-names' -const casing = svgTagNames.filter((d) => d !== d.toLowerCase()) +const casing = svgTagNames.filter(function (d) { + return d !== d.toLowerCase() +}) await fs.writeFile( new URL('../lib/svg-case-sensitive-tag-names.js', import.meta.url), 'export const svgCaseSensitiveTagNames = ' + - JSON.stringify(casing, null, 2) + + JSON.stringify(casing, undefined, 2) + '\n' ) diff --git a/script/generate-jsx.js b/script/generate-jsx.js index ec48f25..4c3b20c 100644 --- a/script/generate-jsx.js +++ b/script/generate-jsx.js @@ -1,36 +1,57 @@ import fs from 'node:fs/promises' import acornJsx from 'acorn-jsx' +import {buildJsx} from 'estree-util-build-jsx' import {fromJs} from 'esast-util-from-js' import {toJs} from 'estree-util-to-js' -import {buildJsx} from 'estree-util-build-jsx' const doc = String( await fs.readFile(new URL('../test/jsx.jsx', import.meta.url)) ) -await fs.writeFile( - new URL('../test/jsx-build-jsx-classic.js', import.meta.url), - toJs( - buildJsx( - fromJs(doc.replace(/'name'/, "'jsx (estree-util-build-jsx, classic)'"), { - plugins: [acornJsx()], - module: true - }), - {pragma: 'h', pragmaFrag: 'null'} - ) - ).value +const treeAutomatic = fromJs( + doc.replace(/'name'/, "'jsx (estree-util-build-jsx, automatic)'"), + {plugins: [acornJsx()], module: true} +) + +const treeAutomaticDevelopment = fromJs( + doc.replace( + /'name'/, + "'jsx (estree-util-build-jsx, automatic, development)'" + ), + {plugins: [acornJsx()], module: true} +) + +const treeClassic = fromJs( + doc.replace(/'name'/, "'jsx (estree-util-build-jsx, classic)'"), + { + plugins: [acornJsx()], + module: true + } ) +buildJsx(treeAutomatic, { + runtime: 'automatic', + importSource: 'hastscript' +}) +buildJsx(treeAutomaticDevelopment, { + runtime: 'automatic', + importSource: 'hastscript', + development: true +}) +buildJsx(treeClassic, {pragma: 'h', pragmaFrag: 'null'}) + await fs.writeFile( new URL('../test/jsx-build-jsx-automatic.js', import.meta.url), + toJs(treeAutomatic).value +) - toJs( - buildJsx( - fromJs( - doc.replace(/'name'/, "'jsx (estree-util-build-jsx, automatic)'"), - {plugins: [acornJsx()], module: true} - ), - {runtime: 'automatic', importSource: 'hastscript'} - ) - ).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 e35ceea..0000000 --- a/svg.js +++ /dev/null @@ -1,6 +0,0 @@ -/** - * @typedef {import('./lib/index.js').Child} Child - * @typedef {import('./lib/index.js').Properties} Properties - */ - -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 828c85b..5141ad1 100644 --- a/test-d/automatic-h.tsx +++ b/test-d/automatic-h.tsx @@ -1,22 +1,13 @@ /* @jsxRuntime automatic */ /* @jsxImportSource hastscript */ -import {expectType, expectError} from 'tsd' +import {expectType} from 'tsd' import type {Root, Element} from 'hast' import {h} from '../index.js' -import {Fragment, jsx, jsxs} from '../jsx-runtime.js' 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,7 +36,8 @@ 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. @@ -53,4 +45,6 @@ expectError() expectType(} />) declare function Bar(props?: Record): Element -expectError() + +// @ts-expect-error: components are not supported. +const b = diff --git a/test-d/automatic-s.tsx b/test-d/automatic-s.tsx index d972823..2d61bf0 100644 --- a/test-d/automatic-s.tsx +++ b/test-d/automatic-s.tsx @@ -1,7 +1,7 @@ /* @jsxRuntime automatic */ /* @jsxImportSource hastscript/svg */ -import {expectType, expectError} from 'tsd' +import {expectType} from 'tsd' import type {Root, Element} from 'hast' import {s} from '../index.js' @@ -35,7 +35,8 @@ 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. @@ -43,4 +44,6 @@ expectError() expectType(} />) declare function Bar(props?: Record): Element -expectError() + +// @ts-expect-error: components are not supported. +const b = diff --git a/test-d/classic-h.tsx b/test-d/classic-h.tsx index 819f0f2..43c59d0 100644 --- a/test-d/classic-h.tsx +++ b/test-d/classic-h.tsx @@ -1,6 +1,6 @@ /* @jsx h */ /* @jsxFrag null */ -import {expectType, expectError} from 'tsd' +import {expectType} from 'tsd' import type {Root, Element} from 'hast' import {h} from '../index.js' @@ -34,12 +34,16 @@ expectType({[, ]}) expectType({[, ]}) expectType({[]}) -expectError() +// @ts-expect-error: not a valid property value. +const a = -// 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(} />) +// @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 = } /> 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 48efedd..6961bfe 100644 --- a/test-d/classic-s.tsx +++ b/test-d/classic-s.tsx @@ -1,6 +1,6 @@ /* @jsx s */ /* @jsxFrag null */ -import {expectType, expectError} from 'tsd' +import {expectType} from 'tsd' import type {Root, Element} from 'hast' import {s} from '../index.js' @@ -34,12 +34,16 @@ expectType({[, ]}) expectType({[, ]}) expectType({[]}) -expectError() +// @ts-expect-error: not a valid property value. +const a = -// 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(} />) +// @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 = } /> 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 5340b5e..b5317fa 100644 --- a/test-d/files.ts +++ b/test-d/files.ts @@ -1,10 +1,6 @@ import {expectType} from 'tsd' import type {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()) diff --git a/test-d/index.ts b/test-d/index.ts index 3ef145e..5e4193f 100644 --- a/test-d/index.ts +++ b/test-d/index.ts @@ -1,17 +1,20 @@ -import {expectType, expectError} from 'tsd' +import {expectType} from 'tsd' import type {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, jsx, jsxs} from '../lib/automatic-runtime-html.js' -// 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,10 +38,12 @@ 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'}, [ @@ -44,11 +51,3 @@ expectType( s('circle', {cx: 120, cy: 120, r: 100}) ]) ) - -expectType(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 14abdc9..887b7f9 100644 --- a/test/core.js +++ b/test/core.js @@ -1,764 +1,690 @@ import assert from 'node:assert/strict' import test from 'node:test' import {h, s} from '../index.js' -import {h as hFromRoot} from '../html.js' -import {s as sFromRoot} from '../svg.js' -import * as coreMod from '../index.js' -import * as htmlMod from '../html.js' -import * as svgMod from '../svg.js' -import * as jsxCoreMod from '../jsx-runtime.js' -import * as jsxHtmlMod from '../html/jsx-runtime.js' -import * as jsxSvgMod from '../svg/jsx-runtime.js' - -test('api', () => { - const core = Object.keys(coreMod) - assert(core.includes('h'), 'should expose `h` from `.`') - assert(core.includes('s'), 'should expose `s` from `.`') - const html = Object.keys(htmlMod) - assert(html.includes('h'), 'should expose `h` from `/html`') - const svg = Object.keys(svgMod) - assert(svg.includes('s'), 'should expose `s` from `/svg`') - const jsxCore = Object.keys(jsxCoreMod) - assert( - jsxCore.includes('Fragment'), - 'should expose `Fragment` from `/jsx-runtime`' - ) - assert(jsxCore.includes('jsx'), 'should expose `jsx` from `/jsx-runtime`') - assert(jsxCore.includes('jsxs'), 'should expose `jsxs` from `/jsx-runtime`') - assert( - jsxCore.includes('jsxDEV'), - 'should expose `jsxDEV` from `/jsx-runtime`' - ) - const jsxHtml = Object.keys(jsxHtmlMod) - assert( - jsxHtml.includes('Fragment'), - 'should expose `Fragment` from `/html/jsx-runtime`' - ) - assert( - jsxHtml.includes('jsx'), - 'should expose `jsx` from `/html/jsx-runtime`' - ) - assert( - jsxHtml.includes('jsxs'), - 'should expose `jsxs` from `/html/jsx-runtime`' - ) - assert( - jsxHtml.includes('jsxDEV'), - 'should expose `jsxDEV` from `/html/jsx-runtime`' - ) - const jsxSvg = Object.keys(jsxSvgMod) - assert( - jsxSvg.includes('Fragment'), - 'should expose `Fragment` from `/svg/jsx-runtime`' - ) - assert(jsxSvg.includes('jsx'), 'should expose `jsx` from `/svg/jsx-runtime`') - assert( - jsxSvg.includes('jsxs'), - 'should expose `jsxs` from `/svg/jsx-runtime`' + +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']) + }) + + 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'] + ) + } ) - assert( - jsxSvg.includes('jsxDEV'), - 'should expose `jsxDEV` from `/svg/jsx-runtime`' + + 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'] + ) + } ) }) -test('hastscript', async (t) => { - assert.equal(h, hFromRoot, '`h` should be exposed from `/html.js`') - assert.equal(s, sFromRoot, '`s` should be exposed from `/svg.js`') - - assert.equal(typeof h, 'function', 'should expose a function') - - await t.test('selector', () => { - assert.deepEqual( - h(), - {type: 'root', children: []}, - 'should create a `root` node without arguments' - ) +test('selector', async function (t) { + await t.test( + 'should create a `root` node without arguments', + async function () { + assert.deepEqual(h(), {type: 'root', children: []}) + } + ) - assert.deepEqual( - h(''), - { + await t.test( + 'should create a `div` element w/ an empty string name', + async function () { + assert.deepEqual(h(''), { type: 'element', tagName: 'div', properties: {}, children: [] - }, - 'should create a `div` element w/ an empty string name' - ) + }) + } + ) - assert.deepEqual( - h('.bar', {class: 'baz'}), - { - type: 'element', - tagName: 'div', - properties: {className: ['bar', 'baz']}, - 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: [] + }) + }) - assert.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' - ) + }) + } + ) - assert.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' - ) + }) + } + ) - assert.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' - ) + }) + } + ) - assert.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' - ) + }) + } + ) - assert.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' - ) + }) + } + ) - assert.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' - ) + }) + } + ) - assert.deepEqual( - h('.foo.bar'), - { + await t.test('should support multiple classes', async function () { + assert.deepEqual(h('.foo.bar'), { + type: 'element', + tagName: 'div', + properties: {className: ['foo', 'bar']}, + children: [] + }) + }) +}) + +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' - ) - }) - - await t.test('properties', async (t) => { - await t.test('known property names', () => { - assert.deepEqual( - h('', {className: 'foo'}), - { - type: 'element', - tagName: 'div', - properties: {className: ['foo']}, - children: [] - }, - 'should support correctly cased property names' - ) - - assert.deepEqual( - h('', {class: 'foo'}), - { - type: 'element', - tagName: 'div', - properties: {className: ['foo']}, - children: [] - }, - 'should map attributes to property names' - ) - - assert.deepEqual( - h('', {CLASS: 'foo'}), - { - type: 'element', - tagName: 'div', - properties: {className: ['foo']}, - children: [] - }, - 'should map attribute-like values to property names' - ) + }) + } + ) - assert.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 attributes to property names', async function () { + assert.deepEqual(h('', {class: 'foo'}), { + type: 'element', + tagName: 'div', + properties: {className: ['foo']}, + children: [] }) + }) - await t.test('unknown property names', () => { - assert.deepEqual( - h('', {allowbigscreen: true}), - { - type: 'element', - tagName: 'div', - properties: {allowbigscreen: true}, - children: [] - }, - 'should keep lower-cased unknown 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: [] + }) + } + ) - assert.deepEqual( - h('', {allowBigScreen: true}), - { - type: 'element', - tagName: 'div', - properties: {allowBigScreen: true}, - children: [] - }, - 'should keep camel-cased unknown names' - ) + 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: [] + }) + } + ) +}) - assert.deepEqual( - h('', {'allow_big-screen': true}), - { - type: 'element', - tagName: 'div', - properties: {'allow_big-screen': true}, - children: [] - }, - 'should keep weirdly cased unknown names' - ) +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: [] }) + }) - await t.test('other namespaces', () => { - assert.deepEqual( - h('', {'aria-valuenow': 1}), - { - type: 'element', - tagName: 'div', - properties: {ariaValueNow: 1}, - children: [] - }, - 'should support aria attribute names' - ) - - assert.deepEqual( - h('', {ariaValueNow: 1}), - { - type: 'element', - tagName: 'div', - properties: {ariaValueNow: 1}, - children: [] - }, - 'should support aria property names' - ) - - assert.deepEqual( - s('', {'color-interpolation-filters': 'sRGB'}), - { - type: 'element', - tagName: 'g', - properties: {colorInterpolationFilters: 'sRGB'}, - children: [] - }, - 'should support svg attribute names' - ) - - assert.deepEqual( - s('', {colorInterpolationFilters: 'sRGB'}), - { - type: 'element', - tagName: 'g', - properties: {colorInterpolationFilters: 'sRGB'}, - children: [] - }, - 'should support svg property names' - ) - - assert.deepEqual( - s('', {'xml:space': 'preserve'}), - { - type: 'element', - tagName: 'g', - properties: {xmlSpace: 'preserve'}, - children: [] - }, - 'should support xml attribute names' - ) - - assert.deepEqual( - s('', {xmlSpace: 'preserve'}), - { - type: 'element', - tagName: 'g', - properties: {xmlSpace: 'preserve'}, - children: [] - }, - 'should support xml property names' - ) - - assert.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' - ) + await t.test('should keep camel-cased unknown names', async function () { + assert.deepEqual(h('', {allowBigScreen: true}), { + type: 'element', + tagName: 'div', + properties: {allowBigScreen: true}, + children: [] + }) + }) - assert.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' - ) + 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: [] + }) + }) +}) - assert.deepEqual( - s('', {'xlink:arcrole': 'http://www.example.com'}), - { - type: 'element', - tagName: 'g', - properties: {xLinkArcRole: 'http://www.example.com'}, - children: [] - }, - 'should support xlink attribute names' - ) +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: [] + }) + }) - assert.deepEqual( - s('', {xLinkArcRole: 'http://www.example.com'}), - { - type: 'element', - tagName: 'g', - properties: {xLinkArcRole: 'http://www.example.com'}, - children: [] - }, - 'should support xlink property names' - ) + await t.test('should support aria property names', async function () { + assert.deepEqual(h('', {ariaValueNow: 1}), { + type: 'element', + tagName: 'div', + properties: {ariaValueNow: 1}, + children: [] }) + }) - await t.test('data property names', () => { - assert.deepEqual( - h('', {'data-foo': true}), - { - type: 'element', - tagName: 'div', - properties: {dataFoo: true}, - children: [] - }, - 'should support data attribute names' - ) + 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: [] + }) + }) - assert.deepEqual( - h('', {'data-123': true}), - { - type: 'element', - tagName: 'div', - properties: {data123: true}, - children: [] - }, - 'should support numeric-first data attribute names' - ) + await t.test('should support svg property names', async function () { + assert.deepEqual(s('', {colorInterpolationFilters: 'sRGB'}), { + type: 'element', + tagName: 'g', + properties: {colorInterpolationFilters: 'sRGB'}, + children: [] + }) + }) - assert.deepEqual( - h('', {dataFooBar: true}), - { - type: 'element', - tagName: 'div', - properties: {dataFooBar: true}, - children: [] - }, - 'should support data property names' - ) + await t.test('should support xml attribute names', async function () { + assert.deepEqual(s('', {'xml:space': 'preserve'}), { + type: 'element', + tagName: 'g', + properties: {xmlSpace: 'preserve'}, + children: [] + }) + }) - assert.deepEqual( - h('', {data123: true}), - { - type: 'element', - tagName: 'div', - properties: {data123: true}, - children: [] - }, - 'should support numeric-first data property names' - ) + await t.test('should support xml property names', async function () { + assert.deepEqual(s('', {xmlSpace: 'preserve'}), { + type: 'element', + tagName: 'g', + properties: {xmlSpace: 'preserve'}, + children: [] + }) + }) - assert.deepEqual( - h('', {'data-foo.bar': true}), - { - type: 'element', - tagName: 'div', - properties: {'dataFoo.bar': true}, - children: [] - }, - 'should support data attribute names with uncommon characters' - ) + 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: [] + }) + }) - assert.deepEqual( - h('', {'dataFoo.bar': true}), - { - type: 'element', - tagName: 'div', - properties: {'dataFoo.bar': true}, - children: [] - }, - 'should support data property names with uncommon characters' - ) + 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: [] + }) + }) - assert.deepEqual( - h('', {'data-foo!bar': true}), - { - type: 'element', - tagName: 'div', - properties: {'data-foo!bar': true}, - children: [] - }, - 'should keep invalid data attribute names' - ) + 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: [] + }) + }) - assert.deepEqual( - h('', {'dataFoo!bar': true}), - { - type: 'element', - tagName: 'div', - properties: {'dataFoo!bar': true}, - children: [] - }, - 'should keep invalid data property names' - ) + 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: [] }) + }) +}) - await t.test('unknown property values', () => { - assert.deepEqual( - h('', {foo: 'bar'}), - { - type: 'element', - tagName: 'div', - properties: {foo: 'bar'}, - children: [] - }, - 'should support unknown `string` 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: [] + }) + }) - assert.deepEqual( - h('', {foo: 3}), - { - type: 'element', - tagName: 'div', - properties: {foo: 3}, - children: [] - }, - 'should support unknown `number` values' - ) + 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: [] + }) + } + ) - assert.deepEqual( - h('', {foo: true}), - { - type: 'element', - tagName: 'div', - properties: {foo: true}, - children: [] - }, - 'should support unknown `boolean` values' - ) + await t.test('should support data property names', async function () { + assert.deepEqual(h('', {dataFooBar: true}), { + type: 'element', + tagName: 'div', + properties: {dataFooBar: true}, + children: [] + }) + }) - assert.deepEqual( - h('', {list: ['bar', 'baz']}), - { - type: 'element', - tagName: 'div', - properties: {list: ['bar', 'baz']}, - children: [] - }, - 'should support unknown `Array` 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: [] + }) + } + ) - assert.deepEqual( - h('', {foo: null}), - { - type: 'element', - tagName: 'div', - properties: {}, - children: [] - }, - 'should ignore properties with a value of `null`' - ) + 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: [] + }) + } + ) - assert.deepEqual( - h('', {foo: undefined}), - { - type: 'element', - tagName: 'div', - properties: {}, - children: [] - }, - 'should ignore properties with a value of `undefined`' - ) + 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: [] + }) + } + ) - assert.deepEqual( - h('', {foo: Number.NaN}), - { - type: 'element', - tagName: 'div', - properties: {}, - children: [] - }, - 'should ignore properties with a value of `NaN`' - ) + 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: [] }) + }) - await t.test('known booleans', () => { - assert.deepEqual( - h('', {allowFullScreen: ''}), - { - type: 'element', - tagName: 'div', - properties: {allowFullScreen: true}, - children: [] - }, - 'should cast valid known `boolean` values' - ) - - assert.deepEqual( - h('', {allowFullScreen: 'yup'}), - { - type: 'element', - tagName: 'div', - properties: {allowFullScreen: 'yup'}, - children: [] - }, - 'should not cast invalid known `boolean` 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: [] + }) + }) +}) - assert.deepEqual( - h('img', {title: 'title'}), - { - type: 'element', - tagName: 'img', - properties: {title: 'title'}, - children: [] - }, - 'should not cast unknown boolean-like 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: [] }) + }) - await t.test('known overloaded booleans', () => { - assert.deepEqual( - h('', {download: ''}), - { - type: 'element', - tagName: 'div', - properties: {download: true}, - children: [] - }, - 'should cast known empty overloaded `boolean` values' - ) + await t.test('should support unknown `number` values', async function () { + assert.deepEqual(h('', {foo: 3}), { + type: 'element', + tagName: 'div', + properties: {foo: 3}, + children: [] + }) + }) - assert.deepEqual( - h('', {download: 'downLOAD'}), - { - type: 'element', - tagName: 'div', - properties: {download: true}, - children: [] - }, - 'should cast known named overloaded `boolean` values' - ) + await t.test('should support unknown `boolean` values', async function () { + assert.deepEqual(h('', {foo: true}), { + type: 'element', + tagName: 'div', + properties: {foo: true}, + children: [] + }) + }) - assert.deepEqual( - h('', {download: 'example.ogg'}), - { - type: 'element', - tagName: 'div', - properties: {download: 'example.ogg'}, - children: [] - }, - 'should not cast overloaded `boolean` values for different 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: [] }) + }) - await t.test('known numbers', () => { - assert.deepEqual( - h('textarea', {cols: '3'}), - { - type: 'element', - tagName: 'textarea', - properties: {cols: 3}, - children: [] - }, - 'should cast valid 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: [] + }) + } + ) - assert.deepEqual( - h('textarea', {cols: 'one'}), - { - type: 'element', - tagName: 'textarea', - properties: {cols: 'one'}, - children: [] - }, - 'should not cast invalid known `numeric` values' - ) + await t.test( + 'should ignore properties with a value of `undefined`', + async function () { + assert.deepEqual(h('', {foo: undefined}), { + type: 'element', + tagName: 'div', + properties: {}, + children: [] + }) + } + ) - assert.deepEqual( - h('meter', {low: '40', high: '90'}), - { - type: 'element', - tagName: 'meter', - properties: {low: 40, high: 90}, - children: [] - }, - 'should cast known `numeric` values' - ) + 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: [] + }) + } + ) +}) + +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: [] }) + }) - await t.test('known lists', () => { - assert.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 not cast invalid known `boolean` values', + async function () { + assert.deepEqual(h('', {allowFullScreen: 'yup'}), { + type: 'element', + tagName: 'div', + properties: {allowFullScreen: 'yup'}, + children: [] + }) + } + ) - assert.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 not cast unknown boolean-like values', + async function () { + assert.deepEqual(h('img', {title: 'title'}), { + type: 'element', + tagName: 'img', + properties: {title: 'title'}, + children: [] + }) + } + ) +}) - assert.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' - ) - }) +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('style', () => { - assert.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 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: [] + }) + } + ) +}) - assert.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('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: [] }) }) - await t.test('children', () => { - assert.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' - ) + }) + } + ) - assert.deepEqual( - h('div', {}, 'foo'), - { + 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: [] + }) + }) +}) + +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: [] + }) + } + ) +}) + +test('style property', async function (t) { + await t.test('should support `style` as an object', async function () { assert.deepEqual( - h('div', {}, {type: 'text', value: 'foo'}), + 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: [] + } ) + }) + await t.test('should support `style` as a string', async function () { assert.deepEqual( - h('div', {}, h('span', {}, 'foo')), + 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'}] + } + ] + }) + }) + + await t.test('should support nodes', async function () { assert.deepEqual( h('div', {}, [ {type: 'text', value: 'foo'}, @@ -772,10 +698,11 @@ test('hastscript', async (t) => { {type: 'text', value: 'foo'}, {type: 'text', value: 'bar'} ] - }, - 'should support nodes' + } ) + }) + await t.test('should support nodes created by `h`', async function () { assert.deepEqual( h('div', {}, [h('span', {}, 'foo'), h('strong', {}, 'bar')]), { @@ -796,13 +723,14 @@ test('hastscript', async (t) => { children: [{type: 'text', value: 'bar'}] } ] - }, - 'should support nodes created by `h`' + } ) + }) - assert.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: {}, @@ -810,24 +738,26 @@ test('hastscript', async (t) => { {type: 'text', value: 'foo'}, {type: 'text', value: 'bar'} ] - }, - 'should support `Array` for a `Text`s' - ) + }) + } + ) - assert.deepEqual( - h('strong', 'foo'), - { + await t.test( + 'should allow omitting `properties` for a `string`', + async function () { + assert.deepEqual(h('strong', 'foo'), { type: 'element', tagName: 'strong', properties: {}, children: [{type: 'text', value: 'foo'}] - }, - 'should allow omitting `properties` for a `string`' - ) + }) + } + ) - assert.deepEqual( - h('strong', h('span', 'foo')), - { + await t.test( + 'should allow omitting `properties` for a node', + async function () { + assert.deepEqual(h('strong', h('span', 'foo')), { type: 'element', tagName: 'strong', properties: {}, @@ -839,13 +769,14 @@ test('hastscript', async (t) => { children: [{type: 'text', value: 'foo'}] } ] - }, - 'should allow omitting `properties` for a node' - ) + }) + } + ) - assert.deepEqual( - h('strong', ['foo', 'bar']), - { + await t.test( + 'should allow omitting `properties` for an array', + async function () { + assert.deepEqual(h('strong', ['foo', 'bar']), { type: 'element', tagName: 'strong', properties: {}, @@ -853,114 +784,126 @@ test('hastscript', async (t) => { {type: 'text', value: 'foo'}, {type: 'text', value: 'bar'} ] - }, - 'should allow omitting `properties` for an array' - ) + }) + } + ) - assert.deepEqual( - h('input', {type: 'text', value: 'foo'}), - { + await t.test( + 'should *not* allow omitting `properties` for an `input[type=text][value]`, as those are void and clash', + async function () { + assert.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' - ) + }) + } + ) - assert.deepEqual( - h('a', {type: 'text/html'}), - { + await t.test( + 'should *not* allow omitting `properties` for a `[type]`, without `value` or `children`', + async function () { + assert.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`' - ) + }) + } + ) - assert.deepEqual( - h('foo', {type: 'text/html', children: {bar: 'baz'}}), - { + await t.test( + 'should *not* allow omitting `properties` when `children` is not set to an array', + async function () { + assert.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' - ) + }) + } + ) - assert.deepEqual( - h('button', {type: 'submit', value: 'Send'}), - { + await t.test( + 'should *not* allow omitting `properties` when a button has a valid type', + async function () { + assert.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' - ) + }) + } + ) - assert.deepEqual( - h('button', {type: 'BUTTON', value: 'Send'}), - { + await t.test( + 'should *not* allow omitting `properties` when a button has a valid non-lowercase type', + async function () { + assert.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' - ) + }) + } + ) - assert.deepEqual( - h('button', {type: 'menu', value: 'Send'}), - { + await t.test( + 'should *not* allow omitting `properties` when a button has a valid type', + async function () { + assert.deepEqual(h('button', {type: 'menu', value: 'Send'}), { type: 'element', tagName: 'button', properties: {type: 'menu', value: 'Send'}, children: [] - }, - 'should *not* allow omitting `properties` when a button has a valid type' - ) + }) + } + ) - assert.deepEqual( - h('button', {type: 'text', value: 'Send'}), - { + await t.test( + 'should allow omitting `properties` when a button has an invalid type', + async function () { + assert.deepEqual(h('button', {type: 'text', value: 'Send'}), { type: 'element', tagName: 'button', properties: {}, children: [{type: 'text', value: 'Send'}] - }, - 'should allow omitting `properties` when a button has an invalid type' - ) + }) + } + ) - 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'}] - } - ] - }, - 'should allow passing multiple child nodes as arguments' - ) + 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'}] + } + ] + } + ) + } + ) - assert.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: {}, @@ -978,45 +921,40 @@ test('hastscript', async (t) => { children: [{type: 'text', value: 'second'}] } ] - }, - 'should allow passing multiple child nodes as arguments when there is no properties argument present' - ) + }) + } + ) - assert.throws( - () => { - // @ts-expect-error runtime. - h('foo', {}, true) - }, - /Expected node, nodes, or string, got `true`/, - 'should throw when given an invalid value' - ) + 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`/) }) +}) - await t.test('