From 63e5121f22dfc54a9f00af6a9a5b145237cf6c35 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Mon, 4 Apr 2022 10:35:30 +0200 Subject: [PATCH 01/13] Update dev-dependencies --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9775479..78ef5c2 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "type-coverage": "^2.0.0", "typescript": "^4.0.0", "unist-builder": "^3.0.0", - "xo": "^0.46.0" + "xo": "^0.48.0" }, "scripts": { "prepack": "npm run build && npm run format", From a4c177b992115b89fad56da7cfa36ef4b4717bd8 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Mon, 4 Apr 2022 10:37:35 +0200 Subject: [PATCH 02/13] Refactor code-style --- lib/index.js | 2 +- lib/omission/omission.js | 2 +- lib/omission/opening.js | 2 +- lib/tree.js | 10 ++++------ lib/types.js | 2 +- package.json | 4 ++-- readme.md | 2 +- 7 files changed, 11 insertions(+), 13 deletions(-) diff --git a/lib/index.js b/lib/index.js index d7d0282..2f2796d 100644 --- a/lib/index.js +++ b/lib/index.js @@ -11,7 +11,7 @@ import {omission} from './omission/index.js' import {one} from './tree.js' /** - * @param {Node|Array.} node + * @param {Node|Array} node * @param {Options} [options] * @returns {string} */ diff --git a/lib/omission/omission.js b/lib/omission/omission.js index be70781..11f2890 100644 --- a/lib/omission/omission.js +++ b/lib/omission/omission.js @@ -7,7 +7,7 @@ const own = {}.hasOwnProperty /** * Factory to check if a given node can have a tag omitted. * - * @param {Object.} handlers + * @param {Record} handlers * @returns {OmitHandle} */ export function omission(handlers) { diff --git a/lib/omission/opening.js b/lib/omission/opening.js index b356ec4..fa95de8 100644 --- a/lib/omission/opening.js +++ b/lib/omission/opening.js @@ -35,7 +35,7 @@ function html(node) { */ function head(node) { const children = node.children - /** @type {Array.} */ + /** @type {Array} */ const seen = [] let index = -1 /** @type {Child} */ diff --git a/lib/tree.js b/lib/tree.js index 2d7534c..0b99497 100644 --- a/lib/tree.js +++ b/lib/tree.js @@ -18,9 +18,7 @@ import {doctype} from './doctype.js' import {raw} from './raw.js' import {text} from './text.js' -/** - * @type {Object.} - */ +/** @type {Record} */ const handlers = { comment, doctype, @@ -56,7 +54,7 @@ export function one(ctx, node, index, parent) { * @param {Parent} parent */ export function all(ctx, parent) { - /** @type {Array.} */ + /** @type {Array} */ const results = [] const children = (parent && parent.children) || [] let index = -1 @@ -80,7 +78,7 @@ export function element(ctx, node, index, parent) { schema.space === 'svg' ? ctx.closeEmpty : ctx.voids.includes(node.tagName.toLowerCase()) - /** @type {Array.} */ + /** @type {Array} */ const parts = [] /** @type {string} */ let last @@ -138,7 +136,7 @@ export function element(ctx, node, index, parent) { * @returns {string} */ function serializeAttributes(ctx, props) { - /** @type {Array.} */ + /** @type {Array} */ const values = [] let index = -1 /** @type {string} */ diff --git a/lib/types.js b/lib/types.js index 39ee917..472ef08 100644 --- a/lib/types.js +++ b/lib/types.js @@ -70,7 +70,7 @@ * @property {boolean} tightClose * @property {boolean} collapseEmpty * @property {boolean} dangerous - * @property {Array.} voids + * @property {Array} voids * @property {StringifyEntitiesOptions} entities * @property {boolean} close * @property {boolean} closeEmpty diff --git a/package.json b/package.json index 78ef5c2..bc914a0 100644 --- a/package.json +++ b/package.json @@ -61,10 +61,10 @@ }, "scripts": { "prepack": "npm run build && npm run format", - "build": "rimraf \"{lib/omission/util/**,lib/omission/**,lib/**,test/**,}*.d.ts\" && tsc && type-coverage", + "build": "rimraf \"{lib,test}/**/*.d.ts\" \"*.d.ts\" && tsc && type-coverage", "format": "remark . -qfo && prettier . -w --loglevel warn && xo --fix", "test-api": "node test/index.js", - "test-coverage": "c8 --check-coverage --branches 100 --functions 100 --lines 100 --statements 100 --reporter lcov node test/index.js", + "test-coverage": "c8 --check-coverage --100 --reporter lcov npm run test-api", "test": "npm run build && npm run format && npm run test-coverage" }, "prettier": { diff --git a/readme.md b/readme.md index 0eea1e0..8baafeb 100644 --- a/readme.md +++ b/readme.md @@ -72,7 +72,7 @@ However, `useNamedReferences`, `useShortestReferences`, and ###### `options.voids` Tag names of [*elements*][element] to serialize without closing tag -(`Array.`, default: [`html-void-elements`][html-void-elements]). +(`Array`, default: [`html-void-elements`][html-void-elements]). Not used in the SVG space. From d672d2461d9349e78dfd37790ad510fabe397a6b Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Fri, 13 May 2022 12:38:52 +0200 Subject: [PATCH 03/13] Add improved docs --- package.json | 6 +- readme.md | 222 ++++++++++++++++++++++++++++++++++++++------------- 2 files changed, 173 insertions(+), 55 deletions(-) diff --git a/package.json b/package.json index bc914a0..264b94b 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,11 @@ }, "remarkConfig": { "plugins": [ - "preset-wooorm" + "preset-wooorm", + [ + "remark-lint-no-html", + false + ] ] }, "typeCoverage": { diff --git a/readme.md b/readme.md index 8baafeb..635876d 100644 --- a/readme.md +++ b/readme.md @@ -8,21 +8,80 @@ [![Backers][backers-badge]][collective] [![Chat][chat-badge]][chat] -[**hast**][hast] utility to serialize to HTML. +[hast][] utility to serialize hast as HTML. -## Install +## Contents + +* [What is this?](#what-is-this) +* [When should I use this?](#when-should-i-use-this) +* [Install](#install) +* [Use](#use) +* [API](#api) + * [`toHtml(tree[, options])`](#tohtmltree-options) +* [Syntax](#syntax) +* [Types](#types) +* [Compatibility](#compatibility) +* [Security](#security) +* [Related](#related) +* [Contribute](#contribute) +* [License](#license) + +## What is this? + +This package is a utility that turns a hast tree into a string of HTML. + +## When should I use this? + +You can use this utility when you’re want to get the serialized HTML that is +represented by the syntax tree, either because you’re done with the syntax tree, +or because you’re integrating with +another tool that does not support +syntax trees. + +This utility has many options to configure how the HTML is serialized. +These options help when building tools that make output pretty (e.g., +formatters) or ugly (e.g., minifiers). + +The utility [`hast-util-from-parse5`][hast-util-from-parse5] combined with +[`parse5`][parse5] does the inverse of this utility. +It turns HTML into hast. + +The rehype plugin [`rehype-stringify`][rehype-stringify] wraps this utility to +also serialize HTML at a higher-level (easier) abstraction. -This package is [ESM only](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c): -Node 12+ is needed to use it and it must be `import`ed instead of `require`d. +## Install -[npm][]: +This package is [ESM only][esm]. +In Node.js (version 12.20+, 14.14+, or 16.0+), install with [npm][]: ```sh npm install hast-util-to-html ``` +In Deno with [`esm.sh`][esmsh]: + +```js +import {toHtml} from "https://esm.sh/hast-util-to-html@8" +``` + +In browsers with [`esm.sh`][esmsh]: + +```html + +``` + ## Use +
Show install command for this example + +```sh +npm install hastscript hast-util-to-html +``` + +
+ ```js import {h} from 'hastscript' import {toHtml} from 'hast-util-to-html' @@ -45,36 +104,24 @@ Yields: ## API -This package exports the following identifiers: `toHtml`. +This package exports the identifier `toHtml`. There is no default export. ### `toHtml(tree[, options])` -Serialize the given [**hast**][hast] [*tree*][tree] (or list of nodes). +Serialize hast ([`Node`][node], `Array`) as HTML. -###### `options.space` - -Whether the [*root*][root] of the [*tree*][tree] is in the `'html'` or `'svg'` -space (enum, `'svg'` or `'html'`, default: `'html'`). +##### `options` -If an `svg` element is found in the HTML space, `toHtml` automatically switches -to the SVG space when entering the element, and switches back when exiting. +Configuration (optional). ###### `options.entities` -Configuration for [`stringify-entities`][stringify-entities] (`Object`, default: -`{}`). -Do not use `escapeOnly`, `attribute`, or `subset` (`toHtml` already passes -those, so they won’t work). -However, `useNamedReferences`, `useShortestReferences`, and -`omitOptionalSemicolons` are all fine. - -###### `options.voids` - -Tag names of [*elements*][element] to serialize without closing tag -(`Array`, default: [`html-void-elements`][html-void-elements]). - -Not used in the SVG space. +Define how to create character references (`Object`, default: `{}`). +Configuration is passed to [`stringify-entities`][stringify-entities]. +You can use the fields `useNamedReferences`, `useShortestReferences`, and +`omitOptionalSemicolons`. +You cannot use the fields `escapeOnly`, `attribute`, or `subset`). ###### `options.upperDoctype` @@ -99,8 +146,8 @@ Not used in the SVG space. ###### `options.omitOptionalTags` Omit optional opening and closing tags (`boolean`, default: `false`). -For example, in `
  1. one
  2. two
`, both `` -closing tags can be omitted. +For example, in `
  1. one
  2. two
`, both `` closing tags +can be omitted. The first because it’s followed by another `li`, the last because it’s followed by nothing. @@ -110,10 +157,11 @@ Not used in the SVG space. Collapse empty attributes: get `class` instead of `class=""` (`boolean`, default: `false`). -**Note**: boolean attributes, such as `hidden`, are always collapsed. Not used in the SVG space. +> 👉 **Note**: boolean attributes (such as `hidden`) are always collapsed. + ###### `options.closeSelfClosing` Close self-closing nodes with an extra slash (`/`): `` instead of @@ -135,12 +183,14 @@ Not used in the HTML space. Do not use an extra space when closing self-closing elements: `` instead of `` (`boolean`, default: `false`). -**Note**: Only used if `closeSelfClosing: true` or `closeEmptyElements: true`. + +> 👉 **Note**: only used if `closeSelfClosing: true` or +> `closeEmptyElements: true`. ###### `options.tightCommaSeparatedLists` Join known comma-separated attribute values with just a comma (`,`), instead of -padding them on the right as well (`,·`, where `·` represents a space) +padding them on the right as well (`,␠`, where `␠` represents a space) (`boolean`, default: `false`). ###### `options.tightAttributes` @@ -148,59 +198,111 @@ padding them on the right as well (`,·`, where `·` represents a space) Join attributes together, without whitespace, if possible: get `class="a b"title="c d"` instead of `class="a b" title="c d"` to save bytes (`boolean`, default: `false`). -**Note**: creates invalid (but working) markup. Not used in the SVG space. +> 👉 **Note**: intentionally creates parse errors in markup (how parse errors +> are handled is well defined, so this works but isn’t pretty). + ###### `options.tightDoctype` Drop unneeded spaces in doctypes: `` instead of `` to save bytes (`boolean`, default: `false`). -**Note**: creates invalid (but working) markup. + +> 👉 **Note**: intentionally creates parse errors in markup (how parse errors +> are handled is well defined, so this works but isn’t pretty). ###### `options.bogusComments` Use “bogus comments” instead of comments to save byes: `` instead of `` (`boolean`, default: `false`). -**Note**: creates invalid (but working) markup. + +> 👉 **Note**: intentionally creates parse errors in markup (how parse errors +> are handled is well defined, so this works but isn’t pretty). ###### `options.allowParseErrors` Do not encode characters which cause parse errors (even though they work), to save bytes (`boolean`, default: `false`). -**Note**: creates invalid (but working) markup. Not used in the SVG space. +> 👉 **Note**: intentionally creates parse errors in markup (how parse errors +> are handled is well defined, so this works but isn’t pretty). + ###### `options.allowDangerousCharacters` Do not encode some characters which cause XSS vulnerabilities in older browsers (`boolean`, default: `false`). -**Note**: Only set this if you completely trust the content. + +> ⚠️ **Danger**: only set this if you completely trust the content. ###### `options.allowDangerousHtml` Allow `raw` nodes and insert them as raw HTML. When falsey, encodes `raw` nodes (`boolean`, default: `false`). -**Note**: Only set this if you completely trust the content. + +> ⚠️ **Danger**: only set this if you completely trust the content. + +###### `options.space` + +Which space the document is in (`'svg'` or `'html'`, default: `'html'`). + +When an `` element is found in the HTML space, `rehype-stringify` already +automatically switches to and from the SVG space when entering and exiting it. + +> 👉 **Note**: hast is not XML. +> It supports SVG as embedded in HTML. +> It does not support the features available in XML. +> Passing SVG might break but fragments of modern SVG should be fine. +> Use [`xast`][xast] if you need to support SVG as XML. + +###### `options.voids` + +Tag names of elements to serialize without closing tag (`Array`, +default: [`html-void-elements`][html-void-elements]). + +Not used in the SVG space. + +> 👉 **Note**: It’s highly unlikely that you want to pass this, because hast is +> not for XML, and HTML will not add more void elements. + +##### Returns + +Serialized HTML (`string`). + +## Syntax + +HTML is serialized according to WHATWG HTML (the living standard), which is also +followed by browsers such as Chrome and Firefox. + +## Types + +This package is fully typed with [TypeScript][]. +It exports the additional type `Options`. + +## Compatibility + +Projects maintained by the unified collective are compatible with all maintained +versions of Node.js. +As of now, that is Node.js 12.20+, 14.14+, and 16.0+. +Our projects sometimes work with older versions, but this is not guaranteed. ## Security Use of `hast-util-to-html` can open you up to a [cross-site scripting (XSS)][xss] attack if the hast tree is unsafe. -Use [`hast-util-santize`][sanitize] to make the hast tree safe. +Use [`hast-util-santize`][hast-util-sanitize] to make the hast tree safe. ## Related -* [`hast-util-sanitize`][sanitize] - — Sanitize hast nodes -* [`rehype-stringify`](https://github.com/rehypejs/rehype/tree/HEAD/packages/rehype-stringify) - — Wrapper around this project for [**rehype**](https://github.com/wooorm/rehype) +* [`hast-util-sanitize`](https://github.com/syntax-tree/hast-util-sanitize) + — sanitize hast ## Contribute -See [`contributing.md` in `syntax-tree/.github`][contributing] for ways to get -started. +See [`contributing.md`][contributing] in [`syntax-tree/.github`][health] for +ways to get started. See [`support.md`][support] for ways to get help. This project has a [code of conduct][coc]. @@ -241,28 +343,40 @@ abide by its terms. [npm]: https://docs.npmjs.com/cli/install +[esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c + +[esmsh]: https://esm.sh + +[typescript]: https://www.typescriptlang.org + [license]: license [author]: https://wooorm.com -[contributing]: https://github.com/syntax-tree/.github/blob/HEAD/contributing.md +[health]: https://github.com/syntax-tree/.github + +[contributing]: https://github.com/syntax-tree/.github/blob/main/contributing.md -[support]: https://github.com/syntax-tree/.github/blob/HEAD/support.md +[support]: https://github.com/syntax-tree/.github/blob/main/support.md -[coc]: https://github.com/syntax-tree/.github/blob/HEAD/code-of-conduct.md +[coc]: https://github.com/syntax-tree/.github/blob/main/code-of-conduct.md + +[xss]: https://en.wikipedia.org/wiki/Cross-site_scripting + +[hast]: https://github.com/syntax-tree/hast + +[node]: https://github.com/syntax-tree/hast#nodes [html-void-elements]: https://github.com/wooorm/html-void-elements [stringify-entities]: https://github.com/wooorm/stringify-entities -[tree]: https://github.com/syntax-tree/unist#tree - -[root]: https://github.com/syntax-tree/unist#root +[hast-util-sanitize]: https://github.com/syntax-tree/hast-util-sanitize -[hast]: https://github.com/syntax-tree/hast +[hast-util-from-parse5]: https://github.com/syntax-tree/hast-util-from-parse5 -[element]: https://github.com/syntax-tree/hast#element +[parse5]: https://github.com/inikulin/parse5 -[xss]: https://en.wikipedia.org/wiki/Cross-site_scripting +[rehype-stringify]: https://github.com/rehypejs/rehype/tree/main/packages/rehype-stringify#readme -[sanitize]: https://github.com/syntax-tree/hast-util-sanitize +[xast]: https://github.com/syntax-tree/xast From b1c65cf62318eda36d54d7cb9f7d61298a656520 Mon Sep 17 00:00:00 2001 From: Titus Date: Sat, 28 May 2022 12:48:30 +0200 Subject: [PATCH 04/13] Fix typo --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 635876d..b247885 100644 --- a/readme.md +++ b/readme.md @@ -32,7 +32,7 @@ This package is a utility that turns a hast tree into a string of HTML. ## When should I use this? -You can use this utility when you’re want to get the serialized HTML that is +You can use this utility when you want to get the serialized HTML that is represented by the syntax tree, either because you’re done with the syntax tree, or because you’re integrating with another tool that does not support From fa187494263aa1087ba1c307abce72b2413e452e Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Sun, 29 May 2022 13:14:51 +0200 Subject: [PATCH 05/13] Add reference to `hast-util-from-html` --- readme.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/readme.md b/readme.md index b247885..31af392 100644 --- a/readme.md +++ b/readme.md @@ -42,8 +42,8 @@ This utility has many options to configure how the HTML is serialized. These options help when building tools that make output pretty (e.g., formatters) or ugly (e.g., minifiers). -The utility [`hast-util-from-parse5`][hast-util-from-parse5] combined with -[`parse5`][parse5] does the inverse of this utility. +The utility [`hast-util-from-html`][hast-util-from-html] does the inverse of +this utility. It turns HTML into hast. The rehype plugin [`rehype-stringify`][rehype-stringify] wraps this utility to @@ -52,7 +52,7 @@ also serialize HTML at a higher-level (easier) abstraction. ## Install This package is [ESM only][esm]. -In Node.js (version 12.20+, 14.14+, or 16.0+), install with [npm][]: +In Node.js (version 12.20+, 14.14+, 16.0+, or 18.0+), install with [npm][]: ```sh npm install hast-util-to-html @@ -373,9 +373,7 @@ abide by its terms. [hast-util-sanitize]: https://github.com/syntax-tree/hast-util-sanitize -[hast-util-from-parse5]: https://github.com/syntax-tree/hast-util-from-parse5 - -[parse5]: https://github.com/inikulin/parse5 +[hast-util-from-html]: https://github.com/syntax-tree/hast-util-from-html [rehype-stringify]: https://github.com/rehypejs/rehype/tree/main/packages/rehype-stringify#readme From 6133f34885cba418b1b1be324d8e4f9b3d0a5baa Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Sun, 8 Jan 2023 14:33:07 +0100 Subject: [PATCH 06/13] Update dev-dependencies --- lib/tree.js | 3 ++- package.json | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/tree.js b/lib/tree.js index 0b99497..c7ff955 100644 --- a/lib/tree.js +++ b/lib/tree.js @@ -50,8 +50,9 @@ export function one(ctx, node, index, parent) { /** * Serialize all children of `parent`. * - * @type {Handle} + * @param {Context} ctx * @param {Parent} parent + * @returns {string} */ export function all(ctx, parent) { /** @type {Array} */ diff --git a/package.json b/package.json index 264b94b..1f59516 100644 --- a/package.json +++ b/package.json @@ -50,14 +50,14 @@ "c8": "^7.0.0", "hastscript": "^7.0.0", "prettier": "^2.0.0", - "remark-cli": "^10.0.0", + "remark-cli": "^11.0.0", "remark-preset-wooorm": "^9.0.0", "rimraf": "^3.0.0", "tape": "^5.0.0", "type-coverage": "^2.0.0", "typescript": "^4.0.0", "unist-builder": "^3.0.0", - "xo": "^0.48.0" + "xo": "^0.53.0" }, "scripts": { "prepack": "npm run build && npm run format", From aa617b4e0132cdda4a586e0e056eb0c18f813491 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Sun, 8 Jan 2023 14:33:17 +0100 Subject: [PATCH 07/13] Update Actions --- .github/workflows/main.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fe284ad..89dc06c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,15 +7,15 @@ jobs: name: ${{matrix.node}} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: dcodeIO/setup-node-nvm@master + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 with: node-version: ${{matrix.node}} - run: npm install - run: npm test - - uses: codecov/codecov-action@v1 + - uses: codecov/codecov-action@v3 strategy: matrix: node: - - lts/erbium + - lts/fermium - node From 37bd2980c945b314d0bc101563ceb9c94c6ad84a Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Sun, 8 Jan 2023 14:54:54 +0100 Subject: [PATCH 08/13] Update `tsconfig.json` --- lib/index.js | 25 +++++++++++++------------ lib/omission/opening.js | 2 ++ lib/omission/util/siblings.js | 10 +++++----- lib/tree.js | 24 ++++++++++++------------ lib/types.js | 12 ++++++------ package.json | 5 ++--- tsconfig.json | 18 ++++++++++-------- 7 files changed, 50 insertions(+), 46 deletions(-) diff --git a/lib/index.js b/lib/index.js index 2f2796d..0c4e898 100644 --- a/lib/index.js +++ b/lib/index.js @@ -15,6 +15,7 @@ import {one} from './tree.js' * @param {Options} [options] * @returns {string} */ +// eslint-disable-next-line complexity export function toHtml(node, options = {}) { const quote = options.quote || '"' /** @type {Quote} */ @@ -32,20 +33,20 @@ export function toHtml(node, options = {}) { omit: options.omitOptionalTags ? omission : undefined, quote, alternative, - smart: options.quoteSmart, - unquoted: options.preferUnquoted, - tight: options.tightAttributes, - upperDoctype: options.upperDoctype, - tightDoctype: options.tightDoctype, - bogusComments: options.bogusComments, - tightLists: options.tightCommaSeparatedLists, - tightClose: options.tightSelfClosing, - collapseEmpty: options.collapseEmptyAttributes, - dangerous: options.allowDangerousHtml, + smart: options.quoteSmart || false, + unquoted: options.preferUnquoted || false, + tight: options.tightAttributes || false, + upperDoctype: options.upperDoctype || false, + tightDoctype: options.tightDoctype || false, + bogusComments: options.bogusComments || false, + tightLists: options.tightCommaSeparatedLists || false, + tightClose: options.tightSelfClosing || false, + collapseEmpty: options.collapseEmptyAttributes || false, + dangerous: options.allowDangerousHtml || false, voids: options.voids || htmlVoidElements.concat(), entities: options.entities || {}, - close: options.closeSelfClosing, - closeEmpty: options.closeEmptyElements + close: options.closeSelfClosing || false, + closeEmpty: options.closeEmptyElements || false } return one( diff --git a/lib/omission/opening.js b/lib/omission/opening.js index fa95de8..9dc02df 100644 --- a/lib/omission/opening.js +++ b/lib/omission/opening.js @@ -82,6 +82,7 @@ function colgroup(node, index, parent) { // Previous colgroup was already omitted. if ( + parent && isElement(previous, 'colgroup') && closing(previous, parent.children.indexOf(previous), parent) ) { @@ -102,6 +103,7 @@ function tbody(node, index, parent) { // Previous table section was already omitted. if ( + parent && isElement(previous, ['thead', 'tbody']) && closing(previous, parent.children.indexOf(previous), parent) ) { diff --git a/lib/omission/util/siblings.js b/lib/omission/util/siblings.js index 750dcde..b5bc719 100644 --- a/lib/omission/util/siblings.js +++ b/lib/omission/util/siblings.js @@ -19,14 +19,14 @@ function siblings(increment) { /** * Find applicable siblings in a direction. * - * @param {Parent} parent - * @param {number} index - * @param {boolean} [includeWhitespace=false] + * @param {Parent | null | undefined} parent + * @param {number | null | undefined} index + * @param {boolean | null | undefined} [includeWhitespace=false] * @returns {Child} */ function sibling(parent, index, includeWhitespace) { - const siblings = parent && parent.children - let offset = index + increment + const siblings = parent ? parent.children : [] + let offset = (index || 0) + increment let next = siblings && siblings[offset] if (!includeWhitespace) { diff --git a/lib/tree.js b/lib/tree.js index c7ff955..8a89f35 100644 --- a/lib/tree.js +++ b/lib/tree.js @@ -51,7 +51,7 @@ export function one(ctx, node, index, parent) { * Serialize all children of `parent`. * * @param {Context} ctx - * @param {Parent} parent + * @param {Parent | null | undefined} parent * @returns {string} */ export function all(ctx, parent) { @@ -133,7 +133,7 @@ export function element(ctx, node, index, parent) { /** * @param {Context} ctx - * @param {Properties} props + * @param {Properties | undefined} props * @returns {string} */ function serializeAttributes(ctx, props) { @@ -142,20 +142,20 @@ function serializeAttributes(ctx, props) { let index = -1 /** @type {string} */ let key - /** @type {string} */ - let value - /** @type {string} */ - let last - for (key in props) { - if (props[key] !== undefined && props[key] !== null) { - value = serializeAttribute(ctx, key, props[key]) - if (value) values.push(value) + if (props) { + for (key in props) { + if (props[key] !== undefined && props[key] !== null) { + const value = serializeAttribute(ctx, key, props[key]) + if (value) values.push(value) + } } } while (++index < values.length) { - last = ctx.tight ? values[index].charAt(values[index].length - 1) : null + const last = ctx.tight + ? values[index].charAt(values[index].length - 1) + : null // In tight mode, don’t add a space after quoted attributes. if (index !== values.length - 1 && last !== '"' && last !== "'") { @@ -176,7 +176,7 @@ function serializeAttributes(ctx, props) { function serializeAttribute(ctx, key, value) { const info = find(ctx.schema, key) let quote = ctx.quote - /** @type {string} */ + /** @type {string | undefined} */ let result if (info.overloadedBoolean && (value === info.attribute || value === '')) { diff --git a/lib/types.js b/lib/types.js index 472ef08..aedb41f 100644 --- a/lib/types.js +++ b/lib/types.js @@ -17,17 +17,17 @@ * @callback Handle * @param {Context} context * @param {Node} node - * @param {number|null} index - * @param {Parent|null} parent + * @param {number | null} index + * @param {Parent | undefined | null} parent * @returns {string} * * @callback OmitHandle * @param {Element} node - * @param {number|null} index - * @param {Parent|null} parent + * @param {number | null | undefined} index + * @param {Parent | null | undefined} parent * @returns {boolean} * - * @typedef {{opening?: OmitHandle, closing?: OmitHandle}} Omission + * @typedef {{opening: OmitHandle, closing: OmitHandle}} Omission * * @typedef {'html'|'svg'} Space * @typedef {'"'|"'"} Quote @@ -57,7 +57,7 @@ * @property {number} valid * @property {number} safe * @property {Schema} schema - * @property {Omission} omit + * @property {Omission | undefined} omit * @property {Quote} quote * @property {Quote} alternative * @property {boolean} smart diff --git a/package.json b/package.json index 1f59516..385f009 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,6 @@ "prettier": "^2.0.0", "remark-cli": "^11.0.0", "remark-preset-wooorm": "^9.0.0", - "rimraf": "^3.0.0", "tape": "^5.0.0", "type-coverage": "^2.0.0", "typescript": "^4.0.0", @@ -61,9 +60,9 @@ }, "scripts": { "prepack": "npm run build && npm run format", - "build": "rimraf \"{lib,test}/**/*.d.ts\" \"*.d.ts\" && tsc && type-coverage", + "build": "tsc --build --clean && tsc --build && type-coverage", "format": "remark . -qfo && prettier . -w --loglevel warn && xo --fix", - "test-api": "node test/index.js", + "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 format && npm run test-coverage" }, diff --git a/tsconfig.json b/tsconfig.json index 2b103bd..1bc9e99 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,15 +1,17 @@ { - "include": ["*.js", "lib/**/*.js", "test/**/*.js"], + "include": ["**/**.js"], + "exclude": ["coverage/", "node_modules/"], "compilerOptions": { - "target": "ES2020", - "lib": ["ES2020"], - "module": "ES2020", - "moduleResolution": "node", - "allowJs": true, "checkJs": true, "declaration": true, "emitDeclarationOnly": true, - "allowSyntheticDefaultImports": true, - "skipLibCheck": true + "exactOptionalPropertyTypes": true, + "forceConsistentCasingInFileNames": true, + "lib": ["es2020"], + "module": "node16", + "newLine": "lf", + "skipLibCheck": true, + "strict": true, + "target": "es2020" } } From 97417a43b484fc1d676d849721a3a72a9cc99fd4 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Sun, 8 Jan 2023 18:57:38 +0100 Subject: [PATCH 09/13] Refactor code-style * Add support for `null` in API input types * Only pass `undefined` around internally, not `null` * Add more docs to JSDoc * Refactor a *ton* of code * Rename `entities` option to `characterReferences` * Add exports of useful types (`CharacterReferences`, `Quote`, `Space`) --- index.js | 3 + lib/comment.js | 32 --- lib/constants.js | 26 --- lib/doctype.js | 15 -- lib/handle/comment.js | 45 ++++ lib/handle/doctype.js | 28 +++ lib/handle/element.js | 268 +++++++++++++++++++++++ lib/handle/index.js | 47 ++++ lib/handle/raw.js | 27 +++ lib/handle/root.js | 23 ++ lib/handle/text.js | 36 ++++ lib/index.js | 104 ++++++--- lib/omission/closing.js | 300 +++++++++++++++++++------- lib/omission/index.js | 9 - lib/omission/omission.js | 3 + lib/omission/opening.js | 78 +++++-- lib/omission/util/comment.js | 9 - lib/omission/util/siblings.js | 4 +- lib/omission/util/whitespace-start.js | 21 -- lib/raw.js | 15 -- lib/text.js | 23 -- lib/tree.js | 275 ----------------------- lib/types.js | 212 +++++++++++++----- package.json | 7 +- readme.md | 81 ++++--- test/attribute.js | 24 +-- test/core.js | 4 +- test/doctype.js | 6 +- test/raw.js | 6 +- test/root.js | 2 +- test/svg.js | 2 +- 31 files changed, 1074 insertions(+), 661 deletions(-) delete mode 100644 lib/comment.js delete mode 100644 lib/constants.js delete mode 100644 lib/doctype.js create mode 100644 lib/handle/comment.js create mode 100644 lib/handle/doctype.js create mode 100644 lib/handle/element.js create mode 100644 lib/handle/index.js create mode 100644 lib/handle/raw.js create mode 100644 lib/handle/root.js create mode 100644 lib/handle/text.js delete mode 100644 lib/omission/index.js delete mode 100644 lib/omission/util/comment.js delete mode 100644 lib/omission/util/whitespace-start.js delete mode 100644 lib/raw.js delete mode 100644 lib/text.js delete mode 100644 lib/tree.js diff --git a/index.js b/index.js index e81b821..2163112 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,8 @@ /** + * @typedef {import('./lib/types.js').CharacterReferences} CharacterReferences * @typedef {import('./lib/types.js').Options} Options + * @typedef {import('./lib/types.js').Quote} Quote + * @typedef {import('./lib/types.js').Space} Space */ export {toHtml} from './lib/index.js' diff --git a/lib/comment.js b/lib/comment.js deleted file mode 100644 index bcefb52..0000000 --- a/lib/comment.js +++ /dev/null @@ -1,32 +0,0 @@ -/** - * @typedef {import('./types.js').Handle} Handle - * @typedef {import('./types.js').Comment} Comment - */ - -import {stringifyEntities} from 'stringify-entities' - -/** - * @type {Handle} - * @param {Comment} node - */ -export function comment(ctx, node) { - // See: - return ctx.bogusComments - ? '']}) - ) + - '>' - : '|--!>|' - - /** - * @param {string} $0 - */ - function encode($0) { - return stringifyEntities( - $0, - Object.assign({}, ctx.entities, {subset: ['<', '>']}) - ) - } -} diff --git a/lib/constants.js b/lib/constants.js deleted file mode 100644 index eae9c7e..0000000 --- a/lib/constants.js +++ /dev/null @@ -1,26 +0,0 @@ -// Maps of subsets. -// Each value is a matrix of tuples. -// The first value causes parse errors, the second is valid. -// Of both values, the first value is unsafe, and the second is safe. -export const constants = { - // See: . - name: [ - ['\t\n\f\r &/=>'.split(''), '\t\n\f\r "&\'/=>`'.split('')], - ['\0\t\n\f\r "&\'/<=>'.split(''), '\0\t\n\f\r "&\'/<=>`'.split('')] - ], - // See: . - unquoted: [ - ['\t\n\f\r &>'.split(''), '\0\t\n\f\r "&\'<=>`'.split('')], - ['\0\t\n\f\r "&\'<=>`'.split(''), '\0\t\n\f\r "&\'<=>`'.split('')] - ], - // See: . - single: [ - ["&'".split(''), '"&\'`'.split('')], - ["\0&'".split(''), '\0"&\'`'.split('')] - ], - // See: . - double: [ - ['"&'.split(''), '"&\'`'.split('')], - ['\0"&'.split(''), '\0"&\'`'.split('')] - ] -} diff --git a/lib/doctype.js b/lib/doctype.js deleted file mode 100644 index 79c3e14..0000000 --- a/lib/doctype.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * @typedef {import('./types.js').Handle} Handle - */ - -/** - * @type {Handle} - */ -export function doctype(ctx) { - return ( - '' - ) -} diff --git a/lib/handle/comment.js b/lib/handle/comment.js new file mode 100644 index 0000000..5049ef0 --- /dev/null +++ b/lib/handle/comment.js @@ -0,0 +1,45 @@ +/** + * @typedef {import('../types.js').Comment} Comment + * @typedef {import('../types.js').Parent} Parent + * @typedef {import('../types.js').State} State + */ + +import {stringifyEntities} from 'stringify-entities' + +/** + * Serialize a comment. + * + * @param {Comment} node + * Node to handle. + * @param {number | undefined} _1 + * Index of `node` in `parent. + * @param {Parent | undefined} _2 + * Parent of `node`. + * @param {State} state + * Info passed around about the current state. + * @returns {string} + * Serialized node. + */ +export function comment(node, _1, _2, state) { + // See: + return state.settings.bogusComments + ? '']}) + ) + + '>' + : '|--!>|' + + /** + * @param {string} $0 + */ + function encode($0) { + return stringifyEntities( + $0, + Object.assign({}, state.settings.characterReferences, { + subset: ['<', '>'] + }) + ) + } +} diff --git a/lib/handle/doctype.js b/lib/handle/doctype.js new file mode 100644 index 0000000..f7737a4 --- /dev/null +++ b/lib/handle/doctype.js @@ -0,0 +1,28 @@ +/** + * @typedef {import('../types.js').DocType} DocType + * @typedef {import('../types.js').Parent} Parent + * @typedef {import('../types.js').State} State + */ + +/** + * Serialize a doctype. + * + * @param {DocType} _1 + * Node to handle. + * @param {number | undefined} _2 + * Index of `node` in `parent. + * @param {Parent | undefined} _3 + * Parent of `node`. + * @param {State} state + * Info passed around about the current state. + * @returns {string} + * Serialized node. + */ +export function doctype(_1, _2, _3, state) { + return ( + '' + ) +} diff --git a/lib/handle/element.js b/lib/handle/element.js new file mode 100644 index 0000000..14ba7cd --- /dev/null +++ b/lib/handle/element.js @@ -0,0 +1,268 @@ +/** + * @typedef {import('../types.js').State} State + * @typedef {import('../types.js').Parent} Parent + * @typedef {import('../types.js').Element} Element + * @typedef {import('../types.js').Properties} Properties + * @typedef {import('../types.js').PropertyValue} PropertyValue + */ + +import {ccount} from 'ccount' +import {stringify as commas} from 'comma-separated-tokens' +import {svg, find} from 'property-information' +import {stringify as spaces} from 'space-separated-tokens' +import {stringifyEntities} from 'stringify-entities' +import {opening} from '../omission/opening.js' +import {closing} from '../omission/closing.js' + +/** + * Maps of subsets. + * + * Each value is a matrix of tuples. + * The value at `0` causes parse errors, the value at `1` is valid. + * Of both, the value at `0` is unsafe, and the value at `1` is safe. + * + * @type {Record<'name' | 'unquoted' | 'single' | 'double', Array<[Array, Array]>>} + */ +const constants = { + // See: . + name: [ + ['\t\n\f\r &/=>'.split(''), '\t\n\f\r "&\'/=>`'.split('')], + ['\0\t\n\f\r "&\'/<=>'.split(''), '\0\t\n\f\r "&\'/<=>`'.split('')] + ], + // See: . + unquoted: [ + ['\t\n\f\r &>'.split(''), '\0\t\n\f\r "&\'<=>`'.split('')], + ['\0\t\n\f\r "&\'<=>`'.split(''), '\0\t\n\f\r "&\'<=>`'.split('')] + ], + // See: . + single: [ + ["&'".split(''), '"&\'`'.split('')], + ["\0&'".split(''), '\0"&\'`'.split('')] + ], + // See: . + double: [ + ['"&'.split(''), '"&\'`'.split('')], + ['\0"&'.split(''), '\0"&\'`'.split('')] + ] +} + +/** + * Serialize an element node. + * + * @param {Element} node + * Node to handle. + * @param {number | undefined} index + * Index of `node` in `parent. + * @param {Parent | undefined} parent + * Parent of `node`. + * @param {State} state + * Info passed around about the current state. + * @returns {string} + * Serialized node. + */ +// eslint-disable-next-line complexity +export function element(node, index, parent, state) { + const schema = state.schema + const omit = schema.space === 'svg' ? false : state.settings.omitOptionalTags + let selfClosing = + schema.space === 'svg' + ? state.settings.closeEmptyElements + : state.settings.voids.includes(node.tagName.toLowerCase()) + /** @type {Array} */ + const parts = [] + /** @type {string} */ + let last + + if (schema.space === 'html' && node.tagName === 'svg') { + state.schema = svg + } + + const attrs = serializeAttributes(state, node.properties) + + const content = state.all( + schema.space === 'html' && node.tagName === 'template' ? node.content : node + ) + + state.schema = schema + + // If the node is categorised as void, but it has children, remove the + // categorisation. + // This enables for example `menuitem`s, which are void in W3C HTML but not + // void in WHATWG HTML, to be stringified properly. + if (content) selfClosing = false + + if (attrs || !omit || !opening(node, index, parent)) { + parts.push('<', node.tagName, attrs ? ' ' + attrs : '') + + if ( + selfClosing && + (schema.space === 'svg' || state.settings.closeSelfClosing) + ) { + last = attrs.charAt(attrs.length - 1) + if ( + !state.settings.tightSelfClosing || + last === '/' || + (last && last !== '"' && last !== "'") + ) { + parts.push(' ') + } + + parts.push('/') + } + + parts.push('>') + } + + parts.push(content) + + if (!selfClosing && (!omit || !closing(node, index, parent))) { + parts.push('') + } + + return parts.join('') +} + +/** + * @param {State} state + * @param {Properties | null | undefined} props + * @returns {string} + */ +function serializeAttributes(state, props) { + /** @type {Array} */ + const values = [] + let index = -1 + /** @type {string} */ + let key + + if (props) { + for (key in props) { + if (props[key] !== undefined && props[key] !== null) { + const value = serializeAttribute(state, key, props[key]) + if (value) values.push(value) + } + } + } + + while (++index < values.length) { + const last = state.settings.tightAttributes + ? values[index].charAt(values[index].length - 1) + : null + + // In tight mode, don’t add a space after quoted attributes. + if (index !== values.length - 1 && last !== '"' && last !== "'") { + values[index] += ' ' + } + } + + return values.join('') +} + +/** + * @param {State} state + * @param {string} key + * @param {PropertyValue} value + * @returns {string} + */ +// eslint-disable-next-line complexity +function serializeAttribute(state, key, value) { + const info = find(state.schema, key) + const x = + state.settings.allowParseErrors && state.schema.space === 'html' ? 0 : 1 + const y = state.settings.allowDangerousCharacters ? 0 : 1 + let quote = state.quote + /** @type {string | undefined} */ + let result + + if (info.overloadedBoolean && (value === info.attribute || value === '')) { + value = true + } else if ( + info.boolean || + (info.overloadedBoolean && typeof value !== 'string') + ) { + value = Boolean(value) + } + + if ( + value === undefined || + value === null || + value === false || + (typeof value === 'number' && Number.isNaN(value)) + ) { + return '' + } + + const name = stringifyEntities( + info.attribute, + Object.assign({}, state.settings.characterReferences, { + // Always encode without parse errors in non-HTML. + subset: constants.name[x][y] + }) + ) + + // No value. + // There is currently only one boolean property in SVG: `[download]` on + // ``. + // This property does not seem to work in browsers (Firefox, Safari, Chrome), + // so I can’t test if dropping the value works. + // But I assume that it should: + // + // ```html + // + // + // + // + // + // + // ``` + // + // See: + if (value === true) return name + + // `spaces` doesn’t accept a second argument, but it’s given here just to + // keep the code cleaner. + value = Array.isArray(value) + ? (info.commaSeparated ? commas : spaces)(value, { + padLeft: !state.settings.tightCommaSeparatedLists + }) + : String(value) + + if (state.settings.collapseEmptyAttributes && !value) return name + + // Check unquoted value. + if (state.settings.preferUnquoted) { + result = stringifyEntities( + value, + Object.assign({}, state.settings.characterReferences, { + subset: constants.unquoted[x][y], + attribute: true + }) + ) + } + + // If we don’t want unquoted, or if `value` contains character references when + // unquoted… + if (result !== value) { + // If the alternative is less common than `quote`, switch. + if ( + state.settings.quoteSmart && + ccount(value, quote) > ccount(value, state.alternative) + ) { + quote = state.alternative + } + + result = + quote + + stringifyEntities( + value, + Object.assign({}, state.settings.characterReferences, { + // Always encode without parse errors in non-HTML. + subset: (quote === "'" ? constants.single : constants.double)[x][y], + attribute: true + }) + ) + + quote + } + + // Don’t add a `=` for unquoted empties. + return name + (result ? '=' + result : result) +} diff --git a/lib/handle/index.js b/lib/handle/index.js new file mode 100644 index 0000000..9783345 --- /dev/null +++ b/lib/handle/index.js @@ -0,0 +1,47 @@ +/** + * @typedef {import('../types.js').State} State + * @typedef {import('../types.js').Node} Node + * @typedef {import('../types.js').Parent} Parent + */ + +import {zwitch} from 'zwitch' +import {comment} from './comment.js' +import {doctype} from './doctype.js' +import {element} from './element.js' +import {raw} from './raw.js' +import {root} from './root.js' +import {text} from './text.js' + +/** + * @type {(node: Node, index: number | undefined, parent: Parent | undefined, state: State) => string} + */ +export const handle = zwitch('type', { + invalid, + unknown, + handlers: {comment, doctype, element, raw, root, text} +}) + +/** + * Fail when a non-node is found in the tree. + * + * @param {unknown} node + * Unknown value. + * @returns {never} + * Never. + */ +function invalid(node) { + throw new Error('Expected node, not `' + node + '`') +} + +/** + * Fail when a node with an unknown type is found in the tree. + * + * @param {unknown} node + * Unknown node. + * @returns {never} + * Never. + */ +function unknown(node) { + // @ts-expect-error: `type` is defined. + throw new Error('Cannot compile unknown node `' + node.type + '`') +} diff --git a/lib/handle/raw.js b/lib/handle/raw.js new file mode 100644 index 0000000..d32555c --- /dev/null +++ b/lib/handle/raw.js @@ -0,0 +1,27 @@ +/** + * @typedef {import('../types.js').State} State + * @typedef {import('../types.js').Parent} Parent + * @typedef {import('../types.js').Raw} Raw + */ + +import {text} from './text.js' + +/** + * Serialize a raw node. + * + * @param {Raw} node + * Node to handle. + * @param {number | undefined} index + * Index of `node` in `parent. + * @param {Parent | undefined} parent + * Parent of `node`. + * @param {State} state + * Info passed around about the current state. + * @returns {string} + * Serialized node. + */ +export function raw(node, index, parent, state) { + return state.settings.allowDangerousHtml + ? node.value + : text(node, index, parent, state) +} diff --git a/lib/handle/root.js b/lib/handle/root.js new file mode 100644 index 0000000..974d2cc --- /dev/null +++ b/lib/handle/root.js @@ -0,0 +1,23 @@ +/** + * @typedef {import('../types.js').Root} Root + * @typedef {import('../types.js').Parent} Parent + * @typedef {import('../types.js').State} State + */ + +/** + * Serialize a root. + * + * @param {Root} node + * Node to handle. + * @param {number | undefined} _1 + * Index of `node` in `parent. + * @param {Parent | undefined} _2 + * Parent of `node`. + * @param {State} state + * Info passed around about the current state. + * @returns {string} + * Serialized node. + */ +export function root(node, _1, _2, state) { + return state.all(node) +} diff --git a/lib/handle/text.js b/lib/handle/text.js new file mode 100644 index 0000000..58d03d4 --- /dev/null +++ b/lib/handle/text.js @@ -0,0 +1,36 @@ +/** + * @typedef {import('../types.js').State} State + * @typedef {import('../types.js').Parent} Parent + * @typedef {import('../types.js').Raw} Raw + * @typedef {import('../types.js').Text} Text + */ + +import {stringifyEntities} from 'stringify-entities' + +/** + * Serialize a text node. + * + * @param {Text | Raw} node + * Node to handle. + * @param {number | undefined} _ + * Index of `node` in `parent. + * @param {Parent | undefined} parent + * Parent of `node`. + * @param {State} state + * Info passed around about the current state. + * @returns {string} + * Serialized node. + */ +export function text(node, _, parent, state) { + // Check if content of `node` should be escaped. + return parent && + parent.type === 'element' && + (parent.tagName === 'script' || parent.tagName === 'style') + ? node.value + : stringifyEntities( + node.value, + Object.assign({}, state.settings.characterReferences, { + subset: ['<', '&'] + }) + ) +} diff --git a/lib/index.js b/lib/index.js index 0c4e898..9140ef9 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,59 +1,103 @@ /** * @typedef {import('./types.js').Node} Node + * @typedef {import('./types.js').Parent} Parent + * @typedef {import('./types.js').Content} Content * @typedef {import('./types.js').Options} Options - * @typedef {import('./types.js').Context} Context + * @typedef {import('./types.js').Settings} Settings + * @typedef {import('./types.js').State} State * @typedef {import('./types.js').Quote} Quote */ import {html, svg} from 'property-information' import {htmlVoidElements} from 'html-void-elements' -import {omission} from './omission/index.js' -import {one} from './tree.js' +import {handle} from './handle/index.js' /** - * @param {Node|Array} node + * @param {Node | Array} node * @param {Options} [options] * @returns {string} */ // eslint-disable-next-line complexity export function toHtml(node, options = {}) { const quote = options.quote || '"' - /** @type {Quote} */ const alternative = quote === '"' ? "'" : '"' if (quote !== '"' && quote !== "'") { throw new Error('Invalid quote `' + quote + '`, expected `\'` or `"`') } - /** @type {Context} */ - const context = { - valid: options.allowParseErrors ? 0 : 1, - safe: options.allowDangerousCharacters ? 0 : 1, + /** @type {State} */ + const state = { + one, + all, + settings: { + omitOptionalTags: options.omitOptionalTags || false, + allowParseErrors: options.allowParseErrors || false, + allowDangerousCharacters: options.allowDangerousCharacters || false, + quoteSmart: options.quoteSmart || false, + preferUnquoted: options.preferUnquoted || false, + tightAttributes: options.tightAttributes || false, + upperDoctype: options.upperDoctype || false, + tightDoctype: options.tightDoctype || false, + bogusComments: options.bogusComments || false, + tightCommaSeparatedLists: options.tightCommaSeparatedLists || false, + tightSelfClosing: options.tightSelfClosing || false, + collapseEmptyAttributes: options.collapseEmptyAttributes || false, + allowDangerousHtml: options.allowDangerousHtml || false, + voids: options.voids || htmlVoidElements, + characterReferences: + options.characterReferences || options.entities || {}, + closeSelfClosing: options.closeSelfClosing || false, + closeEmptyElements: options.closeEmptyElements || false + }, schema: options.space === 'svg' ? svg : html, - omit: options.omitOptionalTags ? omission : undefined, quote, - alternative, - smart: options.quoteSmart || false, - unquoted: options.preferUnquoted || false, - tight: options.tightAttributes || false, - upperDoctype: options.upperDoctype || false, - tightDoctype: options.tightDoctype || false, - bogusComments: options.bogusComments || false, - tightLists: options.tightCommaSeparatedLists || false, - tightClose: options.tightSelfClosing || false, - collapseEmpty: options.collapseEmptyAttributes || false, - dangerous: options.allowDangerousHtml || false, - voids: options.voids || htmlVoidElements.concat(), - entities: options.entities || {}, - close: options.closeSelfClosing || false, - closeEmpty: options.closeEmptyElements || false + alternative } - return one( - context, - // @ts-ignore Assume `node` does not contain a root. + return state.one( Array.isArray(node) ? {type: 'root', children: node} : node, - null, - null + undefined, + undefined ) } + +/** + * Serialize a node. + * + * @this {State} + * Info passed around about the current state. + * @param {Node} node + * Node to handle. + * @param {number | undefined} index + * Index of `node` in `parent. + * @param {Parent | undefined} parent + * Parent of `node`. + * @returns {string} + * Serialized node. + */ +function one(node, index, parent) { + return handle(node, index, parent, this) +} + +/** + * Serialize all children of `parent`. + * + * @this {State} + * Info passed around about the current state. + * @param {Parent | undefined} parent + * Parent whose children to serialize. + * @returns {string} + */ +export function all(parent) { + /** @type {Array} */ + const results = [] + const children = (parent && parent.children) || [] + let index = -1 + + while (++index < children.length) { + results[index] = this.one(children[index], index, parent) + } + + return results.join('') +} diff --git a/lib/omission/closing.js b/lib/omission/closing.js index cd4f680..f295d58 100644 --- a/lib/omission/closing.js +++ b/lib/omission/closing.js @@ -1,11 +1,10 @@ /** - * @typedef {import('../types.js').OmitHandle} OmitHandle + * @typedef {import('../types.js').Element} Element + * @typedef {import('../types.js').Parent} Parent */ -import {isElement} from 'hast-util-is-element' -import {comment} from './util/comment.js' +import {whitespace} from 'hast-util-whitespace' import {siblingAfter} from './util/siblings.js' -import {whitespaceStart} from './util/whitespace-start.js' import {omission} from './omission.js' export const closing = omission({ @@ -34,180 +33,313 @@ export const closing = omission({ /** * Macro for ``, ``, and ``. * - * @type {OmitHandle} + * @param {Element} _ + * Element. + * @param {number | undefined} index + * Index of element in parent. + * @param {Parent | undefined} parent + * Parent of element. + * @returns {boolean} + * Whether the closing tag can be omitted. */ function headOrColgroupOrCaption(_, index, parent) { const next = siblingAfter(parent, index, true) - return !next || (!comment(next) && !whitespaceStart(next)) + return ( + !next || + (next.type !== 'comment' && + !(next.type === 'text' && whitespace(next.value.charAt(0)))) + ) } /** * Whether to omit ``. * - * @type {OmitHandle} + * @param {Element} _ + * Element. + * @param {number | undefined} index + * Index of element in parent. + * @param {Parent | undefined} parent + * Parent of element. + * @returns {boolean} + * Whether the closing tag can be omitted. */ function html(_, index, parent) { const next = siblingAfter(parent, index) - return !next || !comment(next) + return !next || next.type !== 'comment' } /** * Whether to omit ``. * - * @type {OmitHandle} + * @param {Element} _ + * Element. + * @param {number | undefined} index + * Index of element in parent. + * @param {Parent | undefined} parent + * Parent of element. + * @returns {boolean} + * Whether the closing tag can be omitted. */ function body(_, index, parent) { const next = siblingAfter(parent, index) - return !next || !comment(next) + return !next || next.type !== 'comment' } /** * Whether to omit `

`. * - * @type {OmitHandle} + * @param {Element} _ + * Element. + * @param {number | undefined} index + * Index of element in parent. + * @param {Parent | undefined} parent + * Parent of element. + * @returns {boolean} + * Whether the closing tag can be omitted. */ +// eslint-disable-next-line complexity function p(_, index, parent) { const next = siblingAfter(parent, index) return next - ? isElement(next, [ - 'address', - 'article', - 'aside', - 'blockquote', - 'details', - 'div', - 'dl', - 'fieldset', - 'figcaption', - 'figure', - 'footer', - 'form', - 'h1', - 'h2', - 'h3', - 'h4', - 'h5', - 'h6', - 'header', - 'hgroup', - 'hr', - 'main', - 'menu', - 'nav', - 'ol', - 'p', - 'pre', - 'section', - 'table', - 'ul' - ]) + ? next.type === 'element' && + (next.tagName === 'address' || + next.tagName === 'article' || + next.tagName === 'aside' || + next.tagName === 'blockquote' || + next.tagName === 'details' || + next.tagName === 'div' || + next.tagName === 'dl' || + next.tagName === 'fieldset' || + next.tagName === 'figcaption' || + next.tagName === 'figure' || + next.tagName === 'footer' || + next.tagName === 'form' || + next.tagName === 'h1' || + next.tagName === 'h2' || + next.tagName === 'h3' || + next.tagName === 'h4' || + next.tagName === 'h5' || + next.tagName === 'h6' || + next.tagName === 'header' || + next.tagName === 'hgroup' || + next.tagName === 'hr' || + next.tagName === 'main' || + next.tagName === 'menu' || + next.tagName === 'nav' || + next.tagName === 'ol' || + next.tagName === 'p' || + next.tagName === 'pre' || + next.tagName === 'section' || + next.tagName === 'table' || + next.tagName === 'ul') : !parent || // Confusing parent. - !isElement(parent, [ - 'a', - 'audio', - 'del', - 'ins', - 'map', - 'noscript', - 'video' - ]) + !( + parent.type === 'element' && + (parent.tagName === 'a' || + parent.tagName === 'audio' || + parent.tagName === 'del' || + parent.tagName === 'ins' || + parent.tagName === 'map' || + parent.tagName === 'noscript' || + parent.tagName === 'video') + ) } /** * Whether to omit ``. * - * @type {OmitHandle} + * @param {Element} _ + * Element. + * @param {number | undefined} index + * Index of element in parent. + * @param {Parent | undefined} parent + * Parent of element. + * @returns {boolean} + * Whether the closing tag can be omitted. */ function li(_, index, parent) { const next = siblingAfter(parent, index) - return !next || isElement(next, 'li') + return !next || (next.type === 'element' && next.tagName === 'li') } /** * Whether to omit ``. * - * @type {OmitHandle} + * @param {Element} _ + * Element. + * @param {number | undefined} index + * Index of element in parent. + * @param {Parent | undefined} parent + * Parent of element. + * @returns {boolean} + * Whether the closing tag can be omitted. */ function dt(_, index, parent) { const next = siblingAfter(parent, index) - return next && isElement(next, ['dt', 'dd']) + return ( + next && + next.type === 'element' && + (next.tagName === 'dt' || next.tagName === 'dd') + ) } /** * Whether to omit ``. * - * @type {OmitHandle} + * @param {Element} _ + * Element. + * @param {number | undefined} index + * Index of element in parent. + * @param {Parent | undefined} parent + * Parent of element. + * @returns {boolean} + * Whether the closing tag can be omitted. */ function dd(_, index, parent) { const next = siblingAfter(parent, index) - return !next || isElement(next, ['dt', 'dd']) + return ( + !next || + (next.type === 'element' && + (next.tagName === 'dt' || next.tagName === 'dd')) + ) } /** * Whether to omit `` or ``. * - * @type {OmitHandle} + * @param {Element} _ + * Element. + * @param {number | undefined} index + * Index of element in parent. + * @param {Parent | undefined} parent + * Parent of element. + * @returns {boolean} + * Whether the closing tag can be omitted. */ function rubyElement(_, index, parent) { const next = siblingAfter(parent, index) - return !next || isElement(next, ['rp', 'rt']) + return ( + !next || + (next.type === 'element' && + (next.tagName === 'rp' || next.tagName === 'rt')) + ) } /** * Whether to omit ``. * - * @type {OmitHandle} + * @param {Element} _ + * Element. + * @param {number | undefined} index + * Index of element in parent. + * @param {Parent | undefined} parent + * Parent of element. + * @returns {boolean} + * Whether the closing tag can be omitted. */ function optgroup(_, index, parent) { const next = siblingAfter(parent, index) - return !next || isElement(next, 'optgroup') + return !next || (next.type === 'element' && next.tagName === 'optgroup') } /** * Whether to omit ``. * - * @type {OmitHandle} + * @param {Element} _ + * Element. + * @param {number | undefined} index + * Index of element in parent. + * @param {Parent | undefined} parent + * Parent of element. + * @returns {boolean} + * Whether the closing tag can be omitted. */ function option(_, index, parent) { const next = siblingAfter(parent, index) - return !next || isElement(next, ['option', 'optgroup']) + return ( + !next || + (next.type === 'element' && + (next.tagName === 'option' || next.tagName === 'optgroup')) + ) } /** * Whether to omit ``. * - * @type {OmitHandle} + * @param {Element} _ + * Element. + * @param {number | undefined} index + * Index of element in parent. + * @param {Parent | undefined} parent + * Parent of element. + * @returns {boolean} + * Whether the closing tag can be omitted. */ function menuitem(_, index, parent) { const next = siblingAfter(parent, index) - return !next || isElement(next, ['menuitem', 'hr', 'menu']) + return ( + !next || + (next.type === 'element' && + (next.tagName === 'menuitem' || + next.tagName === 'hr' || + next.tagName === 'menu')) + ) } /** * Whether to omit ``. * - * @type {OmitHandle} + * @param {Element} _ + * Element. + * @param {number | undefined} index + * Index of element in parent. + * @param {Parent | undefined} parent + * Parent of element. + * @returns {boolean} + * Whether the closing tag can be omitted. */ function thead(_, index, parent) { const next = siblingAfter(parent, index) - return next && isElement(next, ['tbody', 'tfoot']) + return ( + next && + next.type === 'element' && + (next.tagName === 'tbody' || next.tagName === 'tfoot') + ) } /** * Whether to omit ``. * - * @type {OmitHandle} + * @param {Element} _ + * Element. + * @param {number | undefined} index + * Index of element in parent. + * @param {Parent | undefined} parent + * Parent of element. + * @returns {boolean} + * Whether the closing tag can be omitted. */ function tbody(_, index, parent) { const next = siblingAfter(parent, index) - return !next || isElement(next, ['tbody', 'tfoot']) + return ( + !next || + (next.type === 'element' && + (next.tagName === 'tbody' || next.tagName === 'tfoot')) + ) } /** * Whether to omit ``. * - * @type {OmitHandle} + * @param {Element} _ + * Element. + * @param {number | undefined} index + * Index of element in parent. + * @param {Parent | undefined} parent + * Parent of element. + * @returns {boolean} + * Whether the closing tag can be omitted. */ function tfoot(_, index, parent) { return !siblingAfter(parent, index) @@ -216,19 +348,37 @@ function tfoot(_, index, parent) { /** * Whether to omit ``. * - * @type {OmitHandle} + * @param {Element} _ + * Element. + * @param {number | undefined} index + * Index of element in parent. + * @param {Parent | undefined} parent + * Parent of element. + * @returns {boolean} + * Whether the closing tag can be omitted. */ function tr(_, index, parent) { const next = siblingAfter(parent, index) - return !next || isElement(next, 'tr') + return !next || (next.type === 'element' && next.tagName === 'tr') } /** * Whether to omit `` or ``. * - * @type {OmitHandle} + * @param {Element} _ + * Element. + * @param {number | undefined} index + * Index of element in parent. + * @param {Parent | undefined} parent + * Parent of element. + * @returns {boolean} + * Whether the closing tag can be omitted. */ function cells(_, index, parent) { const next = siblingAfter(parent, index) - return !next || isElement(next, ['td', 'th']) + return ( + !next || + (next.type === 'element' && + (next.tagName === 'td' || next.tagName === 'th')) + ) } diff --git a/lib/omission/index.js b/lib/omission/index.js deleted file mode 100644 index b68a40d..0000000 --- a/lib/omission/index.js +++ /dev/null @@ -1,9 +0,0 @@ -/** - * @typedef {import('../types.js').Omission} Omission - */ - -import {opening} from './opening.js' -import {closing} from './closing.js' - -/** @type {Omission} */ -export const omission = {opening, closing} diff --git a/lib/omission/omission.js b/lib/omission/omission.js index 11f2890..d00fc07 100644 --- a/lib/omission/omission.js +++ b/lib/omission/omission.js @@ -8,7 +8,10 @@ const own = {}.hasOwnProperty * Factory to check if a given node can have a tag omitted. * * @param {Record} handlers + * Omission handlers, where each key is a tag name, and each value is the + * corresponding handler. * @returns {OmitHandle} + * Whether to omit a tag of an element. */ export function omission(handlers) { return omit diff --git a/lib/omission/opening.js b/lib/omission/opening.js index 9dc02df..1f0077d 100644 --- a/lib/omission/opening.js +++ b/lib/omission/opening.js @@ -1,12 +1,11 @@ /** - * @typedef {import('../types.js').OmitHandle} OmitHandle - * @typedef {import('../types.js').Child} Child + * @typedef {import('../types.js').Element} Element + * @typedef {import('../types.js').Parent} Parent + * @typedef {import('../types.js').Content} Content */ -import {isElement} from 'hast-util-is-element' -import {comment} from './util/comment.js' +import {whitespace} from 'hast-util-whitespace' import {siblingBefore, siblingAfter} from './util/siblings.js' -import {whitespaceStart} from './util/whitespace-start.js' import {closing} from './closing.js' import {omission} from './omission.js' @@ -21,29 +20,36 @@ export const opening = omission({ /** * Whether to omit ``. * - * @type {OmitHandle} + * @param {Element} node + * Element. + * @returns {boolean} + * Whether the opening tag can be omitted. */ function html(node) { const head = siblingAfter(node, -1) - return !head || !comment(head) + return !head || head.type !== 'comment' } /** * Whether to omit `
`. * - * @type {OmitHandle} + * @param {Element} node + * Element. + * @returns {boolean} + * Whether the opening tag can be omitted. */ function head(node) { const children = node.children /** @type {Array} */ const seen = [] let index = -1 - /** @type {Child} */ - let child while (++index < children.length) { - child = children[index] - if (isElement(child, ['title', 'base'])) { + const child = children[index] + if ( + child.type === 'element' && + (child.tagName === 'title' || child.tagName === 'base') + ) { if (seen.includes(child.tagName)) return false seen.push(child.tagName) } @@ -55,16 +61,26 @@ function head(node) { /** * Whether to omit ``. * - * @type {OmitHandle} + * @param {Element} node + * Element. + * @returns {boolean} + * Whether the opening tag can be omitted. */ function body(node) { const head = siblingAfter(node, -1, true) return ( !head || - (!comment(head) && - !whitespaceStart(head) && - !isElement(head, ['meta', 'link', 'script', 'style', 'template'])) + (head.type !== 'comment' && + !(head.type === 'text' && whitespace(head.value.charAt(0))) && + !( + head.type === 'element' && + (head.tagName === 'meta' || + head.tagName === 'link' || + head.tagName === 'script' || + head.tagName === 'style' || + head.tagName === 'template') + )) ) } @@ -74,7 +90,14 @@ function body(node) { * implement in the closing tag, to the same effect, so we handle it there * instead. * - * @type {OmitHandle} + * @param {Element} node + * Element. + * @param {number | undefined} index + * Index of element in parent. + * @param {Parent | undefined} parent + * Parent of element. + * @returns {boolean} + * Whether the opening tag can be omitted. */ function colgroup(node, index, parent) { const previous = siblingBefore(parent, index) @@ -83,19 +106,28 @@ function colgroup(node, index, parent) { // Previous colgroup was already omitted. if ( parent && - isElement(previous, 'colgroup') && + previous && + previous.type === 'element' && + previous.tagName === 'colgroup' && closing(previous, parent.children.indexOf(previous), parent) ) { return false } - return head && isElement(head, 'col') + return head && head.type === 'element' && head.tagName === 'col' } /** * Whether to omit ``. * - * @type {OmitHandle} + * @param {Element} node + * Element. + * @param {number | undefined} index + * Index of element in parent. + * @param {Parent | undefined} parent + * Parent of element. + * @returns {boolean} + * Whether the opening tag can be omitted. */ function tbody(node, index, parent) { const previous = siblingBefore(parent, index) @@ -104,11 +136,13 @@ function tbody(node, index, parent) { // Previous table section was already omitted. if ( parent && - isElement(previous, ['thead', 'tbody']) && + previous && + previous.type === 'element' && + (previous.tagName === 'thead' || previous.tagName === 'tbody') && closing(previous, parent.children.indexOf(previous), parent) ) { return false } - return head && isElement(head, 'tr') + return head && head.type === 'element' && head.tagName === 'tr' } diff --git a/lib/omission/util/comment.js b/lib/omission/util/comment.js deleted file mode 100644 index 8c7f5a4..0000000 --- a/lib/omission/util/comment.js +++ /dev/null @@ -1,9 +0,0 @@ -/** - * @typedef {import('../../types.js').Comment} Comment - */ - -import {convert} from 'unist-util-is' - -/** @type {import('unist-util-is').AssertPredicate} */ -// @ts-ignore -export const comment = convert('comment') diff --git a/lib/omission/util/siblings.js b/lib/omission/util/siblings.js index b5bc719..f0d9909 100644 --- a/lib/omission/util/siblings.js +++ b/lib/omission/util/siblings.js @@ -1,6 +1,6 @@ /** * @typedef {import('../../types.js').Parent} Parent - * @typedef {import('../../types.js').Child} Child + * @typedef {import('../../types.js').Content} Content */ import {whitespace} from 'hast-util-whitespace' @@ -22,7 +22,7 @@ function siblings(increment) { * @param {Parent | null | undefined} parent * @param {number | null | undefined} index * @param {boolean | null | undefined} [includeWhitespace=false] - * @returns {Child} + * @returns {Content} */ function sibling(parent, index, includeWhitespace) { const siblings = parent ? parent.children : [] diff --git a/lib/omission/util/whitespace-start.js b/lib/omission/util/whitespace-start.js deleted file mode 100644 index c0b9b09..0000000 --- a/lib/omission/util/whitespace-start.js +++ /dev/null @@ -1,21 +0,0 @@ -/** - * @typedef {import('../../types.js').Node} Node - * @typedef {import('../../types.js').Text} Text - */ - -import {convert} from 'unist-util-is' -import {whitespace} from 'hast-util-whitespace' - -/** @type {import('unist-util-is').AssertPredicate} */ -// @ts-ignore -const isText = convert('text') - -/** - * Check if `node` starts with whitespace. - * - * @param {Node} node - * @returns {boolean} - */ -export function whitespaceStart(node) { - return isText(node) && whitespace(node.value.charAt(0)) -} diff --git a/lib/raw.js b/lib/raw.js deleted file mode 100644 index 122eb60..0000000 --- a/lib/raw.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * @typedef {import('./types.js').Handle} Handle - * @typedef {import('./types.js').Raw} Raw - */ - -import {text} from './text.js' - -/** - * @type {Handle} - * @param {Raw} node - */ -export function raw(ctx, node, index, parent) { - // @ts-ignore Hush. - return ctx.dangerous ? node.value : text(ctx, node, index, parent) -} diff --git a/lib/text.js b/lib/text.js deleted file mode 100644 index 472b77c..0000000 --- a/lib/text.js +++ /dev/null @@ -1,23 +0,0 @@ -/** - * @typedef {import('./types.js').Handle} Handle - * @typedef {import('./types.js').Text} Text - */ - -import {stringifyEntities} from 'stringify-entities' - -/** - * @type {Handle} - * @param {Text} node - */ -export function text(ctx, node, _, parent) { - // Check if content of `node` should be escaped. - return parent && - parent.type === 'element' && - // @ts-expect-error: hush. - (parent.tagName === 'script' || parent.tagName === 'style') - ? node.value - : stringifyEntities( - node.value, - Object.assign({}, ctx.entities, {subset: ['<', '&']}) - ) -} diff --git a/lib/tree.js b/lib/tree.js deleted file mode 100644 index 8a89f35..0000000 --- a/lib/tree.js +++ /dev/null @@ -1,275 +0,0 @@ -/** - * @typedef {import('./types.js').Handle} Handle - * @typedef {import('./types.js').Element} Element - * @typedef {import('./types.js').Context} Context - * @typedef {import('./types.js').Properties} Properties - * @typedef {import('./types.js').PropertyValue} PropertyValue - * @typedef {import('./types.js').Parent} Parent - */ - -import {svg, find} from 'property-information' -import {stringify as spaces} from 'space-separated-tokens' -import {stringify as commas} from 'comma-separated-tokens' -import {stringifyEntities} from 'stringify-entities' -import {ccount} from 'ccount' -import {constants} from './constants.js' -import {comment} from './comment.js' -import {doctype} from './doctype.js' -import {raw} from './raw.js' -import {text} from './text.js' - -/** @type {Record} */ -const handlers = { - comment, - doctype, - element, - // @ts-ignore `raw` is nonstandard - raw, - // @ts-ignore `root` is a parent. - root: all, - text -} - -const own = {}.hasOwnProperty - -/** - * @type {Handle} - */ -export function one(ctx, node, index, parent) { - if (!node || !node.type) { - throw new Error('Expected node, not `' + node + '`') - } - - if (!own.call(handlers, node.type)) { - throw new Error('Cannot compile unknown node `' + node.type + '`') - } - - return handlers[node.type](ctx, node, index, parent) -} - -/** - * Serialize all children of `parent`. - * - * @param {Context} ctx - * @param {Parent | null | undefined} parent - * @returns {string} - */ -export function all(ctx, parent) { - /** @type {Array} */ - const results = [] - const children = (parent && parent.children) || [] - let index = -1 - - while (++index < children.length) { - results[index] = one(ctx, children[index], index, parent) - } - - return results.join('') -} - -/** - * @type {Handle} - * @param {Element} node - */ -// eslint-disable-next-line complexity -export function element(ctx, node, index, parent) { - const schema = ctx.schema - const omit = schema.space === 'svg' ? undefined : ctx.omit - let selfClosing = - schema.space === 'svg' - ? ctx.closeEmpty - : ctx.voids.includes(node.tagName.toLowerCase()) - /** @type {Array} */ - const parts = [] - /** @type {string} */ - let last - - if (schema.space === 'html' && node.tagName === 'svg') { - ctx.schema = svg - } - - const attrs = serializeAttributes(ctx, node.properties) - - const content = all( - ctx, - schema.space === 'html' && node.tagName === 'template' ? node.content : node - ) - - ctx.schema = schema - - // If the node is categorised as void, but it has children, remove the - // categorisation. - // This enables for example `menuitem`s, which are void in W3C HTML but not - // void in WHATWG HTML, to be stringified properly. - if (content) selfClosing = false - - if (attrs || !omit || !omit.opening(node, index, parent)) { - parts.push('<', node.tagName, attrs ? ' ' + attrs : '') - - if (selfClosing && (schema.space === 'svg' || ctx.close)) { - last = attrs.charAt(attrs.length - 1) - if ( - !ctx.tightClose || - last === '/' || - (last && last !== '"' && last !== "'") - ) { - parts.push(' ') - } - - parts.push('/') - } - - parts.push('>') - } - - parts.push(content) - - if (!selfClosing && (!omit || !omit.closing(node, index, parent))) { - parts.push('') - } - - return parts.join('') -} - -/** - * @param {Context} ctx - * @param {Properties | undefined} props - * @returns {string} - */ -function serializeAttributes(ctx, props) { - /** @type {Array} */ - const values = [] - let index = -1 - /** @type {string} */ - let key - - if (props) { - for (key in props) { - if (props[key] !== undefined && props[key] !== null) { - const value = serializeAttribute(ctx, key, props[key]) - if (value) values.push(value) - } - } - } - - while (++index < values.length) { - const last = ctx.tight - ? values[index].charAt(values[index].length - 1) - : null - - // In tight mode, don’t add a space after quoted attributes. - if (index !== values.length - 1 && last !== '"' && last !== "'") { - values[index] += ' ' - } - } - - return values.join('') -} - -/** - * @param {Context} ctx - * @param {string} key - * @param {PropertyValue} value - * @returns {string} - */ -// eslint-disable-next-line complexity -function serializeAttribute(ctx, key, value) { - const info = find(ctx.schema, key) - let quote = ctx.quote - /** @type {string | undefined} */ - let result - - if (info.overloadedBoolean && (value === info.attribute || value === '')) { - value = true - } else if ( - info.boolean || - (info.overloadedBoolean && typeof value !== 'string') - ) { - value = Boolean(value) - } - - if ( - value === undefined || - value === null || - value === false || - (typeof value === 'number' && Number.isNaN(value)) - ) { - return '' - } - - const name = stringifyEntities( - info.attribute, - Object.assign({}, ctx.entities, { - // Always encode without parse errors in non-HTML. - subset: - constants.name[ctx.schema.space === 'html' ? ctx.valid : 1][ctx.safe] - }) - ) - - // No value. - // There is currently only one boolean property in SVG: `[download]` on - // ``. - // This property does not seem to work in browsers (FF, Sa, Ch), so I can’t - // test if dropping the value works. - // But I assume that it should: - // - // ```html - // - // - // - // - // - // - // ``` - // - // See: - if (value === true) return name - - value = - typeof value === 'object' && 'length' in value - ? // `spaces` doesn’t accept a second argument, but it’s given here just to - // keep the code cleaner. - (info.commaSeparated ? commas : spaces)(value, { - padLeft: !ctx.tightLists - }) - : String(value) - - if (ctx.collapseEmpty && !value) return name - - // Check unquoted value. - if (ctx.unquoted) { - result = stringifyEntities( - value, - Object.assign({}, ctx.entities, { - subset: constants.unquoted[ctx.valid][ctx.safe], - attribute: true - }) - ) - } - - // If we don’t want unquoted, or if `value` contains character references when - // unquoted… - if (result !== value) { - // If the alternative is less common than `quote`, switch. - if (ctx.smart && ccount(value, quote) > ccount(value, ctx.alternative)) { - quote = ctx.alternative - } - - result = - quote + - stringifyEntities( - value, - Object.assign({}, ctx.entities, { - // Always encode without parse errors in non-HTML. - subset: (quote === "'" ? constants.single : constants.double)[ - ctx.schema.space === 'html' ? ctx.valid : 1 - ][ctx.safe], - attribute: true - }) - ) + - quote - } - - // Don’t add a `=` for unquoted empties. - return name + (result ? '=' + result : result) -} diff --git a/lib/types.js b/lib/types.js index aedb41f..06d685a 100644 --- a/lib/types.js +++ b/lib/types.js @@ -1,79 +1,175 @@ /** - * @typedef {import('hast').Parent} Parent - * @typedef {import('hast').Literal} Literal + * @typedef {import('unist').Parent} UnistParent + * @typedef {import('unist').Literal} UnistLiteral * @typedef {import('hast').Root} Root * @typedef {import('hast').Comment} Comment + * @typedef {import('hast').DocType} DocType * @typedef {import('hast').Element} Element * @typedef {import('hast').Text} Text - * @typedef {Literal & {type: 'raw'}} Raw - * @typedef {Parent['children'][number]} Child + * @typedef {import('hast').Content} Content * @typedef {import('hast').Properties} Properties - * @typedef {Properties[string]} PropertyValue - * @typedef {Child|Root} Node - * + * @typedef {import('hast-util-raw/complex-types.js').Raw} Raw * @typedef {import('stringify-entities').Options} StringifyEntitiesOptions * @typedef {import('property-information').Schema} Schema - * - * @callback Handle - * @param {Context} context - * @param {Node} node - * @param {number | null} index - * @param {Parent | undefined | null} parent - * @returns {string} + */ + +/** + * @typedef {Content | Root} Node + * @typedef {Extract} Parent + * @typedef {Properties[keyof Properties]} PropertyValue * * @callback OmitHandle - * @param {Element} node - * @param {number | null | undefined} index - * @param {Parent | null | undefined} parent + * Check if a tag can be omitted. + * @param {Element} element + * Element to check. + * @param {number | undefined} index + * Index of element in parent. + * @param {Parent | undefined} parent + * Parent of element. * @returns {boolean} + * Whether to omit a tag. + * + * @typedef {'html' | 'svg'} Space + * Namespace. * - * @typedef {{opening: OmitHandle, closing: OmitHandle}} Omission + * @typedef {Omit} CharacterReferences * - * @typedef {'html'|'svg'} Space - * @typedef {'"'|"'"} Quote + * @typedef {'"' | "'"} Quote + * HTML quotes for attribute values. * * @typedef Options - * @property {Space} [space='html'] Whether the *root* of the *tree* is in the `'html'` or `'svg'` space. If an `svg` element is found in the HTML space, `toHtml` automatically switches to the SVG space when entering the element, and switches back when exiting - * @property {Omit} [entities] Configuration for `stringify-entities` - * @property {Array} [voids] Tag names of *elements* to serialize without closing tag. Not used in the SVG space. Defaults to `html-void-elements` - * @property {boolean} [upperDoctype=false] Use a `
  • one
  • two
  • `, both `` closing tags can be omitted. The first because it’s followed by another `li`, the last because it’s followed by nothing. Not used in the SVG space - * @property {boolean} [collapseEmptyAttributes=false] Collapse empty attributes: `class=""` is stringified as `class` instead. **Note**: boolean attributes, such as `hidden`, are always collapsed. Not used in the SVG space - * @property {boolean} [closeSelfClosing=false] Close self-closing nodes with an extra slash (`/`): `` instead of ``. See `tightSelfClosing` to control whether a space is used before the slash. Not used in the SVG space - * @property {boolean} [closeEmptyElements=false] Close SVG elements without any content with slash (`/`) on the opening tag instead of an end tag: `` instead of ``. See `tightSelfClosing` to control whether a space is used before the slash. Not used in the HTML space - * @property {boolean} [tightSelfClosing=false] Do not use an extra space when closing self-closing elements: `` instead of ``. **Note**: Only used if `closeSelfClosing: true` or `closeEmptyElements: true` - * @property {boolean} [tightCommaSeparatedLists=false] Join known comma-separated attribute values with just a comma (`,`), instead of padding them on the right as well (`,·`, where `·` represents a space) - * @property {boolean} [tightAttributes=false] Join attributes together, without whitespace, if possible: `class="a b" title="c d"` is stringified as `class="a b"title="c d"` instead to save bytes. **Note**: creates invalid (but working) markup. Not used in the SVG space - * @property {boolean} [tightDoctype=false] Drop unneeded spaces in doctypes: `` instead of `` to save bytes. **Note**: creates invalid (but working) markup - * @property {boolean} [bogusComments=false] Use “bogus comments” instead of comments to save byes: `` instead of ``. **Note**: creates invalid (but working) markup - * @property {boolean} [allowParseErrors=false] Do not encode characters which cause parse errors (even though they work), to save bytes. **Note**: creates invalid (but working) markup. Not used in the SVG space - * @property {boolean} [allowDangerousCharacters=false] Do not encode some characters which cause XSS vulnerabilities in older browsers. **Note**: Only set this if you completely trust the content - * @property {boolean} [allowDangerousHtml=false] Allow `raw` nodes and insert them as raw HTML. When falsey, encodes `raw` nodes. **Note**: Only set this if you completely trust the content - * - * @typedef Context - * @property {number} valid - * @property {number} safe + * Configuration. + * @property {Space | null | undefined} [space='html'] + * When an `` element is found in the HTML space, this package already + * automatically switches to and from the SVG space when entering and exiting + * it. + * + * > 👉 **Note**: hast is not XML. + * > It supports SVG as embedded in HTML. + * > It does not support the features available in XML. + * > Passing SVG might break but fragments of modern SVG should be fine. + * > Use [`xast`][xast] if you need to support SVG as XML. + * @property {CharacterReferences | null | undefined} [entities] + * Deprecated: please use `characterReferences`. + * @property {CharacterReferences | null | undefined} [characterReferences] + * Configure how to serialize character references. + * @property {ReadonlyArray | null | undefined} [voids] + * Tag names of elements to serialize without closing tag. + * + * Not used in the SVG space. + * + * > 👉 **Note**: It’s highly unlikely that you want to pass this, because + * > hast is not for XML, and HTML will not add more void elements. + * @property {boolean | null | undefined} [upperDoctype=false] + * Use a `
  • one
  • two
  • `, both `` closing + * tags can be omitted. + * The first because it’s followed by another `li`, the last because it’s + * followed by nothing. + * + * Not used in the SVG space. + * @property {boolean | null | undefined} [collapseEmptyAttributes=false] + * Collapse empty attributes: get `class` instead of `class=""`. + * + * Not used in the SVG space. + * + * > 👉 **Note**: boolean attributes (such as `hidden`) are always collapsed. + * @property {boolean | null | undefined} [closeSelfClosing=false] + * Close self-closing nodes with an extra slash (`/`): `` instead of + * ``. + * + * See `tightSelfClosing` to control whether a space is used before the + * slash. + * + * Not used in the SVG space. + * @property {boolean | null | undefined} [closeEmptyElements=false] + * Close SVG elements without any content with slash (`/`) on the opening tag + * instead of an end tag: `` instead of ``. + * + * See `tightSelfClosing` to control whether a space is used before the + * slash. + * + * Not used in the HTML space. + * @property {boolean | null | undefined} [tightSelfClosing=false] + * Do not use an extra space when closing self-closing elements: `` + * instead of ``. + * + * > 👉 **Note**: only used if `closeSelfClosing: true` or + * > `closeEmptyElements: true`. + * @property {boolean | null | undefined} [tightCommaSeparatedLists=false] + * Join known comma-separated attribute values with just a comma (`,`), + * instead of padding them on the right as well (`,␠`, where `␠` represents a + * space). + * @property {boolean | null | undefined} [tightAttributes=false] + * Join attributes together, without whitespace, if possible: get + * `class="a b"title="c d"` instead of `class="a b" title="c d"` to save + * bytes. + * + * Not used in the SVG space. + * + * > 👉 **Note**: intentionally creates parse errors in markup (how parse + * > errors are handled is well defined, so this works but isn’t pretty). + * @property {boolean | null | undefined} [tightDoctype=false] + * Drop unneeded spaces in doctypes: `` instead of + * `` to save bytes. + * + * > 👉 **Note**: intentionally creates parse errors in markup (how parse + * > errors are handled is well defined, so this works but isn’t pretty). + * @property {boolean | null | undefined} [bogusComments=false] + * Use “bogus comments” instead of comments to save byes: `` + * instead of ``. + * + * > 👉 **Note**: intentionally creates parse errors in markup (how parse + * > errors are handled is well defined, so this works but isn’t pretty). + * @property {boolean | null | undefined} [allowParseErrors=false] + * Do not encode characters which cause parse errors (even though they work), + * to save bytes. + * + * Not used in the SVG space. + * + * > 👉 **Note**: intentionally creates parse errors in markup (how parse + * > errors are handled is well defined, so this works but isn’t pretty). + * @property {boolean | null | undefined} [allowDangerousCharacters=false] + * Do not encode some characters which cause XSS vulnerabilities in older + * browsers. + * + * > ⚠️ **Danger**: only set this if you completely trust the content. + * @property {boolean | null | undefined} [allowDangerousHtml=false] + * Allow `raw` nodes and insert them as raw HTML. + * + * When `false`, `Raw` nodes are encoded. + * + * > ⚠️ **Danger**: only set this if you completely trust the content. + * + * @typedef {Omit}>, 'quote' | 'entities' | 'space'>} Settings + * + * @typedef State + * Info passed around about the current state. + * @property {(node: Node, index: number | undefined, parent: Parent | undefined) => string} one + * Serialize one node. + * @property {(node: Parent | undefined) => string} all + * Serialize the children of a parent node. + * @property {Settings} settings + * User configuration. * @property {Schema} schema - * @property {Omission | undefined} omit + * Current schema. * @property {Quote} quote + * Preferred quote. * @property {Quote} alternative - * @property {boolean} smart - * @property {boolean} unquoted - * @property {boolean} tight - * @property {boolean} upperDoctype - * @property {boolean} tightDoctype - * @property {boolean} bogusComments - * @property {boolean} tightLists - * @property {boolean} tightClose - * @property {boolean} collapseEmpty - * @property {boolean} dangerous - * @property {Array} voids - * @property {StringifyEntitiesOptions} entities - * @property {boolean} close - * @property {boolean} closeEmpty + * Alternative quote. */ export {} diff --git a/package.json b/package.json index 385f009..e540506 100644 --- a/package.json +++ b/package.json @@ -35,15 +35,16 @@ ], "dependencies": { "@types/hast": "^2.0.0", + "@types/unist": "^2.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", - "hast-util-is-element": "^2.0.0", + "hast-util-raw": "^7.0.0", "hast-util-whitespace": "^2.0.0", "html-void-elements": "^2.0.0", "property-information": "^6.0.0", "space-separated-tokens": "^2.0.0", - "stringify-entities": "^4.0.2", - "unist-util-is": "^5.0.0" + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" }, "devDependencies": { "@types/tape": "^4.0.0", diff --git a/readme.md b/readme.md index 31af392..8bce15b 100644 --- a/readme.md +++ b/readme.md @@ -18,6 +18,7 @@ * [Use](#use) * [API](#api) * [`toHtml(tree[, options])`](#tohtmltree-options) + * [`CharacterReferences`](#characterreferences) * [Syntax](#syntax) * [Types](#types) * [Compatibility](#compatibility) @@ -115,18 +116,29 @@ Serialize hast ([`Node`][node], `Array`) as HTML. Configuration (optional). -###### `options.entities` +###### `options.space` + +Which space the document is in (`'svg'` or `'html'`, default: `'html'`). + +When an `` element is found in the HTML space, this package already +automatically switches to and from the SVG space when entering and exiting it. + +> 👉 **Note**: hast is not XML. +> It supports SVG as embedded in HTML. +> It does not support the features available in XML. +> Passing SVG might break but fragments of modern SVG should be fine. +> Use [`xast`][xast] if you need to support SVG as XML. -Define how to create character references (`Object`, default: `{}`). -Configuration is passed to [`stringify-entities`][stringify-entities]. -You can use the fields `useNamedReferences`, `useShortestReferences`, and -`omitOptionalSemicolons`. -You cannot use the fields `escapeOnly`, `attribute`, or `subset`). +###### `options.characterReferences` + +Configure how to serialize character references +([`CharacterReferences`][characterreferences], optional). ###### `options.upperDoctype` -Use a `
  • one
  • two
  • `, both `` closing tags can be omitted. The first because it’s followed by another `li`, the last because it’s followed @@ -166,6 +179,7 @@ Not used in the SVG space. Close self-closing nodes with an extra slash (`/`): `` instead of `` (`boolean`, default: `false`). + See `tightSelfClosing` to control whether a space is used before the slash. Not used in the SVG space. @@ -175,6 +189,7 @@ Not used in the SVG space. Close SVG elements without any content with slash (`/`) on the opening tag instead of an end tag: `` instead of `` (`boolean`, default: `false`). + See `tightSelfClosing` to control whether a space is used before the slash. Not used in the HTML space. @@ -239,23 +254,11 @@ Do not encode some characters which cause XSS vulnerabilities in older browsers ###### `options.allowDangerousHtml` -Allow `raw` nodes and insert them as raw HTML. -When falsey, encodes `raw` nodes (`boolean`, default: `false`). - -> ⚠️ **Danger**: only set this if you completely trust the content. - -###### `options.space` - -Which space the document is in (`'svg'` or `'html'`, default: `'html'`). +Allow `raw` nodes and insert them as raw HTML (`boolean`, default: `false`). -When an `` element is found in the HTML space, `rehype-stringify` already -automatically switches to and from the SVG space when entering and exiting it. +When `false`, `Raw` nodes are encoded. -> 👉 **Note**: hast is not XML. -> It supports SVG as embedded in HTML. -> It does not support the features available in XML. -> Passing SVG might break but fragments of modern SVG should be fine. -> Use [`xast`][xast] if you need to support SVG as XML. +> ⚠️ **Danger**: only set this if you completely trust the content. ###### `options.voids` @@ -271,6 +274,34 @@ Not used in the SVG space. Serialized HTML (`string`). +### `CharacterReferences` + +How to serialize character references (TypeScript type). + +##### Fields + +###### `useNamedReferences` + +Prefer named character references (`&`) where possible (`boolean`, default: +`false`). + +###### `useShortestReferences` + +Prefer the shortest possible reference, if that results in less bytes +(`boolean`, default: `false`). + +> ⚠️ **Note**: `useNamedReferences` can be omitted when using +> `useShortestReferences`. + +###### `omitOptionalSemicolons` + +Whether to omit semicolons when possible (`boolean`, default: `false`). + +> ⚠️ **Note**: this creates what HTML calls “parse errors” but is otherwise +> still valid HTML — don’t use this except when building a minifier. +> Omitting semicolons is possible for certain named and numeric references in +> some cases. + ## Syntax HTML is serialized according to WHATWG HTML (the living standard), which is also @@ -369,8 +400,6 @@ abide by its terms. [html-void-elements]: https://github.com/wooorm/html-void-elements -[stringify-entities]: https://github.com/wooorm/stringify-entities - [hast-util-sanitize]: https://github.com/syntax-tree/hast-util-sanitize [hast-util-from-html]: https://github.com/syntax-tree/hast-util-from-html @@ -378,3 +407,5 @@ abide by its terms. [rehype-stringify]: https://github.com/rehypejs/rehype/tree/main/packages/rehype-stringify#readme [xast]: https://github.com/syntax-tree/xast + +[characterreferences]: #characterreferences diff --git a/test/attribute.js b/test/attribute.js index 0c378ef..5eab374 100644 --- a/test/attribute.js +++ b/test/attribute.js @@ -69,7 +69,7 @@ test('`element` attributes', (t) => { st.deepEqual( toHtml( - // @ts-ignore runtime. + // @ts-expect-error runtime. u('element', {tagName: 'i', properties: {unknown: {toString}}}, []) ), '', @@ -241,7 +241,7 @@ test('`element` attributes', (t) => { ) st.deepEqual( - // @ts-ignore runtime. + // @ts-expect-error runtime. toHtml(u('element', {tagName: 'i', properties: {cols: {toString}}}, [])), '', 'should serialize known numbers set to an object' @@ -261,7 +261,7 @@ test('`element` attributes', (t) => { st.deepEqual( toHtml( - // @ts-ignore runtime. + // @ts-expect-error runtime. u('element', {tagName: 'i', properties: {cols: [true, false]}}, []) ), '', @@ -330,7 +330,7 @@ test('`element` attributes', (t) => { st.deepEqual( toHtml( - // @ts-ignore runtime. + // @ts-expect-error runtime. u('element', {tagName: 'i', properties: {className: {toString}}}, []) ), '', @@ -355,7 +355,7 @@ test('`element` attributes', (t) => { st.deepEqual( toHtml( - // @ts-ignore runtime. + // @ts-expect-error runtime. u('element', {tagName: 'i', properties: {className: [true, false]}}, []) ), '', @@ -412,7 +412,7 @@ test('`element` attributes', (t) => { st.deepEqual( toHtml( - // @ts-ignore runtime. + // @ts-expect-error runtime. u('element', {tagName: 'i', properties: {accept: {toString}}}, []) ), '', @@ -435,7 +435,7 @@ test('`element` attributes', (t) => { st.deepEqual( toHtml( - // @ts-ignore runtime. + // @ts-expect-error runtime. u('element', {tagName: 'i', properties: {accept: [true, false]}}, []) ), '', @@ -489,7 +489,7 @@ test('`element` attributes', (t) => { ) st.deepEqual( - // @ts-ignore runtime. + // @ts-expect-error runtime. toHtml(u('element', {tagName: 'i', properties: {id: {toString}}}, [])), '', 'should serialize known normals set to an object' @@ -508,7 +508,7 @@ test('`element` attributes', (t) => { ) st.deepEqual( - // @ts-ignore runtime. + // @ts-expect-error runtime. toHtml(u('element', {tagName: 'i', properties: {id: [true, false]}}, [])), '', 'should serialize known normals set to an array of booleans as a space-separated list' @@ -576,7 +576,7 @@ test('`element` attributes', (t) => { st.deepEqual( toHtml( - // @ts-ignore runtime. + // @ts-expect-error runtime. u('element', {tagName: 'i', properties: {dataId: {toString}}}, []) ), '', @@ -599,7 +599,7 @@ test('`element` attributes', (t) => { st.deepEqual( toHtml( - // @ts-ignore runtime. + // @ts-expect-error runtime. u('element', {tagName: 'i', properties: {dataId: [true, false]}}, []) ), '', @@ -714,7 +714,7 @@ test('`element` attributes', (t) => { st.throws( () => { - // @ts-ignore runtime. + // @ts-expect-error runtime. toHtml(h('img'), {quote: '`'}) }, /Invalid quote ```, expected `'` or `"`/, diff --git a/test/core.js b/test/core.js index 0892694..9085e37 100644 --- a/test/core.js +++ b/test/core.js @@ -6,7 +6,7 @@ import {toHtml} from '../index.js' test('toHtml()', (t) => { t.throws( () => { - // @ts-ignore runtime. + // @ts-expect-error runtime. toHtml(true) }, /Expected node, not `true`/, @@ -15,7 +15,7 @@ test('toHtml()', (t) => { t.throws( () => { - // @ts-ignore runtime. + // @ts-expect-error runtime. toHtml(u('foo', [])) }, /Cannot compile unknown node `foo`/, diff --git a/test/doctype.js b/test/doctype.js index 6fc83a8..05b10c7 100644 --- a/test/doctype.js +++ b/test/doctype.js @@ -4,21 +4,21 @@ import {toHtml} from '../index.js' test('`doctype`', (t) => { t.deepEqual( - // @ts-ignore hast types out of date. + // @ts-expect-error hast types out of date. toHtml(u('doctype')), '', 'should serialize doctypes' ) t.deepEqual( - // @ts-ignore hast types out of date. + // @ts-expect-error hast types out of date. toHtml(u('doctype'), {tightDoctype: true}), '', 'should serialize doctypes tightly in `tightDoctype` mode' ) t.deepEqual( - // @ts-ignore hast types out of date. + // @ts-expect-error hast types out of date. toHtml(u('doctype'), {upperDoctype: true}), '', 'should serialize uppercase doctypes in `upperDoctype` mode' diff --git a/test/raw.js b/test/raw.js index 08c4cc0..4cdaeac 100644 --- a/test/raw.js +++ b/test/raw.js @@ -1,17 +1,19 @@ +/** + * @typedef {import('hast-util-raw')} + */ + import test from 'tape' import {u} from 'unist-builder' import {toHtml} from '../index.js' test('`element`', (t) => { t.deepEqual( - // @ts-ignore nonstandard. toHtml(u('raw', '')), '<script>alert("XSS!")</script>', 'should encode `raw`s' ) t.deepEqual( - // @ts-ignore nonstandard. toHtml(u('raw', ''), { allowDangerousHtml: true }), diff --git a/test/root.js b/test/root.js index 2e052c2..582d544 100644 --- a/test/root.js +++ b/test/root.js @@ -12,7 +12,7 @@ test('`root`', (t) => { 'should serialize `root`s' ) - // @ts-ignore runtime. + // @ts-expect-error runtime. t.deepEqual(toHtml(u('root')), '', 'should serialize `root`s w/o children') t.end() diff --git a/test/svg.js b/test/svg.js index 420810f..e08b52f 100644 --- a/test/svg.js +++ b/test/svg.js @@ -177,7 +177,7 @@ test('svg', (t) => { ) t.deepEqual( - // @ts-ignore runtime. + // @ts-expect-error runtime. toHtml(s('path', {strokeOpacity: {toString}}), {space: 'svg'}), '', 'should serialize known numeric attributes set to non-numeric values' From 4bc33da3f1d580225368d6cc6af48bf3fd7688f5 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Sun, 8 Jan 2023 19:12:20 +0100 Subject: [PATCH 10/13] Use Node test runner --- .github/workflows/main.yml | 2 +- package.json | 3 +- test/attribute.js | 291 +++++++++++++----------------- test/comment.js | 17 +- test/core.js | 19 +- test/doctype.js | 13 +- test/element.js | 31 ++-- test/omission-closing-body.js | 13 +- test/omission-closing-caption.js | 17 +- test/omission-closing-colgroup.js | 15 +- test/omission-closing-dd.js | 17 +- test/omission-closing-dt.js | 17 +- test/omission-closing-head.js | 15 +- test/omission-closing-html.js | 13 +- test/omission-closing-li.js | 15 +- test/omission-closing-menuitem.js | 21 +-- test/omission-closing-optgroup.js | 15 +- test/omission-closing-option.js | 17 +- test/omission-closing-p.js | 27 ++- test/omission-closing-rp-rt.js | 31 ++-- test/omission-closing-tbody.js | 17 +- test/omission-closing-td-th.js | 31 ++-- test/omission-closing-tfoot.js | 13 +- test/omission-closing-thead.js | 17 +- test/omission-closing-tr.js | 13 +- test/omission-opening-body.js | 25 ++- test/omission-opening-colgroup.js | 19 +- test/omission-opening-head.js | 19 +- test/omission-opening-html.js | 13 +- test/omission-opening-tbody.js | 15 +- test/omission.js | 15 +- test/raw.js | 11 +- test/root.js | 17 +- test/security.js | 15 +- test/svg.js | 95 +++++----- test/text.js | 21 ++- 36 files changed, 462 insertions(+), 503 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 89dc06c..ee318ca 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,5 +17,5 @@ jobs: strategy: matrix: node: - - lts/fermium + - lts/hydrogen - node diff --git a/package.json b/package.json index e540506..f59396c 100644 --- a/package.json +++ b/package.json @@ -47,13 +47,12 @@ "zwitch": "^2.0.4" }, "devDependencies": { - "@types/tape": "^4.0.0", + "@types/node": "^18.0.0", "c8": "^7.0.0", "hastscript": "^7.0.0", "prettier": "^2.0.0", "remark-cli": "^11.0.0", "remark-preset-wooorm": "^9.0.0", - "tape": "^5.0.0", "type-coverage": "^2.0.0", "typescript": "^4.0.0", "unist-builder": "^3.0.0", diff --git a/test/attribute.js b/test/attribute.js index 5eab374..2fed373 100644 --- a/test/attribute.js +++ b/test/attribute.js @@ -1,23 +1,24 @@ -import test from 'tape' +import assert from 'node:assert/strict' +import test from 'node:test' import {h} from 'hastscript' import {u} from 'unist-builder' import {toHtml} from '../index.js' -test('`element` attributes', (t) => { - t.test('unknown', (st) => { - st.deepEqual( +test('`element` attributes', async (t) => { + await t.test('unknown', () => { + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {unknown: false}}, [])), '', 'should ignore unknowns set to `false`' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {unknown: null}}, [])), '', 'should ignore unknowns set to `null`' ) - st.deepEqual( + assert.deepEqual( toHtml( u('element', {tagName: 'i', properties: {unknown: undefined}}, []) ), @@ -25,7 +26,7 @@ test('`element` attributes', (t) => { 'should ignore unknowns set to `undefined`' ) - st.deepEqual( + assert.deepEqual( toHtml( u('element', {tagName: 'i', properties: {unknown: Number.NaN}}, []) ), @@ -33,13 +34,13 @@ test('`element` attributes', (t) => { 'should ignore unknowns set to `NaN`' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {unknown: true}}, [])), '', 'should serialize unknowns set to `true` without value' ) - st.deepEqual( + assert.deepEqual( toHtml( u('element', {tagName: 'i', properties: {unknown: 'unknown'}}, []) ), @@ -47,7 +48,7 @@ test('`element` attributes', (t) => { 'should serialize unknowns set to their name as their name' ) - st.deepEqual( + assert.deepEqual( toHtml( u('element', {tagName: 'i', properties: {unknown: ['a', 'b']}}, []) ), @@ -55,19 +56,19 @@ test('`element` attributes', (t) => { 'should serialize unknown lists as space-separated' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {unknown: 1}}, [])), '', 'should serialize unknowns set to an integer as it’s string version' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {unknown: 0}}, [])), '', 'should serialize unknowns set to `0`' ) - st.deepEqual( + assert.deepEqual( toHtml( // @ts-expect-error runtime. u('element', {tagName: 'i', properties: {unknown: {toString}}}, []) @@ -75,24 +76,22 @@ test('`element` attributes', (t) => { '', 'should serialize unknowns set to objects' ) - - st.end() }) - t.test('known booleans', (st) => { - st.deepEqual( + await t.test('known booleans', () => { + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {hidden: false}}, [])), '', 'should ignore known booleans set to `false`' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {hidden: 0}}, [])), '', 'should ignore falsey known booleans' ) - st.deepEqual( + assert.deepEqual( toHtml( u('element', {tagName: 'i', properties: {hidden: Number.NaN}}, []) ), @@ -100,41 +99,39 @@ test('`element` attributes', (t) => { 'should ignore NaN known booleans' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {hidden: true}}, [])), '', 'should serialize known booleans set to `true` without value' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {hidden: 'hidden'}}, [])), '', 'should serialize known booleans set to their name without value' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {hidden: 1}}, [])), '', 'should serialize truthy known booleans without value' ) - - st.end() }) - t.test('known overloaded booleans', (st) => { - st.deepEqual( + await t.test('known overloaded booleans', () => { + assert.deepEqual( toHtml(u('element', {tagName: 'a', properties: {download: false}}, [])), '
    ', 'should ignore known overloaded booleans set to `false`' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'a', properties: {download: 0}}, [])), '', 'should ignore falsey known overloaded booleans' ) - st.deepEqual( + assert.deepEqual( toHtml( u('element', {tagName: 'a', properties: {download: Number.NaN}}, []) ), @@ -142,13 +139,13 @@ test('`element` attributes', (t) => { 'should ignore NaN known overloaded booleans' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'a', properties: {download: true}}, [])), '', 'should serialize known overloaded booleans set to `true` without value' ) - st.deepEqual( + assert.deepEqual( toHtml( u('element', {tagName: 'a', properties: {download: 'download'}}, []) ), @@ -156,110 +153,108 @@ test('`element` attributes', (t) => { 'should serialize known overloaded booleans set to their name without value' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'a', properties: {download: ''}}, [])), '', 'should serialize known overloaded booleans set to an empty string without value' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'a', properties: {download: 1}}, [])), '', 'should serialize truthy known overloaded booleans without value' ) - st.deepEqual( + assert.deepEqual( toHtml( u('element', {tagName: 'a', properties: {download: 'another'}}, []) ), '', 'should serialize known overloaded booleans set to another string' ) - - st.end() }) - t.test('known numbers', (st) => { - st.deepEqual( + await t.test('known numbers', () => { + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {cols: false}}, [])), '', 'should ignore known numbers set to `false`' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'a', properties: {cols: Number.NaN}}, [])), '', 'should ignore NaN known numbers' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {cols: 0}}, [])), '', 'should serialize known numbers set to `0`' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {cols: -1}}, [])), '', 'should serialize known numbers set to `-1`' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {cols: 1}}, [])), '', 'should serialize known numbers set to `1`' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {cols: Math.PI}}, [])), '', 'should serialize known numbers set to `Math.PI`' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {cols: true}}, [])), '', 'should serialize known numbers set to `true` as without value' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {cols: ''}}, [])), '', 'should serialize known numbers set to an empty string' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {cols: 'cols'}}, [])), '', 'should serialize known numbers set to their name' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {cols: 'another'}}, [])), '', 'should serialize known numbers set to a string' ) - st.deepEqual( + assert.deepEqual( // @ts-expect-error runtime. toHtml(u('element', {tagName: 'i', properties: {cols: {toString}}}, [])), '', 'should serialize known numbers set to an object' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {cols: ['a', 'b']}}, [])), '', 'should serialize known numbers set to an array of strings' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {cols: [0, 50]}}, [])), '', 'should serialize known numbers set to an array of numbers' ) - st.deepEqual( + assert.deepEqual( toHtml( // @ts-expect-error runtime. u('element', {tagName: 'i', properties: {cols: [true, false]}}, []) @@ -267,18 +262,16 @@ test('`element` attributes', (t) => { '', 'should serialize known numbers set to an array of booleans' ) - - st.end() }) - t.test('known space-separated lists', (st) => { - st.deepEqual( + await t.test('known space-separated lists', () => { + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {className: false}}, [])), '', 'should ignore known space-separated lists set to `false`' ) - st.deepEqual( + assert.deepEqual( toHtml( u('element', {tagName: 'a', properties: {className: Number.NaN}}, []) ), @@ -286,25 +279,25 @@ test('`element` attributes', (t) => { 'should ignore NaN known space-separated lists' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {className: 0}}, [])), '', 'should serialize known space-separated lists set to `0`' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {className: true}}, [])), '', 'should serialize known space-separated lists set to `true` as without value' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {className: ''}}, [])), '', 'should serialize known space-separated lists set to an empty string' ) - st.deepEqual( + assert.deepEqual( toHtml( u('element', {tagName: 'i', properties: {className: 'class'}}, []) ), @@ -312,7 +305,7 @@ test('`element` attributes', (t) => { 'should serialize known space-separated lists set to their attribute name' ) - st.deepEqual( + assert.deepEqual( toHtml( u('element', {tagName: 'i', properties: {className: 'className'}}, []) ), @@ -320,7 +313,7 @@ test('`element` attributes', (t) => { 'should serialize known space-separated lists set to their property name' ) - st.deepEqual( + assert.deepEqual( toHtml( u('element', {tagName: 'i', properties: {className: 'another'}}, []) ), @@ -328,7 +321,7 @@ test('`element` attributes', (t) => { 'should serialize known space-separated lists set to a string' ) - st.deepEqual( + assert.deepEqual( toHtml( // @ts-expect-error runtime. u('element', {tagName: 'i', properties: {className: {toString}}}, []) @@ -337,7 +330,7 @@ test('`element` attributes', (t) => { 'should serialize known space-separated lists set to an object' ) - st.deepEqual( + assert.deepEqual( toHtml( u('element', {tagName: 'i', properties: {className: ['a', 'b']}}, []) ), @@ -345,7 +338,7 @@ test('`element` attributes', (t) => { 'should serialize known space-separated lists set to an array of strings' ) - st.deepEqual( + assert.deepEqual( toHtml( u('element', {tagName: 'i', properties: {className: [0, 50]}}, []) ), @@ -353,7 +346,7 @@ test('`element` attributes', (t) => { 'should serialize known space-separated lists set to an array of numbers' ) - st.deepEqual( + assert.deepEqual( toHtml( // @ts-expect-error runtime. u('element', {tagName: 'i', properties: {className: [true, false]}}, []) @@ -361,18 +354,16 @@ test('`element` attributes', (t) => { '', 'should serialize known space-separated lists set to an array of booleans' ) - - st.end() }) - t.test('known comma-separated lists', (st) => { - st.deepEqual( + await t.test('known comma-separated lists', () => { + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {accept: false}}, [])), '', 'should ignore known comma-separated lists set to `false`' ) - st.deepEqual( + assert.deepEqual( toHtml( u('element', {tagName: 'a', properties: {accept: Number.NaN}}, []) ), @@ -380,37 +371,37 @@ test('`element` attributes', (t) => { 'should ignore NaN known comma-separated lists' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {accept: 0}}, [])), '', 'should serialize known comma-separated lists set to `0`' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {accept: true}}, [])), '', 'should serialize known comma-separated lists set to `true` as without value' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {accept: ''}}, [])), '', 'should serialize known comma-separated lists set to an empty string' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {accept: 'accept'}}, [])), '', 'should serialize known comma-separated lists set to their name' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {accept: 'another'}}, [])), '', 'should serialize known comma-separated lists set to a string' ) - st.deepEqual( + assert.deepEqual( toHtml( // @ts-expect-error runtime. u('element', {tagName: 'i', properties: {accept: {toString}}}, []) @@ -419,7 +410,7 @@ test('`element` attributes', (t) => { 'should serialize known comma-separated lists set to an object' ) - st.deepEqual( + assert.deepEqual( toHtml( u('element', {tagName: 'i', properties: {accept: ['a', 'b']}}, []) ), @@ -427,13 +418,13 @@ test('`element` attributes', (t) => { 'should serialize known comma-separated lists set to an array of strings' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {accept: [0, 50]}}, [])), '', 'should serialize known comma-separated lists set to an array of numbers' ) - st.deepEqual( + assert.deepEqual( toHtml( // @ts-expect-error runtime. u('element', {tagName: 'i', properties: {accept: [true, false]}}, []) @@ -441,90 +432,86 @@ test('`element` attributes', (t) => { '', 'should serialize known comma-separated lists set to an array of booleans' ) - - st.end() }) - t.test('known normals', (st) => { - st.deepEqual( + await t.test('known normals', () => { + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {id: false}}, [])), '', 'should ignore known normals set to `false`' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {id: Number.NaN}}, [])), '', 'should ignore NaN known normals' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {id: 0}}, [])), '', 'should serialize known normals set to `0`' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {id: true}}, [])), '', 'should serialize known normals set to `true` as without value' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {id: ''}}, [])), '', 'should serialize known normals set to an empty string' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {id: 'id'}}, [])), '', 'should serialize known normals set to their name' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {id: 'another'}}, [])), '', 'should serialize known normals set to a string' ) - st.deepEqual( + assert.deepEqual( // @ts-expect-error runtime. toHtml(u('element', {tagName: 'i', properties: {id: {toString}}}, [])), '', 'should serialize known normals set to an object' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {id: ['a', 'b']}}, [])), '', 'should serialize known normals set to an array of strings as a space-separated list' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {id: [0, 50]}}, [])), '', 'should serialize known normals set to an array of numbers as a space-separated list' ) - st.deepEqual( + assert.deepEqual( // @ts-expect-error runtime. toHtml(u('element', {tagName: 'i', properties: {id: [true, false]}}, [])), '', 'should serialize known normals set to an array of booleans as a space-separated list' ) - - st.end() }) - t.test('data properties', (st) => { - st.deepEqual( + await t.test('data properties', () => { + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {dataId: false}}, [])), '', 'should ignore data properties set to `false`' ) - st.deepEqual( + assert.deepEqual( toHtml( u('element', {tagName: 'i', properties: {dataId: Number.NaN}}, []) ), @@ -532,49 +519,49 @@ test('`element` attributes', (t) => { 'should ignore NaN data properties' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {dataId: 0}}, [])), '', 'should serialize data properties set to `0`' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {dataId: true}}, [])), '', 'should serialize data properties set to `true` as without value' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {dataId: ''}}, [])), '', 'should serialize data properties set to an empty string' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {dataId: 'dataId'}}, [])), '', 'should serialize data properties set to their property name' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {dataId: 'data-id'}}, [])), '', 'should serialize data properties set to their attribute name' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {dataId: 'another'}}, [])), '', 'should serialize data properties set to a string' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {data123: 'a'}}, [])), '', 'should serialize numeric-first data properties set to a string' ) - st.deepEqual( + assert.deepEqual( toHtml( // @ts-expect-error runtime. u('element', {tagName: 'i', properties: {dataId: {toString}}}, []) @@ -583,7 +570,7 @@ test('`element` attributes', (t) => { 'should serialize data properties set to an object' ) - st.deepEqual( + assert.deepEqual( toHtml( u('element', {tagName: 'i', properties: {dataId: ['a', 'b']}}, []) ), @@ -591,13 +578,13 @@ test('`element` attributes', (t) => { 'should serialize data properties set to an array of strings as a space-separated list' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {dataId: [0, 50]}}, [])), '', 'should serialize data properties set to an array of numbers as a space-separated list' ) - st.deepEqual( + assert.deepEqual( toHtml( // @ts-expect-error runtime. u('element', {tagName: 'i', properties: {dataId: [true, false]}}, []) @@ -605,30 +592,26 @@ test('`element` attributes', (t) => { '', 'should serialize data properties set to an array of booleans as a space-separated list' ) - - st.end() }) - t.test('collapseEmptyAttributes', (st) => { - st.deepEqual( + await t.test('collapseEmptyAttributes', () => { + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {id: ''}}, [])), '', 'should show empty string attributes' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {id: ''}}, []), { collapseEmptyAttributes: true }), '', 'should collapse empty string attributes in `collapseEmptyAttributes` mode' ) - - st.end() }) - t.test('tightAttributes', (st) => { - st.deepEqual( + await t.test('tightAttributes', () => { + assert.deepEqual( toHtml( u('element', {tagName: 'i', properties: {title: 'a', id: 'b'}}, []) ), @@ -636,7 +619,7 @@ test('`element` attributes', (t) => { 'should serialize multiple properties' ) - st.deepEqual( + assert.deepEqual( toHtml( u('element', {tagName: 'i', properties: {title: 'a', id: 'b'}}, []), { @@ -646,12 +629,10 @@ test('`element` attributes', (t) => { '', 'should serialize multiple properties tightly in `tightAttributes` mode' ) - - st.end() }) - t.test('tightCommaSeparatedLists', (st) => { - st.deepEqual( + await t.test('tightCommaSeparatedLists', () => { + assert.deepEqual( toHtml( u('element', {tagName: 'i', properties: {accept: ['a', 'b']}}, []) ), @@ -659,7 +640,7 @@ test('`element` attributes', (t) => { 'should serialize comma-separated attributes' ) - st.deepEqual( + assert.deepEqual( toHtml( u('element', {tagName: 'i', properties: {accept: ['a', 'b']}}, []), { @@ -669,18 +650,16 @@ test('`element` attributes', (t) => { '', 'should serialize comma-separated attributes tighly in `tightCommaSeparatedLists` mode' ) - - st.end() }) - t.test('quote', (st) => { - st.deepEqual( + await t.test('quote', () => { + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {title: 'a'}}, [])), '', 'should quote attribute values with double quotes by default' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {title: 'a'}}, []), { quote: "'" }), @@ -688,7 +667,7 @@ test('`element` attributes', (t) => { "should quote attribute values with single quotes if `quote: '\\''`" ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {title: 'a'}}, []), { quote: '"' }), @@ -696,7 +675,7 @@ test('`element` attributes', (t) => { "should quote attribute values with double quotes if `quote: '\\\"'`" ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {title: "'a'"}}, []), { quote: "'" }), @@ -704,7 +683,7 @@ test('`element` attributes', (t) => { "should quote attribute values with single quotes if `quote: '\\''` even if they occur in value" ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {title: '"a"'}}, []), { quote: '"' }), @@ -712,7 +691,7 @@ test('`element` attributes', (t) => { "should quote attribute values with double quotes if `quote: '\\\"'` even if they occur in value" ) - st.throws( + assert.throws( () => { // @ts-expect-error runtime. toHtml(h('img'), {quote: '`'}) @@ -720,12 +699,10 @@ test('`element` attributes', (t) => { /Invalid quote ```, expected `'` or `"`/, 'should throw on invalid quotes' ) - - st.end() }) - t.test('quoteSmart', (st) => { - st.deepEqual( + await t.test('quoteSmart', () => { + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {title: 'a'}}, []), { allowDangerousCharacters: true, quoteSmart: true @@ -734,7 +711,7 @@ test('`element` attributes', (t) => { 'should quote attribute values with primary quotes by default' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {title: "'a'"}}, []), { allowDangerousCharacters: true, quoteSmart: true @@ -743,7 +720,7 @@ test('`element` attributes', (t) => { 'should quote attribute values with primary quotes if the alternative occurs' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {title: "'\"a'"}}, []), { allowDangerousCharacters: true, quoteSmart: true @@ -752,7 +729,7 @@ test('`element` attributes', (t) => { 'should quote attribute values with primary quotes if they occur less than the alternative' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {title: '"a\''}}, []), { allowDangerousCharacters: true, quoteSmart: true @@ -761,7 +738,7 @@ test('`element` attributes', (t) => { 'should quote attribute values with primary quotes if they occur as much as alternatives (#1)' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {title: '"\'a\'"'}}, []), { allowDangerousCharacters: true, quoteSmart: true @@ -770,7 +747,7 @@ test('`element` attributes', (t) => { 'should quote attribute values with primary quotes if they occur as much as alternatives (#1)' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {title: '"a"'}}, []), { allowDangerousCharacters: true, quoteSmart: true @@ -779,7 +756,7 @@ test('`element` attributes', (t) => { 'should quote attribute values with alternative quotes if the primary occurs' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {title: '"\'a"'}}, []), { allowDangerousCharacters: true, quoteSmart: true @@ -787,12 +764,10 @@ test('`element` attributes', (t) => { '', 'should quote attribute values with alternative quotes if they occur less than the primary' ) - - st.end() }) - t.test('preferUnquoted', (st) => { - st.deepEqual( + await t.test('preferUnquoted', () => { + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {id: 'a'}}, []), { preferUnquoted: true }), @@ -800,7 +775,7 @@ test('`element` attributes', (t) => { 'should omit quotes in `preferUnquoted`' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {id: 'a b'}}, []), { preferUnquoted: true }), @@ -808,31 +783,29 @@ test('`element` attributes', (t) => { 'should keep quotes in `preferUnquoted` and impossible' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {id: ''}}, []), { preferUnquoted: true }), '', 'should not add `=` when omitting quotes on empty values' ) - - st.end() }) - t.test('entities in attributes', (st) => { - st.deepEqual( + await t.test('entities in attributes', () => { + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {'3<5\0': 'a'}}, [])), '', 'should encode entities in attribute names' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {title: '3<5\0'}}, [])), '', 'should encode entities in attribute values' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {'3=5\0': 'a'}}, []), { allowParseErrors: true }), @@ -840,7 +813,7 @@ test('`element` attributes', (t) => { 'should not encode characters in attribute names which cause parse errors, but work, in `allowParseErrors` mode' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {title: '3=5\0'}}, []), { allowParseErrors: true }), @@ -848,18 +821,14 @@ test('`element` attributes', (t) => { 'should not encode characters in attribute values which cause parse errors, but work, in `allowParseErrors` mode' ) - st.deepEqual( + assert.deepEqual( toHtml(u('element', {tagName: 'i', properties: {title: "3'5"}}, []), { allowDangerousCharacters: true }), '', 'should not encode characters which cause XSS issues in older browsers, in `allowDangerousCharacters` mode' ) - - st.end() }) - - t.end() }) function toString() { diff --git a/test/comment.js b/test/comment.js index 38310ee..83099d5 100644 --- a/test/comment.js +++ b/test/comment.js @@ -1,27 +1,28 @@ -import test from 'tape' +import assert from 'node:assert/strict' +import test from 'node:test' import {u} from 'unist-builder' import {toHtml} from '../index.js' -test('`comment`', (t) => { - t.deepEqual( +test('`comment`', () => { + assert.deepEqual( toHtml(u('comment', 'alpha')), '', 'should serialize `comment`s' ) - t.deepEqual( + assert.deepEqual( toHtml(u('comment', 'AT&T')), '', 'should not encode `comment`s' ) - t.deepEqual( + assert.deepEqual( toHtml(u('comment', 'asd'), {bogusComments: true}), '', '`bogusComments`: should serialize bogus comments' ) - t.deepEqual( + assert.deepEqual( toHtml(u('comment', 'ad'), {bogusComments: true}), '', '`bogusComments`: should prevent breaking out of bogus comments' @@ -47,7 +48,7 @@ test('`comment`', (t) => { let index = -1 while (++index < matrix.length) { - t.deepEqual( + assert.deepEqual( toHtml(u('comment', matrix[index][0])), '', 'security: should ' + @@ -57,6 +58,4 @@ test('`comment`', (t) => { '`' ) } - - t.end() }) diff --git a/test/core.js b/test/core.js index 9085e37..1dc385e 100644 --- a/test/core.js +++ b/test/core.js @@ -1,10 +1,11 @@ -import test from 'tape' +import assert from 'node:assert/strict' +import test from 'node:test' import {u} from 'unist-builder' import {h} from 'hastscript' import {toHtml} from '../index.js' -test('toHtml()', (t) => { - t.throws( +test('toHtml()', () => { + assert.throws( () => { // @ts-expect-error runtime. toHtml(true) @@ -13,7 +14,7 @@ test('toHtml()', (t) => { 'should throw on non-nodes' ) - t.throws( + assert.throws( () => { // @ts-expect-error runtime. toHtml(u('foo', [])) @@ -22,8 +23,10 @@ test('toHtml()', (t) => { 'should throw on unknown nodes' ) - t.equal(toHtml(h('')), '
    ', 'should support a node') - t.equal(toHtml([h('b'), h('i')]), '', 'should support an array') - - t.end() + assert.equal(toHtml(h('')), '
    ', 'should support a node') + assert.equal( + toHtml([h('b'), h('i')]), + '', + 'should support an array' + ) }) diff --git a/test/doctype.js b/test/doctype.js index 05b10c7..6e26220 100644 --- a/test/doctype.js +++ b/test/doctype.js @@ -1,28 +1,27 @@ -import test from 'tape' +import assert from 'node:assert/strict' +import test from 'node:test' import {u} from 'unist-builder' import {toHtml} from '../index.js' -test('`doctype`', (t) => { - t.deepEqual( +test('`doctype`', () => { + assert.deepEqual( // @ts-expect-error hast types out of date. toHtml(u('doctype')), '', 'should serialize doctypes' ) - t.deepEqual( + assert.deepEqual( // @ts-expect-error hast types out of date. toHtml(u('doctype'), {tightDoctype: true}), '', 'should serialize doctypes tightly in `tightDoctype` mode' ) - t.deepEqual( + assert.deepEqual( // @ts-expect-error hast types out of date. toHtml(u('doctype'), {upperDoctype: true}), '', 'should serialize uppercase doctypes in `upperDoctype` mode' ) - - t.end() }) diff --git a/test/element.js b/test/element.js index 1c35062..00dd8af 100644 --- a/test/element.js +++ b/test/element.js @@ -1,41 +1,46 @@ -import test from 'tape' +import assert from 'node:assert/strict' +import test from 'node:test' import {h} from 'hastscript' import {toHtml} from '../index.js' -test('`element`', (t) => { - t.deepEqual( +test('`element`', () => { + assert.deepEqual( toHtml(h('i', 'bravo')), 'bravo', 'should serialize `element`s' ) - t.deepEqual( + assert.deepEqual( toHtml(h('foo')), '', 'should serialize unknown `element`s' ) - t.deepEqual(toHtml(h('img')), '', 'should serialize void `element`s') + assert.deepEqual( + toHtml(h('img')), + '', + 'should serialize void `element`s' + ) - t.deepEqual( + assert.deepEqual( toHtml(h('foo'), {voids: ['foo']}), '', 'should serialize given void `element`s' ) - t.deepEqual( + assert.deepEqual( toHtml(h('img'), {closeSelfClosing: true}), '', 'should serialize with ` /` in `closeSelfClosing` mode' ) - t.deepEqual( + assert.deepEqual( toHtml(h('img'), {closeSelfClosing: true, tightSelfClosing: true}), '', 'should serialize voids with `/` in `closeSelfClosing` and `tightSelfClosing` mode' ) - t.deepEqual( + assert.deepEqual( toHtml(h('input', {type: 'checkbox'}), { preferUnquoted: true, tightSelfClosing: true, @@ -45,7 +50,7 @@ test('`element`', (t) => { 'should serialize voids with `/` in `closeSelfClosing` and `tightSelfClosing` mode, w/ space after an unquoted attribute (1)' ) - t.deepEqual( + assert.deepEqual( toHtml(h('img', {src: 'index.jpg'}), { preferUnquoted: true, closeSelfClosing: true, @@ -55,7 +60,7 @@ test('`element`', (t) => { 'should serialize voids with `/` in `closeSelfClosing` and `tightSelfClosing` mode, w/ space after an unquoted attribute (2)' ) - t.deepEqual( + assert.deepEqual( toHtml(h('img', {title: '/'}), { preferUnquoted: true, closeSelfClosing: true, @@ -65,7 +70,7 @@ test('`element`', (t) => { 'should serialize voids with a ` /` in if an unquoted attribute ends with `/`' ) - t.deepEqual( + assert.deepEqual( toHtml({ type: 'element', tagName: 'template', @@ -79,6 +84,4 @@ test('`element`', (t) => { '', 'should support `