From bd621c55595b603ddcba010e2bba799c49c63443 Mon Sep 17 00:00:00 2001 From: Titus Date: Thu, 5 Jan 2023 11:20:47 +0100 Subject: [PATCH 01/20] Fix typo --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index f79d343..bed209b 100644 --- a/readme.md +++ b/readme.md @@ -377,7 +377,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]. From 1c838d9001e04d8c63937d17ec29c1dbec9b80ec Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Mon, 16 Jan 2023 15:34:01 +0100 Subject: [PATCH 02/20] Fix typos --- lib/core.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/core.js b/lib/core.js index 02a0585..f5ca445 100644 --- a/lib/core.js +++ b/lib/core.js @@ -56,7 +56,7 @@ export function core(schema, defaultTagName, caseSensitive) { * @type {{ * (): Root * (selector: null | undefined, ...children: Array): Root - * (selector: string, properties?: HProperties, ...children: Array): Element + * (selector: string, properties: HProperties, ...children: Array): Element * (selector: string, ...children: Array): Element * }} */ @@ -64,8 +64,8 @@ export function core(schema, defaultTagName, caseSensitive) { /** * Hyperscript compatible DSL for creating virtual hast trees. * - * @param {string | null} [selector] - * @param {HProperties | HChild} [properties] + * @param {string | null | undefined} [selector] + * @param {HProperties | HChild | null | undefined} [properties] * @param {Array} children * @returns {HResult} */ From 3ff3138974899d6ed5f33bbbe6e0998043b9c3cd Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Mon, 16 Jan 2023 15:34:17 +0100 Subject: [PATCH 03/20] Use Node 16 in Actions --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 82ef83e2521e6158083e3dc37194b2b14371a14d Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Mon, 16 Jan 2023 15:35:30 +0100 Subject: [PATCH 04/20] Remove example --- example.js | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100644 example.js 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}) - ]) -) From 30369c8d8609c507ff44ec9ad30068416906c0aa Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Mon, 16 Jan 2023 15:38:39 +0100 Subject: [PATCH 05/20] Refactor tests for exposed identifiers --- test/core.js | 67 +++++++++++++++++++--------------------------------- 1 file changed, 24 insertions(+), 43 deletions(-) diff --git a/test/core.js b/test/core.js index 14abdc9..0f96b2a 100644 --- a/test/core.js +++ b/test/core.js @@ -11,54 +11,35 @@ 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.deepEqual( + Object.keys(coreMod).sort(), + ['h', 's'], + 'should expose the public api (`/`)' ) - 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`' + assert.deepEqual( + Object.keys(htmlMod).sort(), + ['h'], + 'should expose the public api (`/html`)' ) - const jsxHtml = Object.keys(jsxHtmlMod) - assert( - jsxHtml.includes('Fragment'), - 'should expose `Fragment` from `/html/jsx-runtime`' + assert.deepEqual( + Object.keys(svgMod).sort(), + ['s'], + 'should expose the public api (`/svg`)' ) - assert( - jsxHtml.includes('jsx'), - 'should expose `jsx` from `/html/jsx-runtime`' + assert.deepEqual( + Object.keys(jsxCoreMod).sort(), + ['Fragment', 'jsx', 'jsxDEV', 'jsxs'], + 'should expose the public api (`/jsx-runtime`)' ) - assert( - jsxHtml.includes('jsxs'), - 'should expose `jsxs` from `/html/jsx-runtime`' + assert.deepEqual( + Object.keys(jsxHtmlMod).sort(), + ['Fragment', 'jsx', 'jsxDEV', 'jsxs'], + 'should expose the public api (`/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`' - ) - assert( - jsxSvg.includes('jsxDEV'), - 'should expose `jsxDEV` from `/svg/jsx-runtime`' + assert.deepEqual( + Object.keys(jsxSvgMod).sort(), + ['Fragment', 'jsx', 'jsxDEV', 'jsxs'], + 'should expose the public api (`/svg/jsx-runtime`)' ) }) From 35b265e05aaa8b5b6884a1d89c64759586fbd5a7 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Mon, 16 Jan 2023 15:41:32 +0100 Subject: [PATCH 06/20] Add some links --- readme.md | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/readme.md b/readme.md index bed209b..0ad9469 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 14.14+ and 16.0+), install with [npm][]: ```sh npm install hastscript @@ -146,7 +146,7 @@ Yields: ## API -This package exports the identifiers `h` and `s`. +This package exports the identifiers [`h`][h] and [`s`][s]. There is no default export. The export map supports the automatic JSX runtime. @@ -199,6 +199,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 @@ -300,7 +301,8 @@ 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`][child], [`Properties`][properties], +and [`Result`][result]. ## Compatibility @@ -323,7 +325,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 +333,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 +346,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 +355,7 @@ tree.children.push(h('span.handle', username)) Yields: ```html - + ``` Either do not use user-provided input in `hastscript` or use @@ -454,6 +456,10 @@ abide by its terms. [hast-util-sanitize]: https://github.com/syntax-tree/hast-util-sanitize +[h]: #hselector-properties-children + +[s]: #sselector-properties-children + [child]: #child [properties]: #properties-1 From e0311333a25beb44e54d64aac6ea3ab99d547a1b Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Tue, 17 Jan 2023 15:51:13 +0100 Subject: [PATCH 07/20] Fix typo --- readme.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 0ad9469..1af0e25 100644 --- a/readme.md +++ b/readme.md @@ -181,11 +181,12 @@ Properties of the element ([`Properties`][properties], optional). ###### `children` -Children of the element ([`Child`][child] or `Array`, optional). +Children of the node ([`Child`][child] or `Array`, optional). ##### Returns Created tree ([`Result`][result]). + [`Element`][element] when a `selector` is passed, otherwise [`Root`][root]. ### `s(selector?[, properties][, …children])` From 0410550d70c1316188d11463b1579534b72130e3 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Wed, 28 Jun 2023 10:06:57 +0100 Subject: [PATCH 08/20] Update `xo` Reviewed-by: Christian Murphy Reviewed-by: Remco Haszing Reviewed-by: Titus Wormer Closes GH-20. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b338b5c..687a013 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "type-coverage": "^2.0.0", "typescript": "^4.0.0", "unist-builder": "^3.0.0", - "xo": "^0.53.0" + "xo": "^0.54.0" }, "scripts": { "prepack": "npm run build && npm run format", From 42748d46995c0eccd1e9815ae95c8d428602cf42 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Tue, 1 Aug 2023 14:02:12 +0200 Subject: [PATCH 09/20] Update dev-dependencies --- package.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 687a013..19981b2 100644 --- a/package.json +++ b/package.json @@ -64,27 +64,27 @@ "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", + "c8": "^8.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", + "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", + "typescript": "^5.0.0", "unist-builder": "^3.0.0", - "xo": "^0.54.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" From 04a40a576575bae9770b520f41df24f7309e0eff Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Tue, 1 Aug 2023 14:08:18 +0200 Subject: [PATCH 10/20] Update `@types/hast`, utilities --- lib/core.js | 1 - package.json | 12 ++++----- script/generate-jsx.js | 61 ++++++++++++++++++++++++++++-------------- test/index.js | 1 + 4 files changed, 48 insertions(+), 27 deletions(-) diff --git a/lib/core.js b/lib/core.js index f5ca445..f15d1e1 100644 --- a/lib/core.js +++ b/lib/core.js @@ -93,7 +93,6 @@ export function core(schema, defaultTagName, caseSensitive) { for (key in properties) { if (own.call(properties, key)) { - // @ts-expect-error `node.properties` is set. addProperty(schema, node.properties, key, properties[key]) } } diff --git a/package.json b/package.json index 19981b2..555545b 100644 --- a/package.json +++ b/package.json @@ -57,9 +57,9 @@ "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" }, @@ -67,9 +67,9 @@ "@types/node": "^20.0.0", "acorn-jsx": "^5.0.0", "c8": "^8.0.0", - "esast-util-from-js": "^1.0.0", - "estree-util-build-jsx": "^2.0.0", - "estree-util-to-js": "^1.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", @@ -77,7 +77,7 @@ "tsd": "^0.28.0", "type-coverage": "^2.0.0", "typescript": "^5.0.0", - "unist-builder": "^3.0.0", + "unist-builder": "^4.0.0", "xo": "^0.55.0" }, "scripts": { diff --git a/script/generate-jsx.js b/script/generate-jsx.js index ec48f25..ca3dc6f 100644 --- a/script/generate-jsx.js +++ b/script/generate-jsx.js @@ -8,29 +8,50 @@ 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 +) + +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 +) - 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-classic.js', import.meta.url), + toJs(treeClassic).value ) diff --git a/test/index.js b/test/index.js index 372ef82..04e777f 100644 --- a/test/index.js +++ b/test/index.js @@ -2,4 +2,5 @@ import './core.js' import './jsx-build-jsx-classic.js' import './jsx-build-jsx-automatic.js' +import './jsx-build-jsx-automatic-development.js' /* eslint-enable import/no-unassigned-import */ From 024729f47d818f265ccd8aa254a841ff34ee7592 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Tue, 1 Aug 2023 14:10:39 +0200 Subject: [PATCH 11/20] Refactor `package.json`, `tsconfig.json` --- package.json | 21 +++++++++++---------- tsconfig.json | 24 +++++++++++------------- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/package.json b/package.json index 555545b..6f00262 100644 --- a/package.json +++ b/package.json @@ -86,28 +86,29 @@ "generate": "node script/generate-jsx.js && node script/build.js", "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 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, "strict": true + }, + "xo": { + "prettier": true } } diff --git a/tsconfig.json b/tsconfig.json index 6250214..8a5d91e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,23 +1,21 @@ { - "include": ["**/**.js", "**/**.jsx"], - "exclude": [ - "coverage/", - "node_modules/", - "lib/jsx-automatic.js", - "lib/jsx-classic.js" - ], "compilerOptions": { "checkJs": true, + "customConditions": ["development"], "declaration": true, "emitDeclarationOnly": true, "exactOptionalPropertyTypes": true, - "forceConsistentCasingInFileNames": true, + "jsx": "preserve", "lib": ["es2020"], "module": "node16", - "newLine": "lf", - "skipLibCheck": true, "strict": true, - "target": "es2020", - "jsx": "preserve" - } + "target": "es2020" + }, + "exclude": ["coverage/", "node_modules/"], + "include": [ + "**/**.js", + "**/**.jsx", + "lib/jsx-automatic.d.ts", + "lib/jsx-classic.d.ts" + ] } From 21d232dd4e5617ec83618a2a5462e7ec9263508e Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Tue, 1 Aug 2023 14:10:55 +0200 Subject: [PATCH 12/20] Refactor `.npmrc` --- .npmrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From de3143c4630862b7285241f15c4adc27b86bf470 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Tue, 1 Aug 2023 16:04:19 +0200 Subject: [PATCH 13/20] Refactor code-style --- lib/core.js | 204 +++-- lib/html.js | 8 +- lib/jsx-automatic.d.ts | 36 +- lib/jsx-classic.d.ts | 38 +- lib/runtime-html.js | 7 +- lib/runtime-svg.js | 5 +- lib/runtime.js | 57 +- lib/svg.js | 8 +- package.json | 13 + readme.md | 16 +- script/build.js | 6 +- script/generate-jsx.js | 2 +- test-d/automatic-h.tsx | 10 +- test-d/automatic-s.tsx | 9 +- test-d/classic-h.tsx | 18 +- test-d/classic-s.tsx | 18 +- test-d/index.ts | 21 +- test/core.js | 1715 ++++++++++++++++++++-------------------- test/jsx.jsx | 216 ++--- 19 files changed, 1253 insertions(+), 1154 deletions(-) diff --git a/lib/core.js b/lib/core.js index f15d1e1..bb04782 100644 --- a/lib/core.js +++ b/lib/core.js @@ -1,127 +1,149 @@ /** - * @typedef {import('hast').Root} Root - * @typedef {import('hast').Content} Content * @typedef {import('hast').Element} Element + * @typedef {import('hast').Nodes} Nodes * @typedef {import('hast').Properties} Properties + * @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} HResult * Result from a `h` (or `s`) call. * - * @typedef {string | number} HStyleValue + * @typedef {number | string} HStyleValue * Value for a CSS style field. * @typedef {Record} HStyle * Supported value of a `style` prop. - * @typedef {string | number | boolean | null | undefined} HPrimitiveValue + * @typedef {boolean | number | string | null | undefined} HPrimitiveValue * Primitive property value. - * @typedef {Array} HArrayValue + * @typedef {Array} HArrayValue * List of property values for space- or comma separated values (such as `className`). - * @typedef {HPrimitiveValue | HArrayValue} HPropertyValue + * @typedef {HArrayValue | HPrimitiveValue} HPropertyValue * Primitive value or list value. * @typedef {{[property: string]: HPropertyValue | HStyle}} HProperties * Acceptable value for element properties. * - * @typedef {string | number | null | undefined} HPrimitiveChild + * @typedef {number | string | null | undefined} HPrimitiveChild * Primitive children, either ignored (nullish), or turned into text nodes. - * @typedef {Array} HArrayChild + * @typedef {Array} HArrayChildNested * List of children. - * @typedef {Node | HPrimitiveChild | HArrayChild} HChild + * @typedef {Array} HArrayChild + * List of children. + * @typedef {HArrayChild | HPrimitiveChild | Nodes} HChild * 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) { 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 | undefined} [selector] - * @param {HProperties | HChild | null | undefined} [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 {...HChild} children + * @returns {Root} + * + * @overload + * @param {string} selector + * @param {HProperties} properties + * @param {...HChild} children + * @returns {Element} + * + * @overload + * @param {string} selector + * @param {...HChild} children + * @returns {Element} + * + * @param {string | null | undefined} [selector] + * Selector. + * @param {HChild | HProperties | null | undefined} [properties] + * Properties (or first child) (default: `undefined`). + * @param {...HChild} children + * Children. + * @returns {HResult} + * Result. + */ + function h(selector, properties, ...children) { + let index = -1 + /** @type {HResult} */ + let node + + if (selector === undefined || selector === null) { + node = {type: 'root', children: []} + // Properties are not supported for roots. + const child = /** @type {HChild} */ (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)) { - 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 {HChild | HProperties} value + * Value to check. * @param {string} name + * Tag name. * @returns {value is HProperties} + * Whether `value` is a properties object. */ function isProperties(value, name) { if ( @@ -150,10 +172,15 @@ 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 {HPropertyValue | HStyle} value + * Property value. + * @returns {undefined} + * Nothing. */ function addProperty(schema, properties, key, value) { const info = find(schema, key) @@ -192,12 +219,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 @@ -205,17 +235,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 {Array} nodes + * Children. * @param {HChild} value - * @returns {void} + * Child. + * @returns {undefined} + * Nothing. */ function addChild(nodes, value) { let index = -1 @@ -243,9 +277,13 @@ function addChild(nodes, value) { * Parse a single primitives. * * @param {Info} info + * Property information. * @param {string} name + * Property name. * @param {HPrimitiveValue} value + * Property value. * @returns {HPrimitiveValue} + * Property value. */ function parsePrimitive(info, name, value) { if (typeof value === 'string') { diff --git a/lib/html.js b/lib/html.js index cdc0917..d919ee3 100644 --- a/lib/html.js +++ b/lib/html.js @@ -5,14 +5,18 @@ * 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').ElementChildrenAttribute} h.JSX.ElementChildrenAttribute * @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' +// Note: this explicit type is needed, otherwise TS creates broken types. +/** @type {ReturnType} */ export const h = core(html, 'div') diff --git a/lib/jsx-automatic.d.ts b/lib/jsx-automatic.d.ts index 4b8d37d..1194759 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 {HChild, HProperties, HResult} from './core.js' export namespace JSX { /** - * This defines the return value of JSX syntax. + * Define the return value of JSX syntax. */ type Element = HResult /** - * 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 | { /** - * 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 } } - - /** - * 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..e2b03b6 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' /** - * 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 /** - * 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 | { /** - * 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 } } - -/** - * 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 index 636e2d9..4a8f563 100644 --- a/lib/runtime-html.js +++ b/lib/runtime-html.js @@ -1,6 +1,7 @@ -// Export `JSX` as a global for TypeScript. -import {runtime} from './runtime.js' import {h} from './html.js' +import {runtime} from './runtime.js' +// Export `JSX` as a global for TypeScript. export * from './jsx-automatic.js' -export const {Fragment, jsx, jsxs, jsxDEV} = runtime(h) + +export const {Fragment, jsx, jsxDEV, jsxs} = runtime(h) diff --git a/lib/runtime-svg.js b/lib/runtime-svg.js index b60c533..18abf66 100644 --- a/lib/runtime-svg.js +++ b/lib/runtime-svg.js @@ -1,6 +1,7 @@ -// Export `JSX` as a global for TypeScript. import {runtime} from './runtime.js' import {s} from './svg.js' +// Export `JSX` as a global for TypeScript. export * from './jsx-automatic.js' -export const {Fragment, jsx, jsxs, jsxDEV} = runtime(s) + +export const {Fragment, jsx, jsxDEV, jsxs} = runtime(s) diff --git a/lib/runtime.js b/lib/runtime.js index ce1f489..a5ee349 100644 --- a/lib/runtime.js +++ b/lib/runtime.js @@ -1,12 +1,12 @@ /** + * @typedef {import('./core.js').core} Core * @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').HResult} HResult * @typedef {import('./core.js').HStyle} HStyle - * @typedef {import('./core.js').core} Core + * @typedef {import('./core.js').Root} Root * * @typedef {Record} JSXProps */ @@ -15,26 +15,39 @@ * Create an automatic runtime. * * @param {ReturnType} f + * `h` function. + * @returns + * Automatic JSX runtime. */ 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) - } - ) + /** + * @overload + * @param {null} type + * @param {{children?: HChild}} 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 {HProperties & {children?: HChild}} props + * Properties. + * @returns {HResult} + * 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, jsxs: jsx, jsxDEV: jsx} + return {Fragment: null, jsx, jsxDEV: jsx, jsxs: jsx} } diff --git a/lib/svg.js b/lib/svg.js index 3c68309..3dc89e9 100644 --- a/lib/svg.js +++ b/lib/svg.js @@ -5,15 +5,19 @@ * 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').ElementChildrenAttribute} s.JSX.ElementChildrenAttribute * @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' +// Note: this explicit type is needed, otherwise TS creates broken types. +/** @type {ReturnType} */ export const s = core(svg, 'g', svgCaseSensitiveTagNames) diff --git a/package.json b/package.json index 6f00262..7217d97 100644 --- a/package.json +++ b/package.json @@ -106,9 +106,22 @@ "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 } } diff --git a/readme.md b/readme.md index 1af0e25..1ff6e5f 100644 --- a/readme.md +++ b/readme.md @@ -210,12 +210,12 @@ are used instead. ```ts type Child = - | string + | Array + | Node | number + | string | null | undefined - | Node - | Array ``` ### `Properties` @@ -229,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 > ``` @@ -248,7 +248,7 @@ Result from a `h` (or `s`) call (TypeScript type). ###### Type ```ts -type Result = Root | Element +type Result = Element | Root ``` ## Syntax tree 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 ca3dc6f..4c3b20c 100644 --- a/script/generate-jsx.js +++ b/script/generate-jsx.js @@ -1,8 +1,8 @@ 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)) diff --git a/test-d/automatic-h.tsx b/test-d/automatic-h.tsx index 828c85b..c732e1e 100644 --- a/test-d/automatic-h.tsx +++ b/test-d/automatic-h.tsx @@ -1,7 +1,7 @@ /* @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' @@ -9,6 +9,7 @@ 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', {})) @@ -45,7 +46,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 +55,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/index.ts b/test-d/index.ts index 3ef145e..2fdf187 100644 --- a/test-d/index.ts +++ b/test-d/index.ts @@ -1,9 +1,9 @@ -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 {h, s} from '../index.js' import {Fragment, jsx, jsxs} from '../jsx-runtime.js' +import {s as sFromRoot} from '../svg.js' // Ensure files are loadable in TS. expectType(hFromRoot()) @@ -11,7 +11,8 @@ expectType(sFromRoot()) 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 +21,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 +36,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'}, [ diff --git a/test/core.js b/test/core.js index 0f96b2a..052e42e 100644 --- a/test/core.js +++ b/test/core.js @@ -1,745 +1,713 @@ 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', () => { - assert.deepEqual( - Object.keys(coreMod).sort(), - ['h', 's'], - 'should expose the public api (`/`)' - ) - assert.deepEqual( - Object.keys(htmlMod).sort(), - ['h'], - 'should expose the public api (`/html`)' - ) - assert.deepEqual( - Object.keys(svgMod).sort(), - ['s'], - 'should expose the public api (`/svg`)' - ) - assert.deepEqual( - Object.keys(jsxCoreMod).sort(), - ['Fragment', 'jsx', 'jsxDEV', 'jsxs'], - 'should expose the public api (`/jsx-runtime`)' + +test('core', async function (t) { + await t.test('should expose the public api (`/`)', async function () { + assert.deepEqual(Object.keys(await import('../index.js')).sort(), [ + 'h', + 's' + ]) + }) + + await t.test('should expose the public api (`/html`)', async function () { + assert.deepEqual(Object.keys(await import('../html.js')).sort(), ['h']) + }) + + await t.test('should expose the public api (`/svg`)', async function () { + assert.deepEqual(Object.keys(await import('../svg.js')).sort(), ['s']) + }) + + await t.test( + 'should expose the public api (`/jsx-runtime`)', + async function () { + assert.deepEqual(Object.keys(await import('../jsx-runtime.js')).sort(), [ + 'Fragment', + 'jsx', + 'jsxDEV', + 'jsxs' + ]) + } ) - assert.deepEqual( - Object.keys(jsxHtmlMod).sort(), - ['Fragment', 'jsx', 'jsxDEV', 'jsxs'], - 'should expose the public api (`/html/jsx-runtime`)' + + await t.test( + 'should expose the public api (`/html/jsx-runtime`)', + async function () { + assert.deepEqual( + Object.keys(await import('../html/jsx-runtime.js')).sort(), + ['Fragment', 'jsx', 'jsxDEV', 'jsxs'] + ) + } ) - assert.deepEqual( - Object.keys(jsxSvgMod).sort(), - ['Fragment', 'jsx', 'jsxDEV', 'jsxs'], - 'should expose the public api (`/svg/jsx-runtime`)' + + await t.test( + 'should expose the public api (`/svg/jsx-runtime`)', + async function () { + assert.deepEqual( + Object.keys(await import('../svg/jsx-runtime.js')).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'}, @@ -753,10 +721,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')]), { @@ -777,13 +746,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: {}, @@ -791,24 +761,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: {}, @@ -820,13 +792,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: {}, @@ -834,114 +807,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: {}, @@ -959,45 +944,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('