262 lines
9.6 KiB
JavaScript
262 lines
9.6 KiB
JavaScript
|
/**
|
|||
|
* @typedef {import('hast').Nodes} Nodes
|
|||
|
* @typedef {import('hast').Parents} Parents
|
|||
|
* @typedef {import('hast').RootContent} RootContent
|
|||
|
*
|
|||
|
* @typedef {import('property-information').Schema} Schema
|
|||
|
*
|
|||
|
* @typedef {import('stringify-entities').Options} StringifyEntitiesOptions
|
|||
|
*/
|
|||
|
|
|||
|
/**
|
|||
|
* @typedef {Omit<StringifyEntitiesOptions, 'attribute' | 'escapeOnly' | 'subset'>} CharacterReferences
|
|||
|
*
|
|||
|
* @typedef Options
|
|||
|
* Configuration.
|
|||
|
* @property {boolean | null | undefined} [allowDangerousCharacters=false]
|
|||
|
* Do not encode some characters which cause XSS vulnerabilities in older
|
|||
|
* browsers (default: `false`).
|
|||
|
*
|
|||
|
* > ⚠️ **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 (default: `false`).
|
|||
|
*
|
|||
|
* 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 (default: `false`).
|
|||
|
*
|
|||
|
* 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: `<?charlie>`
|
|||
|
* instead of `<!--charlie-->` (default: `false`).
|
|||
|
*
|
|||
|
* > 👉 **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 (optional).
|
|||
|
* @property {boolean | null | undefined} [closeEmptyElements=false]
|
|||
|
* Close SVG elements without any content with slash (`/`) on the opening tag
|
|||
|
* instead of an end tag: `<circle />` instead of `<circle></circle>`
|
|||
|
* (default: `false`).
|
|||
|
*
|
|||
|
* 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 (`/`): `<img />` instead of
|
|||
|
* `<img>` (default: `false`).
|
|||
|
*
|
|||
|
* 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=""` (default:
|
|||
|
* `false`).
|
|||
|
*
|
|||
|
* Not used in the SVG space.
|
|||
|
*
|
|||
|
* > 👉 **Note**: boolean attributes (such as `hidden`) are always collapsed.
|
|||
|
* @property {boolean | null | undefined} [omitOptionalTags=false]
|
|||
|
* Omit optional opening and closing tags (default: `false`).
|
|||
|
*
|
|||
|
* For example, in `<ol><li>one</li><li>two</li></ol>`, both `</li>` 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 (default: `false`).
|
|||
|
*
|
|||
|
* Not used in the SVG space.
|
|||
|
* @property {Quote | null | undefined} [quote='"']
|
|||
|
* Preferred quote to use (default: `'"'`).
|
|||
|
* @property {boolean | null | undefined} [quoteSmart=false]
|
|||
|
* Use the other quote if that results in less bytes (default: `false`).
|
|||
|
* @property {Space | null | undefined} [space='html']
|
|||
|
* When an `<svg>` element is found in the HTML space, this package already
|
|||
|
* automatically switches to and from the SVG space when entering and exiting
|
|||
|
* it (default: `'html'`).
|
|||
|
*
|
|||
|
* > 👉 **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 (default: `false`).
|
|||
|
*
|
|||
|
* 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) (default: `false`).
|
|||
|
* @property {boolean | null | undefined} [tightDoctype=false]
|
|||
|
* Drop unneeded spaces in doctypes: `<!doctypehtml>` instead of
|
|||
|
* `<!doctype html>` to save bytes (default: `false`).
|
|||
|
*
|
|||
|
* > 👉 **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: `<img/>`
|
|||
|
* instead of `<img />` (default: `false`).
|
|||
|
*
|
|||
|
* > 👉 **Note**: only used if `closeSelfClosing: true` or
|
|||
|
* > `closeEmptyElements: true`.
|
|||
|
* @property {boolean | null | undefined} [upperDoctype=false]
|
|||
|
* Use a `<!DOCTYPE…` instead of `<!doctype…` (default: `false`).
|
|||
|
*
|
|||
|
* Useless except for XHTML.
|
|||
|
* @property {ReadonlyArray<string> | null | undefined} [voids]
|
|||
|
* Tag names of elements to serialize without closing tag (default: `html-void-elements`).
|
|||
|
*
|
|||
|
* Not used in the SVG space.
|
|||
|
*
|
|||
|
* > 👉 **Note**: It’s highly unlikely that you want to pass this, because
|
|||
|
* > hast is not for XML, and HTML will not add more void elements.
|
|||
|
*
|
|||
|
* @typedef {'"' | "'"} Quote
|
|||
|
* HTML quotes for attribute values.
|
|||
|
*
|
|||
|
* @typedef {Omit<Required<{[key in keyof Options]: Exclude<Options[key], null | undefined>}>, 'space' | 'quote'>} Settings
|
|||
|
*
|
|||
|
* @typedef {'html' | 'svg'} Space
|
|||
|
* Namespace.
|
|||
|
*
|
|||
|
* @typedef State
|
|||
|
* Info passed around about the current state.
|
|||
|
* @property {(node: Nodes, index: number | undefined, parent: Parents | undefined) => string} one
|
|||
|
* Serialize one node.
|
|||
|
* @property {(node: Parents | undefined) => string} all
|
|||
|
* Serialize the children of a parent node.
|
|||
|
* @property {Settings} settings
|
|||
|
* User configuration.
|
|||
|
* @property {Schema} schema
|
|||
|
* Current schema.
|
|||
|
* @property {Quote} quote
|
|||
|
* Preferred quote.
|
|||
|
* @property {Quote} alternative
|
|||
|
* Alternative quote.
|
|||
|
*/
|
|||
|
|
|||
|
import {htmlVoidElements} from 'html-void-elements'
|
|||
|
import {html, svg} from 'property-information'
|
|||
|
import {handle} from './handle/index.js'
|
|||
|
|
|||
|
/** @type {Options} */
|
|||
|
const emptyOptions = {}
|
|||
|
|
|||
|
/** @type {CharacterReferences} */
|
|||
|
const emptyCharacterReferences = {}
|
|||
|
|
|||
|
/** @type {Array<never>} */
|
|||
|
const emptyChildren = []
|
|||
|
|
|||
|
/**
|
|||
|
* Serialize hast as HTML.
|
|||
|
*
|
|||
|
* @param {Array<RootContent> | Nodes} tree
|
|||
|
* Tree to serialize.
|
|||
|
* @param {Options | null | undefined} [options]
|
|||
|
* Configuration (optional).
|
|||
|
* @returns {string}
|
|||
|
* Serialized HTML.
|
|||
|
*/
|
|||
|
export function toHtml(tree, options) {
|
|||
|
const options_ = options || emptyOptions
|
|||
|
const quote = options_.quote || '"'
|
|||
|
const alternative = quote === '"' ? "'" : '"'
|
|||
|
|
|||
|
if (quote !== '"' && quote !== "'") {
|
|||
|
throw new Error('Invalid quote `' + quote + '`, expected `\'` or `"`')
|
|||
|
}
|
|||
|
|
|||
|
/** @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 || emptyCharacterReferences,
|
|||
|
closeSelfClosing: options_.closeSelfClosing || false,
|
|||
|
closeEmptyElements: options_.closeEmptyElements || false
|
|||
|
},
|
|||
|
schema: options_.space === 'svg' ? svg : html,
|
|||
|
quote,
|
|||
|
alternative
|
|||
|
}
|
|||
|
|
|||
|
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 {Nodes} node
|
|||
|
* Node to handle.
|
|||
|
* @param {number | undefined} index
|
|||
|
* Index of `node` in `parent.
|
|||
|
* @param {Parents | 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 {Parents | undefined} parent
|
|||
|
* Parent whose children to serialize.
|
|||
|
* @returns {string}
|
|||
|
*/
|
|||
|
export function all(parent) {
|
|||
|
/** @type {Array<string>} */
|
|||
|
const results = []
|
|||
|
const children = (parent && parent.children) || emptyChildren
|
|||
|
let index = -1
|
|||
|
|
|||
|
while (++index < children.length) {
|
|||
|
results[index] = this.one(children[index], index, parent)
|
|||
|
}
|
|||
|
|
|||
|
return results.join('')
|
|||
|
}
|