UNPKG

@blankeos/velite

Version:

Turns Markdown / MDX, YAML, JSON, or other files into app's data layer with type-safe schema.

2,052 lines (1,886 loc) 275 kB
import { B as BRAND, D as DIRTY, E as EMPTY_PATH, I as INVALID, N as NEVER, O as OK, P as ParseStatus, Z as ZodType, a as ZodAny, b as ZodArray, c as ZodBigInt, d as ZodBoolean, e as ZodBranded, f as ZodCatch, g as ZodDate, h as ZodDefault, i as ZodDiscriminatedUnion, j as ZodEffects, k as ZodEnum, l as ZodError, m as ZodFirstPartyTypeKind, n as ZodFunction, o as ZodIntersection, p as ZodIssueCode, q as ZodLazy, r as ZodLiteral, s as ZodMap, t as ZodNaN, u as ZodNativeEnum, v as ZodNever, w as ZodNull, x as ZodNullable, y as ZodNumber, z as ZodObject, A as ZodOptional, C as ZodParsedType, F as ZodPipeline, G as ZodPromise, H as ZodReadonly, J as ZodRecord, K as ZodSet, L as ZodString, M as ZodSymbol, Q as ZodTuple, R as ZodUndefined, S as ZodUnion, T as ZodUnknown, U as ZodVoid, V as addIssueToContext, W as anyType, X as arrayType, Y as bigIntType, _ as booleanType, $ as coerce, a0 as custom, a1 as dateType, a2 as datetimeRegex, a3 as errorMap, a4 as discriminatedUnionType, a5 as effectsType, a6 as enumType, a7 as functionType, a8 as getErrorMap, a9 as getParsedType, aa as instanceOfType, ab as intersectionType, ac as isAborted, ad as isAsync, ae as isDirty, af as isValid, ag as late, ah as lazyType, ai as literalType, aj as makeIssue, ak as mapType, al as nanType, am as nativeEnumType, an as neverType, ao as nullType, ap as nullableType, aq as numberType, ar as objectType, as as objectUtil, at as oboolean, au as onumber, av as optionalType, aw as ostring, ax as pipelineType, ay as preprocessType, az as promiseType, aA as quotelessJson, aB as recordType, aC as setType, aD as setErrorMap, aE as strictObjectType, aF as stringType, aG as symbolType, aH as tupleType, aI as undefinedType, aJ as unionType, aK as unknownType, aL as util, aM as voidType, aN as isRelativePath, aO as processAsset, aP as getImageMetadata, aQ as raw$2, aR as svg, aS as find, aT as stringify, aU as stringify$1, aV as zwitch, aW as html$3, aX as htmlVoidElements, aY as convert, aZ as visitParents, a_ as ok, a$ as unicodeWhitespace, b0 as unicodePunctuation, b1 as normalizeIdentifier, b2 as classifyCharacter, b3 as visit, b4 as toString, b5 as EXIT, b6 as asciiAlphanumeric, b7 as asciiAlpha, b8 as markdownLineEndingOrSpace, b9 as asciiControl, ba as blankLine, bb as factorySpace, bc as splice, bd as resolveAll, be as markdownLineEnding, bf as markdownSpace, bg as combineExtensions, bh as fromMarkdown, bi as toHast, bj as getDefaultExportFromCjs, bk as VFile, bl as rehypeCopyLinkedFiles, bm as remarkCopyLinkedFiles, bn as structuredClone } from './velite-oUlu1YbQ.js'; export { bv as VeliteFile, bp as assets, bq as build, br as defineCollection, bu as defineConfig, bs as defineLoader, bt as defineSchema, bo as logger } from './velite-oUlu1YbQ.js'; import { readFile } from 'node:fs/promises'; import { join, relative } from 'node:path'; import 'os'; import 'path'; import 'util'; import 'stream'; import 'events'; import 'fs'; import 'node:crypto'; import 'node:url'; import 'esbuild'; var z = /*#__PURE__*/Object.freeze({ __proto__: null, BRAND: BRAND, DIRTY: DIRTY, EMPTY_PATH: EMPTY_PATH, INVALID: INVALID, NEVER: NEVER, OK: OK, ParseStatus: ParseStatus, Schema: ZodType, ZodAny: ZodAny, ZodArray: ZodArray, ZodBigInt: ZodBigInt, ZodBoolean: ZodBoolean, ZodBranded: ZodBranded, ZodCatch: ZodCatch, ZodDate: ZodDate, ZodDefault: ZodDefault, ZodDiscriminatedUnion: ZodDiscriminatedUnion, ZodEffects: ZodEffects, ZodEnum: ZodEnum, ZodError: ZodError, ZodFirstPartyTypeKind: ZodFirstPartyTypeKind, ZodFunction: ZodFunction, ZodIntersection: ZodIntersection, ZodIssueCode: ZodIssueCode, ZodLazy: ZodLazy, ZodLiteral: ZodLiteral, ZodMap: ZodMap, ZodNaN: ZodNaN, ZodNativeEnum: ZodNativeEnum, ZodNever: ZodNever, ZodNull: ZodNull, ZodNullable: ZodNullable, ZodNumber: ZodNumber, ZodObject: ZodObject, ZodOptional: ZodOptional, ZodParsedType: ZodParsedType, ZodPipeline: ZodPipeline, ZodPromise: ZodPromise, ZodReadonly: ZodReadonly, ZodRecord: ZodRecord, ZodSchema: ZodType, ZodSet: ZodSet, ZodString: ZodString, ZodSymbol: ZodSymbol, ZodTransformer: ZodEffects, ZodTuple: ZodTuple, ZodType: ZodType, ZodUndefined: ZodUndefined, ZodUnion: ZodUnion, ZodUnknown: ZodUnknown, ZodVoid: ZodVoid, addIssueToContext: addIssueToContext, any: anyType, array: arrayType, bigint: bigIntType, boolean: booleanType, coerce: coerce, custom: custom, date: dateType, datetimeRegex: datetimeRegex, defaultErrorMap: errorMap, discriminatedUnion: discriminatedUnionType, effect: effectsType, enum: enumType, function: functionType, getErrorMap: getErrorMap, getParsedType: getParsedType, instanceof: instanceOfType, intersection: intersectionType, isAborted: isAborted, isAsync: isAsync, isDirty: isDirty, isValid: isValid, late: late, lazy: lazyType, literal: literalType, makeIssue: makeIssue, map: mapType, nan: nanType, nativeEnum: nativeEnumType, never: neverType, null: nullType, nullable: nullableType, number: numberType, object: objectType, get objectUtil () { return objectUtil; }, oboolean: oboolean, onumber: onumber, optional: optionalType, ostring: ostring, pipeline: pipelineType, preprocess: preprocessType, promise: promiseType, quotelessJson: quotelessJson, record: recordType, set: setType, setErrorMap: setErrorMap, strictObject: strictObjectType, string: stringType, symbol: symbolType, transformer: effectsType, tuple: tupleType, undefined: undefinedType, union: unionType, unknown: unknownType, get util () { return util; }, void: voidType }); const excerpt = ({ length = 260 } = {}) => custom((i) => i === void 0 || typeof i === "string").transform(async (value, { meta, addIssue }) => { value = value ?? meta.plain; if (value == null || value.length === 0) { addIssue({ code: "custom", message: "The content is empty" }); return ""; } return value.slice(0, length); }); const file = ({ allowNonRelativePath = true } = {}) => stringType().transform(async (value, { meta, addIssue }) => { try { if (allowNonRelativePath && !isRelativePath(value)) return value; const { output } = meta.config; return await processAsset(value, meta.path, output.name, output.base); } catch (err) { const message = err instanceof Error ? err.message : String(err); addIssue({ fatal: true, code: "custom", message }); return null; } }); const image$1 = ({ absoluteRoot } = {}) => stringType().transform(async (value, { meta, addIssue }) => { try { if (absoluteRoot && /^\//.test(value)) { const buffer = await readFile(join(absoluteRoot, value)); const metadata = await getImageMetadata(buffer); if (metadata == null) throw new Error(`Failed to get image metadata: ${value}`); return { src: value, ...metadata }; } const { output } = meta.config; return await processAsset(value, meta.path, output.name, output.base, true); } catch (err) { const message = err instanceof Error ? err.message : String(err); addIssue({ fatal: true, code: "custom", message }); return null; } }); const isodate = () => stringType().refine((value) => !isNaN(Date.parse(value)), "Invalid date string").transform((value) => new Date(value).toISOString()); /** * @typedef {import('hast').Root} Root * @typedef {import('hast-util-raw').Options} RawOptions * @typedef {import('vfile').VFile} VFile */ /** * Parse the tree (and raw nodes) again, keeping positional info okay. * * @param {Options | null | undefined} [options] * Configuration (optional). * @returns * Transform. */ function rehypeRaw(options) { /** * @param {Root} tree * Tree. * @param {VFile} file * File. * @returns {Root} * New tree. */ return function (tree, file) { // Assume root in -> root out. const result = /** @type {Root} */ (raw$2(tree, {...options, file})); return result } } /** * @typedef CoreOptions * @property {ReadonlyArray<string>} [subset=[]] * Whether to only escape the given subset of characters. * @property {boolean} [escapeOnly=false] * Whether to only escape possibly dangerous characters. * Those characters are `"`, `&`, `'`, `<`, `>`, and `` ` ``. * * @typedef FormatOptions * @property {(code: number, next: number, options: CoreWithFormatOptions) => string} format * Format strategy. * * @typedef {CoreOptions & FormatOptions & import('./util/format-smart.js').FormatSmartOptions} CoreWithFormatOptions */ const defaultSubsetRegex = /["&'<>`]/g; const surrogatePairsRegex = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g; const controlCharactersRegex = // eslint-disable-next-line no-control-regex, unicorn/no-hex-escape /[\x01-\t\v\f\x0E-\x1F\x7F\x81\x8D\x8F\x90\x9D\xA0-\uFFFF]/g; const regexEscapeRegex = /[|\\{}()[\]^$+*?.]/g; /** @type {WeakMap<ReadonlyArray<string>, RegExp>} */ const subsetToRegexCache = new WeakMap(); /** * Encode certain characters in `value`. * * @param {string} value * @param {CoreWithFormatOptions} options * @returns {string} */ function core(value, options) { value = value.replace( options.subset ? charactersToExpressionCached(options.subset) : defaultSubsetRegex, basic ); if (options.subset || options.escapeOnly) { return value } return ( value // Surrogate pairs. .replace(surrogatePairsRegex, surrogate) // BMP control characters (C0 except for LF, CR, SP; DEL; and some more // non-ASCII ones). .replace(controlCharactersRegex, basic) ) /** * @param {string} pair * @param {number} index * @param {string} all */ function surrogate(pair, index, all) { return options.format( (pair.charCodeAt(0) - 0xd800) * 0x400 + pair.charCodeAt(1) - 0xdc00 + 0x10000, all.charCodeAt(index + 2), options ) } /** * @param {string} character * @param {number} index * @param {string} all */ function basic(character, index, all) { return options.format( character.charCodeAt(0), all.charCodeAt(index + 1), options ) } } /** * A wrapper function that caches the result of `charactersToExpression` with a WeakMap. * This can improve performance when tooling calls `charactersToExpression` repeatedly * with the same subset. * * @param {ReadonlyArray<string>} subset * @returns {RegExp} */ function charactersToExpressionCached(subset) { let cached = subsetToRegexCache.get(subset); if (!cached) { cached = charactersToExpression(subset); subsetToRegexCache.set(subset, cached); } return cached } /** * @param {ReadonlyArray<string>} subset * @returns {RegExp} */ function charactersToExpression(subset) { /** @type {Array<string>} */ const groups = []; let index = -1; while (++index < subset.length) { groups.push(subset[index].replace(regexEscapeRegex, '\\$&')); } return new RegExp('(?:' + groups.join('|') + ')', 'g') } const hexadecimalRegex = /[\dA-Fa-f]/; /** * Configurable ways to encode characters as hexadecimal references. * * @param {number} code * @param {number} next * @param {boolean|undefined} omit * @returns {string} */ function toHexadecimal(code, next, omit) { const value = '&#x' + code.toString(16).toUpperCase(); return omit && next && !hexadecimalRegex.test(String.fromCharCode(next)) ? value : value + ';' } const decimalRegex = /\d/; /** * Configurable ways to encode characters as decimal references. * * @param {number} code * @param {number} next * @param {boolean|undefined} omit * @returns {string} */ function toDecimal(code, next, omit) { const value = '&#' + String(code); return omit && next && !decimalRegex.test(String.fromCharCode(next)) ? value : value + ';' } /** * List of legacy HTML named character references that don’t need a trailing semicolon. * * @type {Array<string>} */ const characterEntitiesLegacy = [ 'AElig', 'AMP', 'Aacute', 'Acirc', 'Agrave', 'Aring', 'Atilde', 'Auml', 'COPY', 'Ccedil', 'ETH', 'Eacute', 'Ecirc', 'Egrave', 'Euml', 'GT', 'Iacute', 'Icirc', 'Igrave', 'Iuml', 'LT', 'Ntilde', 'Oacute', 'Ocirc', 'Ograve', 'Oslash', 'Otilde', 'Ouml', 'QUOT', 'REG', 'THORN', 'Uacute', 'Ucirc', 'Ugrave', 'Uuml', 'Yacute', 'aacute', 'acirc', 'acute', 'aelig', 'agrave', 'amp', 'aring', 'atilde', 'auml', 'brvbar', 'ccedil', 'cedil', 'cent', 'copy', 'curren', 'deg', 'divide', 'eacute', 'ecirc', 'egrave', 'eth', 'euml', 'frac12', 'frac14', 'frac34', 'gt', 'iacute', 'icirc', 'iexcl', 'igrave', 'iquest', 'iuml', 'laquo', 'lt', 'macr', 'micro', 'middot', 'nbsp', 'not', 'ntilde', 'oacute', 'ocirc', 'ograve', 'ordf', 'ordm', 'oslash', 'otilde', 'ouml', 'para', 'plusmn', 'pound', 'quot', 'raquo', 'reg', 'sect', 'shy', 'sup1', 'sup2', 'sup3', 'szlig', 'thorn', 'times', 'uacute', 'ucirc', 'ugrave', 'uml', 'uuml', 'yacute', 'yen', 'yuml' ]; /** * Map of named character references from HTML 4. * * @type {Record<string, string>} */ const characterEntitiesHtml4 = { nbsp: ' ', iexcl: '¡', cent: '¢', pound: '£', curren: '¤', yen: '¥', brvbar: '¦', sect: '§', uml: '¨', copy: '©', ordf: 'ª', laquo: '«', not: '¬', shy: '­', reg: '®', macr: '¯', deg: '°', plusmn: '±', sup2: '²', sup3: '³', acute: '´', micro: 'µ', para: '¶', middot: '·', cedil: '¸', sup1: '¹', ordm: 'º', raquo: '»', frac14: '¼', frac12: '½', frac34: '¾', iquest: '¿', Agrave: 'À', Aacute: 'Á', Acirc: 'Â', Atilde: 'Ã', Auml: 'Ä', Aring: 'Å', AElig: 'Æ', Ccedil: 'Ç', Egrave: 'È', Eacute: 'É', Ecirc: 'Ê', Euml: 'Ë', Igrave: 'Ì', Iacute: 'Í', Icirc: 'Î', Iuml: 'Ï', ETH: 'Ð', Ntilde: 'Ñ', Ograve: 'Ò', Oacute: 'Ó', Ocirc: 'Ô', Otilde: 'Õ', Ouml: 'Ö', times: '×', Oslash: 'Ø', Ugrave: 'Ù', Uacute: 'Ú', Ucirc: 'Û', Uuml: 'Ü', Yacute: 'Ý', THORN: 'Þ', szlig: 'ß', agrave: 'à', aacute: 'á', acirc: 'â', atilde: 'ã', auml: 'ä', aring: 'å', aelig: 'æ', ccedil: 'ç', egrave: 'è', eacute: 'é', ecirc: 'ê', euml: 'ë', igrave: 'ì', iacute: 'í', icirc: 'î', iuml: 'ï', eth: 'ð', ntilde: 'ñ', ograve: 'ò', oacute: 'ó', ocirc: 'ô', otilde: 'õ', ouml: 'ö', divide: '÷', oslash: 'ø', ugrave: 'ù', uacute: 'ú', ucirc: 'û', uuml: 'ü', yacute: 'ý', thorn: 'þ', yuml: 'ÿ', fnof: 'ƒ', Alpha: 'Α', Beta: 'Β', Gamma: 'Γ', Delta: 'Δ', Epsilon: 'Ε', Zeta: 'Ζ', Eta: 'Η', Theta: 'Θ', Iota: 'Ι', Kappa: 'Κ', Lambda: 'Λ', Mu: 'Μ', Nu: 'Ν', Xi: 'Ξ', Omicron: 'Ο', Pi: 'Π', Rho: 'Ρ', Sigma: 'Σ', Tau: 'Τ', Upsilon: 'Υ', Phi: 'Φ', Chi: 'Χ', Psi: 'Ψ', Omega: 'Ω', alpha: 'α', beta: 'β', gamma: 'γ', delta: 'δ', epsilon: 'ε', zeta: 'ζ', eta: 'η', theta: 'θ', iota: 'ι', kappa: 'κ', lambda: 'λ', mu: 'μ', nu: 'ν', xi: 'ξ', omicron: 'ο', pi: 'π', rho: 'ρ', sigmaf: 'ς', sigma: 'σ', tau: 'τ', upsilon: 'υ', phi: 'φ', chi: 'χ', psi: 'ψ', omega: 'ω', thetasym: 'ϑ', upsih: 'ϒ', piv: 'ϖ', bull: '•', hellip: '…', prime: '′', Prime: '″', oline: '‾', frasl: '⁄', weierp: '℘', image: 'ℑ', real: 'ℜ', trade: '™', alefsym: 'ℵ', larr: '←', uarr: '↑', rarr: '→', darr: '↓', harr: '↔', crarr: '↵', lArr: '⇐', uArr: '⇑', rArr: '⇒', dArr: '⇓', hArr: '⇔', forall: '∀', part: '∂', exist: '∃', empty: '∅', nabla: '∇', isin: '∈', notin: '∉', ni: '∋', prod: '∏', sum: '∑', minus: '−', lowast: '∗', radic: '√', prop: '∝', infin: '∞', ang: '∠', and: '∧', or: '∨', cap: '∩', cup: '∪', int: '∫', there4: '∴', sim: '∼', cong: '≅', asymp: '≈', ne: '≠', equiv: '≡', le: '≤', ge: '≥', sub: '⊂', sup: '⊃', nsub: '⊄', sube: '⊆', supe: '⊇', oplus: '⊕', otimes: '⊗', perp: '⊥', sdot: '⋅', lceil: '⌈', rceil: '⌉', lfloor: '⌊', rfloor: '⌋', lang: '〈', rang: '〉', loz: '◊', spades: '♠', clubs: '♣', hearts: '♥', diams: '♦', quot: '"', amp: '&', lt: '<', gt: '>', OElig: 'Œ', oelig: 'œ', Scaron: 'Š', scaron: 'š', Yuml: 'Ÿ', circ: 'ˆ', tilde: '˜', ensp: ' ', emsp: ' ', thinsp: ' ', zwnj: '‌', zwj: '‍', lrm: '‎', rlm: '‏', ndash: '–', mdash: '—', lsquo: '‘', rsquo: '’', sbquo: '‚', ldquo: '“', rdquo: '”', bdquo: '„', dagger: '†', Dagger: '‡', permil: '‰', lsaquo: '‹', rsaquo: '›', euro: '€' }; /** * List of legacy (that don’t need a trailing `;`) named references which could, * depending on what follows them, turn into a different meaning * * @type {Array<string>} */ const dangerous = [ 'cent', 'copy', 'divide', 'gt', 'lt', 'not', 'para', 'times' ]; const own$3 = {}.hasOwnProperty; /** * `characterEntitiesHtml4` but inverted. * * @type {Record<string, string>} */ const characters = {}; /** @type {string} */ let key; for (key in characterEntitiesHtml4) { if (own$3.call(characterEntitiesHtml4, key)) { characters[characterEntitiesHtml4[key]] = key; } } const notAlphanumericRegex = /[^\dA-Za-z]/; /** * Configurable ways to encode characters as named references. * * @param {number} code * @param {number} next * @param {boolean|undefined} omit * @param {boolean|undefined} attribute * @returns {string} */ function toNamed(code, next, omit, attribute) { const character = String.fromCharCode(code); if (own$3.call(characters, character)) { const name = characters[character]; const value = '&' + name; if ( omit && characterEntitiesLegacy.includes(name) && !dangerous.includes(name) && (!attribute || (next && next !== 61 /* `=` */ && notAlphanumericRegex.test(String.fromCharCode(next)))) ) { return value } return value + ';' } return '' } /** * @typedef FormatSmartOptions * @property {boolean} [useNamedReferences=false] * Prefer named character references (`&amp;`) where possible. * @property {boolean} [useShortestReferences=false] * Prefer the shortest possible reference, if that results in less bytes. * **Note**: `useNamedReferences` can be omitted when using `useShortestReferences`. * @property {boolean} [omitOptionalSemicolons=false] * Whether to omit semicolons when possible. * **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. * @property {boolean} [attribute=false] * Create character references which don’t fail in attributes. * **Note**: `attribute` only applies when operating dangerously with * `omitOptionalSemicolons: true`. */ /** * Configurable ways to encode a character yielding pretty or small results. * * @param {number} code * @param {number} next * @param {FormatSmartOptions} options * @returns {string} */ function formatSmart(code, next, options) { let numeric = toHexadecimal(code, next, options.omitOptionalSemicolons); /** @type {string|undefined} */ let named; if (options.useNamedReferences || options.useShortestReferences) { named = toNamed( code, next, options.omitOptionalSemicolons, options.attribute ); } // Use the shortest numeric reference when requested. // A simple algorithm would use decimal for all code points under 100, as // those are shorter than hexadecimal: // // * `&#99;` vs `&#x63;` (decimal shorter) // * `&#100;` vs `&#x64;` (equal) // // However, because we take `next` into consideration when `omit` is used, // And it would be possible that decimals are shorter on bigger values as // well if `next` is hexadecimal but not decimal, we instead compare both. if ( (options.useShortestReferences || !named) && options.useShortestReferences ) { const decimal = toDecimal(code, next, options.omitOptionalSemicolons); if (decimal.length < numeric.length) { numeric = decimal; } } return named && (!options.useShortestReferences || named.length < numeric.length) ? named : numeric } /** * @typedef {import('./core.js').CoreOptions & import('./util/format-smart.js').FormatSmartOptions} Options * @typedef {import('./core.js').CoreOptions} LightOptions */ /** * Encode special characters in `value`. * * @param {string} value * Value to encode. * @param {Options} [options] * Configuration. * @returns {string} * Encoded value. */ function stringifyEntities(value, options) { return core(value, Object.assign({format: formatSmart}, options)) } /** * @import {Comment, Parents} from 'hast' * @import {State} from '../index.js' */ const htmlCommentRegex = /^>|^->|<!--|-->|--!>|<!-$/g; // Declare arrays as variables so it can be cached by `stringifyEntities` const bogusCommentEntitySubset = ['>']; const commentEntitySubset = ['<', '>']; /** * Serialize a comment. * * @param {Comment} node * Node to handle. * @param {number | undefined} _1 * Index of `node` in `parent. * @param {Parents | undefined} _2 * Parent of `node`. * @param {State} state * Info passed around about the current state. * @returns {string} * Serialized node. */ function comment(node, _1, _2, state) { // See: <https://html.spec.whatwg.org/multipage/syntax.html#comments> return state.settings.bogusComments ? '<?' + stringifyEntities( node.value, Object.assign({}, state.settings.characterReferences, { subset: bogusCommentEntitySubset }) ) + '>' : '<!--' + node.value.replace(htmlCommentRegex, encode) + '-->' /** * @param {string} $0 */ function encode($0) { return stringifyEntities( $0, Object.assign({}, state.settings.characterReferences, { subset: commentEntitySubset }) ) } } /** * @import {Doctype, Parents} from 'hast' * @import {State} from '../index.js' */ /** * Serialize a doctype. * * @param {Doctype} _1 * Node to handle. * @param {number | undefined} _2 * Index of `node` in `parent. * @param {Parents | undefined} _3 * Parent of `node`. * @param {State} state * Info passed around about the current state. * @returns {string} * Serialized node. */ function doctype(_1, _2, _3, state) { return ( '<!' + (state.settings.upperDoctype ? 'DOCTYPE' : 'doctype') + (state.settings.tightDoctype ? '' : ' ') + 'html>' ) } /** * Count how often a character (or substring) is used in a string. * * @param {string} value * Value to search in. * @param {string} character * Character (or substring) to look for. * @return {number} * Number of times `character` occurred in `value`. */ function ccount(value, character) { const source = String(value); if (typeof character !== 'string') { throw new TypeError('Expected character') } let count = 0; let index = source.indexOf(character); while (index !== -1) { count++; index = source.indexOf(character, index + character.length); } return count } /** * @typedef {import('hast').Nodes} Nodes */ // HTML whitespace expression. // See <https://infra.spec.whatwg.org/#ascii-whitespace>. const re = /[ \t\n\f\r]/g; /** * Check if the given value is *inter-element whitespace*. * * @param {Nodes | string} thing * Thing to check (`Node` or `string`). * @returns {boolean} * Whether the `value` is inter-element whitespace (`boolean`): consisting of * zero or more of space, tab (`\t`), line feed (`\n`), carriage return * (`\r`), or form feed (`\f`); if a node is passed it must be a `Text` node, * whose `value` field is checked. */ function whitespace(thing) { return typeof thing === 'object' ? thing.type === 'text' ? empty(thing.value) : false : empty(thing) } /** * @param {string} value * @returns {boolean} */ function empty(value) { return value.replace(re, '') === '' } /** * @import {Parents, RootContent} from 'hast' */ const siblingAfter = siblings(1); const siblingBefore = siblings(-1); /** @type {Array<RootContent>} */ const emptyChildren$1 = []; /** * Factory to check siblings in a direction. * * @param {number} increment */ function siblings(increment) { return sibling /** * Find applicable siblings in a direction. * * @template {Parents} Parent * Parent type. * @param {Parent | undefined} parent * Parent. * @param {number | undefined} index * Index of child in `parent`. * @param {boolean | undefined} [includeWhitespace=false] * Whether to include whitespace (default: `false`). * @returns {Parent extends {children: Array<infer Child>} ? Child | undefined : never} * Child of parent. */ function sibling(parent, index, includeWhitespace) { const siblings = parent ? parent.children : emptyChildren$1; let offset = (index || 0) + increment; let next = siblings[offset]; if (!includeWhitespace) { while (next && whitespace(next)) { offset += increment; next = siblings[offset]; } } // @ts-expect-error: it’s a correct child. return next } } /** * @import {Element, Parents} from 'hast' */ /** * @callback OmitHandle * Check if a tag can be omitted. * @param {Element} element * Element to check. * @param {number | undefined} index * Index of element in parent. * @param {Parents | undefined} parent * Parent of element. * @returns {boolean} * Whether to omit a tag. * */ const own$2 = {}.hasOwnProperty; /** * Factory to check if a given node can have a tag omitted. * * @param {Record<string, OmitHandle>} 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. */ function omission(handlers) { return omit /** * Check if a given node can have a tag omitted. * * @type {OmitHandle} */ function omit(node, index, parent) { return ( own$2.call(handlers, node.tagName) && handlers[node.tagName](node, index, parent) ) } } /** * @import {Element, Parents} from 'hast' */ const closing = omission({ body: body$1, caption: headOrColgroupOrCaption, colgroup: headOrColgroupOrCaption, dd, dt, head: headOrColgroupOrCaption, html: html$2, li, optgroup, option, p, rp: rubyElement, rt: rubyElement, tbody: tbody$1, td: cells, tfoot, th: cells, thead, tr }); /** * Macro for `</head>`, `</colgroup>`, and `</caption>`. * * @param {Element} _ * Element. * @param {number | undefined} index * Index of element in parent. * @param {Parents | 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 || (next.type !== 'comment' && !(next.type === 'text' && whitespace(next.value.charAt(0)))) ) } /** * Whether to omit `</html>`. * * @param {Element} _ * Element. * @param {number | undefined} index * Index of element in parent. * @param {Parents | undefined} parent * Parent of element. * @returns {boolean} * Whether the closing tag can be omitted. */ function html$2(_, index, parent) { const next = siblingAfter(parent, index); return !next || next.type !== 'comment' } /** * Whether to omit `</body>`. * * @param {Element} _ * Element. * @param {number | undefined} index * Index of element in parent. * @param {Parents | undefined} parent * Parent of element. * @returns {boolean} * Whether the closing tag can be omitted. */ function body$1(_, index, parent) { const next = siblingAfter(parent, index); return !next || next.type !== 'comment' } /** * Whether to omit `</p>`. * * @param {Element} _ * Element. * @param {number | undefined} index * Index of element in parent. * @param {Parents | undefined} parent * Parent of element. * @returns {boolean} * Whether the closing tag can be omitted. */ function p(_, index, parent) { const next = siblingAfter(parent, index); return next ? 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. !( 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 `</li>`. * * @param {Element} _ * Element. * @param {number | undefined} index * Index of element in parent. * @param {Parents | 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 || (next.type === 'element' && next.tagName === 'li') } /** * Whether to omit `</dt>`. * * @param {Element} _ * Element. * @param {number | undefined} index * Index of element in parent. * @param {Parents | undefined} parent * Parent of element. * @returns {boolean} * Whether the closing tag can be omitted. */ function dt(_, index, parent) { const next = siblingAfter(parent, index); return Boolean( next && next.type === 'element' && (next.tagName === 'dt' || next.tagName === 'dd') ) } /** * Whether to omit `</dd>`. * * @param {Element} _ * Element. * @param {number | undefined} index * Index of element in parent. * @param {Parents | 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 || (next.type === 'element' && (next.tagName === 'dt' || next.tagName === 'dd')) ) } /** * Whether to omit `</rt>` or `</rp>`. * * @param {Element} _ * Element. * @param {number | undefined} index * Index of element in parent. * @param {Parents | 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 || (next.type === 'element' && (next.tagName === 'rp' || next.tagName === 'rt')) ) } /** * Whether to omit `</optgroup>`. * * @param {Element} _ * Element. * @param {number | undefined} index * Index of element in parent. * @param {Parents | 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 || (next.type === 'element' && next.tagName === 'optgroup') } /** * Whether to omit `</option>`. * * @param {Element} _ * Element. * @param {number | undefined} index * Index of element in parent. * @param {Parents | 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 || (next.type === 'element' && (next.tagName === 'option' || next.tagName === 'optgroup')) ) } /** * Whether to omit `</thead>`. * * @param {Element} _ * Element. * @param {number | undefined} index * Index of element in parent. * @param {Parents | undefined} parent * Parent of element. * @returns {boolean} * Whether the closing tag can be omitted. */ function thead(_, index, parent) { const next = siblingAfter(parent, index); return Boolean( next && next.type === 'element' && (next.tagName === 'tbody' || next.tagName === 'tfoot') ) } /** * Whether to omit `</tbody>`. * * @param {Element} _ * Element. * @param {number | undefined} index * Index of element in parent. * @param {Parents | undefined} parent * Parent of element. * @returns {boolean} * Whether the closing tag can be omitted. */ function tbody$1(_, index, parent) { const next = siblingAfter(parent, index); return ( !next || (next.type === 'element' && (next.tagName === 'tbody' || next.tagName === 'tfoot')) ) } /** * Whether to omit `</tfoot>`. * * @param {Element} _ * Element. * @param {number | undefined} index * Index of element in parent. * @param {Parents | undefined} parent * Parent of element. * @returns {boolean} * Whether the closing tag can be omitted. */ function tfoot(_, index, parent) { return !siblingAfter(parent, index) } /** * Whether to omit `</tr>`. * * @param {Element} _ * Element. * @param {number | undefined} index * Index of element in parent. * @param {Parents | 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 || (next.type === 'element' && next.tagName === 'tr') } /** * Whether to omit `</td>` or `</th>`. * * @param {Element} _ * Element. * @param {number | undefined} index * Index of element in parent. * @param {Parents | 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 || (next.type === 'element' && (next.tagName === 'td' || next.tagName === 'th')) ) } /** * @import {Element, Parents} from 'hast' */ const opening = omission({ body, colgroup, head, html: html$1, tbody }); /** * Whether to omit `<html>`. * * @param {Element} node * Element. * @returns {boolean} * Whether the opening tag can be omitted. */ function html$1(node) { const head = siblingAfter(node, -1); return !head || head.type !== 'comment' } /** * Whether to omit `<head>`. * * @param {Element} node * Element. * @returns {boolean} * Whether the opening tag can be omitted. */ function head(node) { /** @type {Set<string>} */ const seen = new Set(); // Whether `srcdoc` or not, // make sure the content model at least doesn’t have too many `base`s/`title`s. for (const child of node.children) { if ( child.type === 'element' && (child.tagName === 'base' || child.tagName === 'title') ) { if (seen.has(child.tagName)) return false seen.add(child.tagName); } } // “May be omitted if the element is empty, // or if the first thing inside the head element is an element.” const child = node.children[0]; return !child || child.type === 'element' } /** * Whether to omit `<body>`. * * @param {Element} node * Element. * @returns {boolean} * Whether the opening tag can be omitted. */ function body(node) { const head = siblingAfter(node, -1, true); return ( !head || (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') )) ) } /** * Whether to omit `<colgroup>`. * The spec describes some logic for the opening tag, but it’s easier to * implement in the closing tag, to the same effect, so we handle it there * instead. * * @param {Element} node * Element. * @param {number | undefined} index * Index of element in parent. * @param {Parents | undefined} parent * Parent of element. * @returns {boolean} * Whether the opening tag can be omitted. */ function colgroup(node, index, parent) { const previous = siblingBefore(parent, index); const head = siblingAfter(node, -1, true); // Previous colgroup was already omitted. if ( parent && previous && previous.type === 'element' && previous.tagName === 'colgroup' && closing(previous, parent.children.indexOf(previous), parent) ) { return false } return Boolean(head && head.type === 'element' && head.tagName === 'col') } /** * Whether to omit `<tbody>`. * * @param {Element} node * Element. * @param {number | undefined} index * Index of element in parent. * @param {Parents | undefined} parent * Parent of element. * @returns {boolean} * Whether the opening tag can be omitted. */ function tbody(node, index, parent) { const previous = siblingBefore(parent, index); const head = siblingAfter(node, -1); // Previous table section was already omitted. if ( parent && previous && previous.type === 'element' && (previous.tagName === 'thead' || previous.tagName === 'tbody') && closing(previous, parent.children.indexOf(previous), parent) ) { return false } return Boolean(head && head.type === 'element' && head.tagName === 'tr') } /** * @import {Element, Parents, Properties} from 'hast' * @import {State} from '../index.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<'double' | 'name' | 'single' | 'unquoted', Array<[Array<string>, Array<string>]>>} */ const constants = { // See: <https://html.spec.whatwg.org/#attribute-name-state>. name: [ ['\t\n\f\r &/=>'.split(''), '\t\n\f\r "&\'/=>`'.split('')], ['\0\t\n\f\r "&\'/<=>'.split(''), '\0\t\n\f\r "&\'/<=>`'.split('')] ], // See: <https://html.spec.whatwg.org/#attribute-value-(unquoted)-state>. 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: <https://html.spec.whatwg.org/#attribute-value-(single-quoted)-state>. single: [ ["&'".split(''), '"&\'`'.split('')], ["\0&'".split(''), '\0"&\'`'.split('')] ], // See: <https://html.spec.whatwg.org/#attribute-value-(double-quoted)-state>. 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 {Parents | undefined} parent * Parent of `node`. * @param {State} state * Info passed around about the current state. * @returns {string} * Serialized node. */ 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<string>} */ const parts = []; /** @type {string} */ let last; if (schema.space === 'html' && node.tagName === 'svg') { state.schema = svg; } const attributes = 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. // Note: `menuitem` has since been removed from the HTML spec, and so is no // longer void. if (content) selfClosing = false; if (attributes || !omit || !opening(node, index, parent)) { parts.push('<', node.tagName, attributes ? ' ' + attributes : ''); if ( selfClosing && (schema.space === 'svg' || state.settings.closeSelfClosing) ) { last = attributes.charAt(attributes.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('</' + node.tagName + '>'); } return parts.join('') } /** * @param {State} state * @param {Properties | null | undefined} properties * @returns {string} */ function serializeAttributes(state, properties) { /** @type {Array<string>} */ const values = []; let index = -1; /** @type {string} */ let key; if (properties) { for (key in properties) { if (properties[key] !== null && properties[key] !== undefined) { const value = serializeAttribute(state, key, properties[key]); if (value) values.push(value); } } } while (++index < values.length) { const last = state.settings.tightAttributes ? values[index].charAt(values[index].length - 1) : undefined; // 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 {Properties[keyof Properties]} value * @returns {string} */ 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 === info.attribute || value === '') ) { value = Boolean(value); } if ( value === null || value === undefined || 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 // `<a>`. // 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 // <!doctype html> // <svg viewBox="0 0 100 100"> // <a href=https://example.com download> // <circle cx=50 cy=40 r=35 /> // </a> // </svg> // ``` // // See: <https://github.com/wooorm/property-information/blob/main/lib/svg.js> 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 ? stringify : stringify$1)(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, { attribute: true, subset: constants.unquoted[x][y] }) ); } // 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) } /** * @import {Parents, Text} from 'hast' * @import {Raw} from 'mdast-util-to-hast' * @import {State} from '../index.js' */ // Declare array as variable so it can be cached by `stringifyEntities` const textEntitySubset = ['<', '&']; /** * Serialize a text node. * * @param {Raw | Text} node * Node to handle. * @param {number | undefined} _ * Index of `node` in `parent. * @param {Parents | undefined} parent * Parent of `node`. * @param {State} state * Info passed around about the current state. * @returns {string} * Serialized node. */ function text$2(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: textEntitySubset }) ) } /** * @import {Parents} from 'hast' * @import {Raw} from 'mdast-util-to-hast' * @import {State} from '../index.js' */ /** * Serialize a raw node. * * @param {Raw} node * Node to handle. * @param {number | undefined} index * Index of `node` in `parent. * @param {Parents | undefined} parent * Parent of `node`. * @param {State} state * Info passed around about the current state. * @returns {string} * Serialized node. */ function raw$1(node, index, parent, state) { return state.settings.allowDangerousHtml ? node.value : text$2(node, index, parent, state) } /** * @import {Parents, Root} from 'hast' * @import {State} from '../index.js' */ /** * Serialize a root. * * @param {Root} node * Node to handle. * @param {number | undefined} _1 * Index of `node` in `parent. * @param {Parents | undefined} _2 * Parent of `node`. * @param {State} state * Info passed around about the current state. * @returns {string} * Serialized node. */ function root$1(node, _1, _2, state) { return state.all(node) } /** * @import {Nodes, Parents} from 'hast' * @import {State} from '../index.js' */ /** * @type {(node: Nodes, index: number | undefined, parent: Parents | undefined, state: State) => string} */ const handle$1 = zwitch('type', { invalid, unknown, handlers: {comment, doctype, element, raw: raw$1, root: root$1, text: text$2} }); /** * 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_) { // `type` is guaranteed by runtime JS. const node = /** @type {Nodes} */ (node_); throw new Error('Cannot compile unknown node `' + node.type + '`') } /** * @import {Nodes, Parents, RootContent} from 'hast' * @import {Schema} from 'property-information' * @import {Options as StringifyEntitiesOptions} from 'stringify-entities' */ /** @type {Options} */ const emptyOptions$1 = {}; /** @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. */ function toHtml(tree, options) { const options_ = options || emptyOptions$1; const quote = options_.quote || '"'; const alternative = quote === '"' ? "'" : '"'; if (quote !== '"' && quote !== "'") { throw new Error('Invalid quote `' + quote + '`, expected `\'` or `"`') } /** @type {State} */ const state = { one: one$1, all: all$1, settings: { omitOptionalTags: options_.omitOptionalTags || false, allowParseErrors: options_.allowParseErrors || false, allowDangerousCharacters: options_.allowDangerousCharacters || false, quoteSmart: options_.quoteSmart || false, preferUnquote