diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fe284ad..ee318ca 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/hydrogen - node 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 d7d0282..812dca7 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,58 +1,107 @@ /** * @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').Quote} Quote + * @typedef {import('./types.js').State} State */ 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 {Options} [options] + * Serialize hast as HTML. + * + * @param {Node | Array} tree + * Tree to serialize. + * @param {Options | null | undefined} [options] + * Configuration. * @returns {string} + * Serialized HTML. */ -export function toHtml(node, options = {}) { - const quote = options.quote || '"' - /** @type {Quote} */ +// eslint-disable-next-line complexity +export function toHtml(tree, options) { + const options_ = options || {} + const quote = options_.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, - schema: options.space === 'svg' ? svg : html, - omit: options.omitOptionalTags ? omission : undefined, + /** @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, 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, - voids: options.voids || htmlVoidElements.concat(), - entities: options.entities || {}, - close: options.closeSelfClosing, - closeEmpty: options.closeEmptyElements + alternative } - return one( - context, - // @ts-ignore Assume `node` does not contain a root. - Array.isArray(node) ? {type: 'root', children: node} : node, - null, - null + return state.one( + Array.isArray(tree) ? {type: 'root', children: tree} : tree, + 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 be70781..d00fc07 100644 --- a/lib/omission/omission.js +++ b/lib/omission/omission.js @@ -7,8 +7,11 @@ const own = {}.hasOwnProperty /** * Factory to check if a given node can have a tag omitted. * - * @param {Object.} handlers + * @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 b356ec4..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.} */ + /** @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) @@ -82,19 +105,29 @@ function colgroup(node, index, parent) { // Previous colgroup was already omitted. if ( - isElement(previous, 'colgroup') && + parent && + 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) @@ -102,11 +135,14 @@ function tbody(node, index, parent) { // Previous table section was already omitted. if ( - isElement(previous, ['thead', 'tbody']) && + parent && + 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 750dcde..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' @@ -19,14 +19,14 @@ function siblings(increment) { /** * Find applicable siblings in a direction. * - * @param {Parent} parent - * @param {number} index - * @param {boolean} [includeWhitespace=false] - * @returns {Child} + * @param {Parent | null | undefined} parent + * @param {number | null | undefined} index + * @param {boolean | null | undefined} [includeWhitespace=false] + * @returns {Content} */ 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/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 2d7534c..0000000 --- a/lib/tree.js +++ /dev/null @@ -1,276 +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 {Object.} - */ -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`. - * - * @type {Handle} - * @param {Parent} parent - */ -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} props - * @returns {string} - */ -function serializeAttributes(ctx, props) { - /** @type {Array.} */ - const values = [] - 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) - } - } - - while (++index < values.length) { - 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} */ - 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 39ee917..7c20bed 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|null} parent - * @returns {string} + */ + +/** + * @typedef {Content | Root} Node + * @typedef {Extract} Parent + * @typedef {Properties[keyof Properties]} PropertyValue * * @callback OmitHandle - * @param {Element} node - * @param {number|null} index - * @param {Parent|null} 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 {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. + * @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} [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 {CharacterReferences | null | undefined} [characterReferences] + * Configure how to serialize character references. + * @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} [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} [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 {CharacterReferences | null | undefined} [entities] + * Deprecated: please use `characterReferences`. + * @property {boolean | null | undefined} [omitOptionalTags=false] + * Omit optional opening and closing tags. + * + * 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. + * + * Not used in the SVG space. + * @property {boolean | null | undefined} [preferUnquoted=false] + * Leave attributes unquoted if that results in less bytes. + * + * Not used in the SVG space. + * @property {Quote | null | undefined} [quote='"'] + * Preferred quote to use. + * @property {boolean | null | undefined} [quoteSmart=false] + * Use the other quote if that results in less bytes. + * @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 {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} [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} [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} [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} [upperDoctype=false] + * Use a ` | 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. + * + * @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} 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 9775479..bc96da5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hast-util-to-html", - "version": "8.0.3", + "version": "8.0.4", "description": "hast utility to serialize to HTML", "license": "MIT", "keywords": [ @@ -35,36 +35,35 @@ ], "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", + "@types/node": "^18.0.0", "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.46.0" + "xo": "^0.53.0" }, "scripts": { "prepack": "npm run build && npm run format", - "build": "rimraf \"{lib/omission/util/**,lib/omission/**,lib/**,test/**,}*.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-coverage": "c8 --check-coverage --branches 100 --functions 100 --lines 100 --statements 100 --reporter lcov 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" }, "prettier": { @@ -80,7 +79,11 @@ }, "remarkConfig": { "plugins": [ - "preset-wooorm" + "preset-wooorm", + [ + "remark-lint-no-html", + false + ] ] }, "typeCoverage": { diff --git a/readme.md b/readme.md index 0eea1e0..6141e32 100644 --- a/readme.md +++ b/readme.md @@ -8,21 +8,83 @@ [![Backers][backers-badge]][collective] [![Chat][chat-badge]][chat] -[**hast**][hast] utility to serialize to HTML. +[hast][] utility to serialize hast as HTML. + +## 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) + * [`CharacterReferences`](#characterreferences) + * [`Options`](#options) + * [`Quote`](#quote-1) + * [`Space`](#space-1) +* [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 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 (such as +formatters) or ugly (such as minifiers). + +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 +also serialize HTML at a higher-level (easier) abstraction. ## Install -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. - -[npm][]: +This package is [ESM only][esm]. +In Node.js (version 14.14+ and 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,162 +107,265 @@ Yields: ## API -This package exports the following identifiers: `toHtml`. +This package exports the identifier [`toHtml`][tohtml]. There is no default export. ### `toHtml(tree[, options])` -Serialize the given [**hast**][hast] [*tree*][tree] (or list of nodes). +Serialize hast as HTML. -###### `options.space` +###### Parameters -Whether the [*root*][root] of the [*tree*][tree] is in the `'html'` or `'svg'` -space (enum, `'svg'` or `'html'`, default: `'html'`). +* `tree` ([`Node`][node] or `Array`) + — tree to serialize +* `options` ([`Options`][options], optional) + — configuration -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. +###### Returns -###### `options.entities` +Serialized HTML (`string`). -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. +### `CharacterReferences` -###### `options.voids` +How to serialize character references (TypeScript type). -Tag names of [*elements*][element] to serialize without closing tag -(`Array.`, default: [`html-void-elements`][html-void-elements]). +##### Fields -Not used in the SVG space. +###### `useNamedReferences` + +Prefer named character references (`&`) where possible (`boolean`, default: +`false`). -###### `options.upperDoctype` +###### `useShortestReferences` -Use a ` ⚠️ **Note**: `useNamedReferences` can be omitted when using +> `useShortestReferences`. -Preferred quote to use (`'"'` or `'\''`, default: `'"'`). +###### `omitOptionalSemicolons` -###### `options.quoteSmart` +Whether to omit semicolons when possible (`boolean`, default: `false`). -Use the other quote if that results in less bytes (`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. -###### `options.preferUnquoted` +### `Options` -Leave attributes unquoted if that results in less bytes (`boolean`, default: -`false`). +Configuration (TypeScript type). -Not used in the SVG space. +##### Fields -###### `options.omitOptionalTags` +###### `allowDangerousCharacters` -Omit optional opening and closing tags (`boolean`, default: `false`). -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. +Do not encode some characters which cause XSS vulnerabilities in older browsers +(`boolean`, default: `false`). + +> ⚠️ **Danger**: only set this if you completely trust the content. + +###### `allowDangerousHtml` + +Allow `raw` nodes and insert them as raw HTML (`boolean`, default: `false`). + +When `false`, `Raw` nodes are encoded. + +> ⚠️ **Danger**: only set this if you completely trust the content. + +###### `allowParseErrors` + +Do not encode characters which cause parse errors (even though they work), to +save bytes (`boolean`, default: `false`). Not used in the SVG space. -###### `options.collapseEmptyAttributes` +> 👉 **Note**: intentionally creates parse errors in markup (how parse errors +> are handled is well defined, so this works but isn’t pretty). -Collapse empty attributes: get `class` instead of `class=""` (`boolean`, +###### `bogusComments` + +Use “bogus comments” instead of comments to save byes: `` instead of +`` (`boolean`, default: `false`). + +> 👉 **Note**: intentionally creates parse errors in markup (how parse errors +> are handled is well defined, so this works but isn’t pretty). + +###### `characterReferences` + +Configure how to serialize character references +([`CharacterReferences`][characterreferences], optional). + +###### `closeEmptyElements` + +Close SVG elements without any content with slash (`/`) on the opening tag +instead of an end tag: `` instead of `` (`boolean`, default: `false`). -**Note**: boolean attributes, such as `hidden`, are always collapsed. -Not used in the SVG space. +See `tightSelfClosing` to control whether a space is used before the slash. + +Not used in the HTML space. -###### `options.closeSelfClosing` +###### `closeSelfClosing` 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. -###### `options.closeEmptyElements` +###### `collapseEmptyAttributes` -Close SVG elements without any content with slash (`/`) on the opening tag -instead of an end tag: `` instead of `` (`boolean`, +Collapse empty attributes: get `class` instead of `class=""` (`boolean`, default: `false`). -See `tightSelfClosing` to control whether a space is used before the slash. -Not used in the HTML space. +Not used in the SVG space. -###### `options.tightSelfClosing` +> 👉 **Note**: boolean attributes (such as `hidden`) are always collapsed. -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`. +###### `omitOptionalTags` -###### `options.tightCommaSeparatedLists` +Omit optional opening and closing tags (`boolean`, default: `false`). -Join known comma-separated attribute values with just a comma (`,`), instead of -padding them on the right as well (`,·`, where `·` represents a space) -(`boolean`, default: `false`). +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. + +Not used in the SVG space. + +###### `preferUnquoted` + +Leave attributes unquoted if that results in less bytes (`boolean`, default: +`false`). + +Not used in the SVG space. + +###### `quote` + +Preferred quote to use ([`Quote`][quote], default: `'"'`). + +###### `quoteSmart` + +Use the other quote if that results in less bytes (`boolean`, default: `false`). + +###### `space` -###### `options.tightAttributes` +Which space the document is in ([`Space`][space], 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. + +###### `tightAttributes` 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. -###### `options.tightDoctype` +> 👉 **Note**: intentionally creates parse errors in markup (how parse errors +> are handled is well defined, so this works but isn’t pretty). + +###### `tightCommaSeparatedLists` + +Join known comma-separated attribute values with just a comma (`,`), instead of +padding them on the right as well (`,␠`, where `␠` represents a space) +(`boolean`, default: `false`). + +###### `tightDoctype` Drop unneeded spaces in doctypes: `` instead of `` to save bytes (`boolean`, default: `false`). -**Note**: creates invalid (but working) markup. -###### `options.bogusComments` +> 👉 **Note**: intentionally creates parse errors in markup (how parse errors +> are handled is well defined, so this works but isn’t pretty). -Use “bogus comments” instead of comments to save byes: `` instead of -`` (`boolean`, default: `false`). -**Note**: creates invalid (but working) markup. +###### `tightSelfClosing` + +Do not use an extra space when closing self-closing elements: `` instead +of `` (`boolean`, default: `false`). -###### `options.allowParseErrors` +> 👉 **Note**: only used if `closeSelfClosing: true` or +> `closeEmptyElements: true`. -Do not encode characters which cause parse errors (even though they work), to -save bytes (`boolean`, default: `false`). -**Note**: creates invalid (but working) markup. +###### `upperDoctype` + +Use a ``, +default: [`html-void-elements`][html-void-elements]). Not used in the SVG space. -###### `options.allowDangerousCharacters` +> 👉 **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. -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. +### `Quote` + +HTML quotes for attribute values (TypeScript type). + +###### Type + +```ts +type Quote = '"' | "'" +``` + +### `Space` + +Namespace (TypeScript type). + +###### Type + +```ts +type Space = 'html' | 'svg' +``` + +## Syntax + +HTML is serialized according to WHATWG HTML (the living standard), which is also +followed by browsers such as Chrome and Firefox. -###### `options.allowDangerousHtml` +## Types -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. +This package is fully typed with [TypeScript][]. +It exports the additional types [`CharacterReferences`][characterreferences], +[`Options`][options], [`Quote`][quote], and [`Space`][space]. + +## Compatibility + +Projects maintained by the unified collective are compatible with all maintained +versions of Node.js. +As of now, that is Node.js 14.14+ and 16.0+. +Our projects sometimes work with older versions, but this is not guaranteed. ## 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 +406,46 @@ 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 +[hast-util-sanitize]: https://github.com/syntax-tree/hast-util-sanitize -[tree]: https://github.com/syntax-tree/unist#tree +[hast-util-from-html]: https://github.com/syntax-tree/hast-util-from-html -[root]: https://github.com/syntax-tree/unist#root +[rehype-stringify]: https://github.com/rehypejs/rehype/tree/main/packages/rehype-stringify#readme -[hast]: https://github.com/syntax-tree/hast +[xast]: https://github.com/syntax-tree/xast -[element]: https://github.com/syntax-tree/hast#element +[tohtml]: #tohtmltree-options -[xss]: https://en.wikipedia.org/wiki/Cross-site_scripting +[characterreferences]: #characterreferences + +[options]: #options + +[space]: #space -[sanitize]: https://github.com/syntax-tree/hast-util-sanitize +[quote]: #quote diff --git a/test/attribute.js b/test/attribute.js index 0c378ef..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,44 +56,42 @@ 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-ignore runtime. + // @ts-expect-error runtime. u('element', {tagName: 'i', properties: {unknown: {toString}}}, []) ), '', '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,129 +153,125 @@ 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( - // @ts-ignore runtime. + 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-ignore runtime. + // @ts-expect-error runtime. u('element', {tagName: 'i', properties: {cols: [true, false]}}, []) ), '', '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,16 +321,16 @@ test('`element` attributes', (t) => { 'should serialize known space-separated lists set to a string' ) - st.deepEqual( + assert.deepEqual( toHtml( - // @ts-ignore runtime. + // @ts-expect-error runtime. u('element', {tagName: 'i', properties: {className: {toString}}}, []) ), '', '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,26 +346,24 @@ test('`element` attributes', (t) => { 'should serialize known space-separated lists set to an array of numbers' ) - st.deepEqual( + assert.deepEqual( toHtml( - // @ts-ignore runtime. + // @ts-expect-error runtime. u('element', {tagName: 'i', properties: {className: [true, false]}}, []) ), '', '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,46 +371,46 @@ 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-ignore runtime. + // @ts-expect-error runtime. u('element', {tagName: 'i', properties: {accept: {toString}}}, []) ), '', 'should serialize known comma-separated lists set to an object' ) - st.deepEqual( + assert.deepEqual( toHtml( u('element', {tagName: 'i', properties: {accept: ['a', 'b']}}, []) ), @@ -427,104 +418,100 @@ 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-ignore runtime. + // @ts-expect-error runtime. u('element', {tagName: 'i', properties: {accept: [true, false]}}, []) ), '', '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( - // @ts-ignore runtime. + 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( - // @ts-ignore runtime. + 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,58 +519,58 @@ 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-ignore runtime. + // @ts-expect-error runtime. u('element', {tagName: 'i', properties: {dataId: {toString}}}, []) ), '', 'should serialize data properties set to an object' ) - st.deepEqual( + assert.deepEqual( toHtml( u('element', {tagName: 'i', properties: {dataId: ['a', 'b']}}, []) ), @@ -591,44 +578,40 @@ 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-ignore runtime. + // @ts-expect-error runtime. u('element', {tagName: 'i', properties: {dataId: [true, false]}}, []) ), '', '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,20 +691,18 @@ test('`element` attributes', (t) => { "should quote attribute values with double quotes if `quote: '\\\"'` even if they occur in value" ) - st.throws( + assert.throws( () => { - // @ts-ignore runtime. + // @ts-expect-error runtime. toHtml(h('img'), {quote: '`'}) }, /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 0892694..f40a85e 100644 --- a/test/core.js +++ b/test/core.js @@ -1,29 +1,39 @@ -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' +import * as mod from '../index.js' -test('toHtml()', (t) => { - t.throws( +test('toHtml()', () => { + assert.deepEqual( + Object.keys(mod).sort(), + ['toHtml'], + 'should expose the public api' + ) + + assert.throws( () => { - // @ts-ignore runtime. + // @ts-expect-error runtime. toHtml(true) }, /Expected node, not `true`/, 'should throw on non-nodes' ) - t.throws( + assert.throws( () => { - // @ts-ignore runtime. + // @ts-expect-error runtime. toHtml(u('foo', [])) }, /Cannot compile unknown node `foo`/, '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 6fc83a8..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( - // @ts-ignore hast types out of date. +test('`doctype`', () => { + assert.deepEqual( + // @ts-expect-error hast types out of date. toHtml(u('doctype')), '', 'should serialize doctypes' ) - t.deepEqual( - // @ts-ignore hast types out of date. + assert.deepEqual( + // @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. + 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 `