UNPKG

html-validate

Version:

Offline HTML5 validator and linter

2,125 lines (2,064 loc) 396 kB
import Ajv from 'ajv'; import { e as entities$1, h as html5, b as bundledElements } from './elements.js'; import betterAjvErrors from '@sidvind/better-ajv-errors'; import { n as naturalJoin } from './utils/natural-join.js'; import kleur from 'kleur'; import { stylish as stylish$1 } from '@html-validate/stylish'; import semver from 'semver'; const $schema$2 = "http://json-schema.org/draft-06/schema#"; const $id$2 = "http://json-schema.org/draft-06/schema#"; const title = "Core schema meta-schema"; const definitions$1 = { schemaArray: { type: "array", minItems: 1, items: { $ref: "#" } }, nonNegativeInteger: { type: "integer", minimum: 0 }, nonNegativeIntegerDefault0: { allOf: [ { $ref: "#/definitions/nonNegativeInteger" }, { "default": 0 } ] }, simpleTypes: { "enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ] }, stringArray: { type: "array", items: { type: "string" }, uniqueItems: true, "default": [ ] } }; const type$2 = [ "object", "boolean" ]; const properties$2 = { $id: { type: "string", format: "uri-reference" }, $schema: { type: "string", format: "uri" }, $ref: { type: "string", format: "uri-reference" }, title: { type: "string" }, description: { type: "string" }, "default": { }, examples: { type: "array", items: { } }, multipleOf: { type: "number", exclusiveMinimum: 0 }, maximum: { type: "number" }, exclusiveMaximum: { type: "number" }, minimum: { type: "number" }, exclusiveMinimum: { type: "number" }, maxLength: { $ref: "#/definitions/nonNegativeInteger" }, minLength: { $ref: "#/definitions/nonNegativeIntegerDefault0" }, pattern: { type: "string", format: "regex" }, additionalItems: { $ref: "#" }, items: { anyOf: [ { $ref: "#" }, { $ref: "#/definitions/schemaArray" } ], "default": { } }, maxItems: { $ref: "#/definitions/nonNegativeInteger" }, minItems: { $ref: "#/definitions/nonNegativeIntegerDefault0" }, uniqueItems: { type: "boolean", "default": false }, contains: { $ref: "#" }, maxProperties: { $ref: "#/definitions/nonNegativeInteger" }, minProperties: { $ref: "#/definitions/nonNegativeIntegerDefault0" }, required: { $ref: "#/definitions/stringArray" }, additionalProperties: { $ref: "#" }, definitions: { type: "object", additionalProperties: { $ref: "#" }, "default": { } }, properties: { type: "object", additionalProperties: { $ref: "#" }, "default": { } }, patternProperties: { type: "object", additionalProperties: { $ref: "#" }, "default": { } }, dependencies: { type: "object", additionalProperties: { anyOf: [ { $ref: "#" }, { $ref: "#/definitions/stringArray" } ] } }, propertyNames: { $ref: "#" }, "const": { }, "enum": { type: "array", minItems: 1, uniqueItems: true }, type: { anyOf: [ { $ref: "#/definitions/simpleTypes" }, { type: "array", items: { $ref: "#/definitions/simpleTypes" }, minItems: 1, uniqueItems: true } ] }, format: { type: "string" }, allOf: { $ref: "#/definitions/schemaArray" }, anyOf: { $ref: "#/definitions/schemaArray" }, oneOf: { $ref: "#/definitions/schemaArray" }, not: { $ref: "#" } }; var ajvSchemaDraft = { $schema: $schema$2, $id: $id$2, title: title, definitions: definitions$1, type: type$2, properties: properties$2, "default": { } }; function getDefaultExportFromCjs (x) { return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; } var cjs; var hasRequiredCjs; function requireCjs () { if (hasRequiredCjs) return cjs; hasRequiredCjs = 1; var isMergeableObject = function isMergeableObject(value) { return isNonNullObject(value) && !isSpecial(value) }; function isNonNullObject(value) { return !!value && typeof value === 'object' } function isSpecial(value) { var stringValue = Object.prototype.toString.call(value); return stringValue === '[object RegExp]' || stringValue === '[object Date]' || isReactElement(value) } // see https://github.com/facebook/react/blob/b5ac963fb791d1298e7f396236383bc955f916c1/src/isomorphic/classic/element/ReactElement.js#L21-L25 var canUseSymbol = typeof Symbol === 'function' && Symbol.for; var REACT_ELEMENT_TYPE = canUseSymbol ? Symbol.for('react.element') : 0xeac7; function isReactElement(value) { return value.$$typeof === REACT_ELEMENT_TYPE } function emptyTarget(val) { return Array.isArray(val) ? [] : {} } function cloneUnlessOtherwiseSpecified(value, options) { return (options.clone !== false && options.isMergeableObject(value)) ? deepmerge(emptyTarget(value), value, options) : value } function defaultArrayMerge(target, source, options) { return target.concat(source).map(function(element) { return cloneUnlessOtherwiseSpecified(element, options) }) } function getMergeFunction(key, options) { if (!options.customMerge) { return deepmerge } var customMerge = options.customMerge(key); return typeof customMerge === 'function' ? customMerge : deepmerge } function getEnumerableOwnPropertySymbols(target) { return Object.getOwnPropertySymbols ? Object.getOwnPropertySymbols(target).filter(function(symbol) { return Object.propertyIsEnumerable.call(target, symbol) }) : [] } function getKeys(target) { return Object.keys(target).concat(getEnumerableOwnPropertySymbols(target)) } function propertyIsOnObject(object, property) { try { return property in object } catch(_) { return false } } // Protects from prototype poisoning and unexpected merging up the prototype chain. function propertyIsUnsafe(target, key) { return propertyIsOnObject(target, key) // Properties are safe to merge if they don't exist in the target yet, && !(Object.hasOwnProperty.call(target, key) // unsafe if they exist up the prototype chain, && Object.propertyIsEnumerable.call(target, key)) // and also unsafe if they're nonenumerable. } function mergeObject(target, source, options) { var destination = {}; if (options.isMergeableObject(target)) { getKeys(target).forEach(function(key) { destination[key] = cloneUnlessOtherwiseSpecified(target[key], options); }); } getKeys(source).forEach(function(key) { if (propertyIsUnsafe(target, key)) { return } if (propertyIsOnObject(target, key) && options.isMergeableObject(source[key])) { destination[key] = getMergeFunction(key, options)(target[key], source[key], options); } else { destination[key] = cloneUnlessOtherwiseSpecified(source[key], options); } }); return destination } function deepmerge(target, source, options) { options = options || {}; options.arrayMerge = options.arrayMerge || defaultArrayMerge; options.isMergeableObject = options.isMergeableObject || isMergeableObject; // cloneUnlessOtherwiseSpecified is added to `options` so that custom arrayMerge() // implementations can use it. The caller may not replace it. options.cloneUnlessOtherwiseSpecified = cloneUnlessOtherwiseSpecified; var sourceIsArray = Array.isArray(source); var targetIsArray = Array.isArray(target); var sourceAndTargetTypesMatch = sourceIsArray === targetIsArray; if (!sourceAndTargetTypesMatch) { return cloneUnlessOtherwiseSpecified(source, options) } else if (sourceIsArray) { return options.arrayMerge(target, source, options) } else { return mergeObject(target, source, options) } } deepmerge.all = function deepmergeAll(array, options) { if (!Array.isArray(array)) { throw new Error('first argument should be an array') } return array.reduce(function(prev, next) { return deepmerge(prev, next, options) }, {}) }; var deepmerge_1 = deepmerge; cjs = deepmerge_1; return cjs; } var cjsExports = /*@__PURE__*/ requireCjs(); var deepmerge = /*@__PURE__*/getDefaultExportFromCjs(cjsExports); function stringify(value) { if (typeof value === "string") { return value; } else { return JSON.stringify(value); } } class WrappedError extends Error { constructor(message) { super(stringify(message)); } } function ensureError(value) { if (value instanceof Error) { return value; } else { return new WrappedError(value); } } class NestedError extends Error { constructor(message, nested) { super(message); Error.captureStackTrace(this, NestedError); this.name = NestedError.name; if (nested?.stack) { this.stack ??= ""; this.stack += ` Caused by: ${nested.stack}`; } } } class UserError extends NestedError { constructor(message, nested) { super(message, nested); Error.captureStackTrace(this, UserError); this.name = UserError.name; Object.defineProperty(this, "isUserError", { value: true, enumerable: false, writable: false }); } /** * @public */ /* istanbul ignore next: default implementation */ prettyFormat() { return void 0; } } class InheritError extends UserError { tagName; inherit; filename; constructor({ tagName, inherit }) { const message = `Element <${tagName}> cannot inherit from <${inherit}>: no such element`; super(message); Error.captureStackTrace(this, InheritError); this.name = InheritError.name; this.tagName = tagName; this.inherit = inherit; this.filename = null; } prettyFormat() { const { message, tagName, inherit } = this; const source = this.filename ? ["", "This error occurred when loading element metadata from:", `"${this.filename}"`, ""] : [""]; return [ message, ...source, "This usually occurs when the elements are defined in the wrong order, try one of the following:", "", ` - Ensure the spelling of "${inherit}" is correct.`, ` - Ensure the file containing "${inherit}" is loaded before the file containing "${tagName}".`, ` - Move the definition of "${inherit}" above the definition for "${tagName}".` ].join("\n"); } } function isUserError(error) { return Boolean(error && typeof error === "object" && "isUserError" in error); } function getSummary(schema, obj, errors) { const output = betterAjvErrors(schema, obj, errors, { format: "js" }); return output.length > 0 ? output[0].error : "unknown validation error"; } class SchemaValidationError extends UserError { /** Configuration filename the error originates from */ filename; /** Configuration object the error originates from */ obj; /** JSON schema used when validating the configuration */ schema; /** List of schema validation errors */ errors; constructor(filename, message, obj, schema, errors) { const summary = getSummary(schema, obj, errors); super(`${message}: ${summary}`); this.filename = filename; this.obj = obj; this.schema = schema; this.errors = errors; } } const $schema$1 = "http://json-schema.org/draft-06/schema#"; const $id$1 = "https://html-validate.org/schemas/elements.json"; const type$1 = "object"; const properties$1 = { $schema: { type: "string" } }; const patternProperties = { "^[^$].*$": { type: "object", properties: { inherit: { title: "Inherit from another element", description: "Most properties from the parent element will be copied onto this one", type: "string" }, embedded: { title: "Mark this element as belonging in the embedded content category", $ref: "#/definitions/contentCategory" }, flow: { title: "Mark this element as belonging in the flow content category", $ref: "#/definitions/contentCategory" }, heading: { title: "Mark this element as belonging in the heading content category", $ref: "#/definitions/contentCategory" }, interactive: { title: "Mark this element as belonging in the interactive content category", $ref: "#/definitions/contentCategory" }, metadata: { title: "Mark this element as belonging in the metadata content category", $ref: "#/definitions/contentCategory" }, phrasing: { title: "Mark this element as belonging in the phrasing content category", $ref: "#/definitions/contentCategory" }, sectioning: { title: "Mark this element as belonging in the sectioning content category", $ref: "#/definitions/contentCategory" }, deprecated: { title: "Mark element as deprecated", description: "Deprecated elements should not be used. If a message is provided it will be included in the error", anyOf: [ { type: "boolean" }, { type: "string" }, { $ref: "#/definitions/deprecatedElement" } ] }, foreign: { title: "Mark element as foreign", description: "Foreign elements are elements which have a start and end tag but is otherwize not parsed", type: "boolean" }, "void": { title: "Mark element as void", description: "Void elements are elements which cannot have content and thus must not use an end tag", type: "boolean" }, transparent: { title: "Mark element as transparent", description: "Transparent elements follows the same content model as its parent, i.e. the content must be allowed in the parent.", anyOf: [ { type: "boolean" }, { type: "array", items: { type: "string" } } ] }, implicitClosed: { title: "List of elements which implicitly closes this element", description: "Some elements are automatically closed when another start tag occurs", type: "array", items: { type: "string" } }, implicitRole: { title: "Implicit ARIA role for this element", description: "Some elements have implicit ARIA roles.", deprecated: true, "function": true }, aria: { title: "WAI-ARIA properties for this element", $ref: "#/definitions/Aria" }, scriptSupporting: { title: "Mark element as script-supporting", description: "Script-supporting elements are elements which can be inserted where othersise not permitted to assist in templating", type: "boolean" }, focusable: { title: "Mark this element as focusable", description: "This element may contain an associated label element.", anyOf: [ { type: "boolean" }, { "function": true } ] }, form: { title: "Mark element as a submittable form element", type: "boolean" }, formAssociated: { title: "Mark element as a form-associated element", $ref: "#/definitions/FormAssociated" }, labelable: { title: "Mark this element as labelable", description: "This element may contain an associated label element.", anyOf: [ { type: "boolean" }, { "function": true } ] }, templateRoot: { title: "Mark element as an element ignoring DOM ancestry, i.e. <template>.", description: "The <template> element can contain any elements.", type: "boolean" }, deprecatedAttributes: { title: "List of deprecated attributes", type: "array", items: { type: "string" } }, requiredAttributes: { title: "List of required attributes", type: "array", items: { type: "string" } }, attributes: { title: "List of known attributes and allowed values", $ref: "#/definitions/PermittedAttribute" }, permittedContent: { title: "List of elements or categories allowed as content in this element", $ref: "#/definitions/Permitted" }, permittedDescendants: { title: "List of elements or categories allowed as descendants in this element", $ref: "#/definitions/Permitted" }, permittedOrder: { title: "Required order of child elements", $ref: "#/definitions/PermittedOrder" }, permittedParent: { title: "List of elements or categories allowed as parent to this element", $ref: "#/definitions/Permitted" }, requiredAncestors: { title: "List of required ancestor elements", $ref: "#/definitions/RequiredAncestors" }, requiredContent: { title: "List of required content elements", $ref: "#/definitions/RequiredContent" }, textContent: { title: "Allow, disallow or require textual content", description: "This property controls whenever an element allows, disallows or requires text. Text from any descendant counts, not only direct children", "default": "default", type: "string", "enum": [ "none", "default", "required", "accessible" ] } }, additionalProperties: false } }; const definitions = { Aria: { type: "object", additionalProperties: false, properties: { implicitRole: { title: "Implicit ARIA role for this element", description: "Some elements have implicit ARIA roles.", anyOf: [ { type: "string" }, { "function": true } ] }, naming: { title: "Prohibit or allow this element to be named by aria-label or aria-labelledby", anyOf: [ { type: "string", "enum": [ "prohibited", "allowed" ] }, { "function": true } ] } } }, contentCategory: { anyOf: [ { type: "boolean" }, { "function": true } ] }, deprecatedElement: { type: "object", additionalProperties: false, properties: { message: { type: "string", title: "A short text message shown next to the regular error message." }, documentation: { type: "string", title: "An extended markdown formatted message shown with the contextual rule documentation." }, source: { type: "string", title: "Element source, e.g. what standard or library deprecated this element.", "default": "html5" } } }, FormAssociated: { type: "object", additionalProperties: false, properties: { disablable: { type: "boolean", title: "Disablable elements can be disabled using the disabled attribute." }, listed: { type: "boolean", title: "Listed elements have a name attribute and is listed in the form and fieldset elements property." } } }, Permitted: { type: "array", items: { anyOf: [ { type: "string" }, { type: "array", items: { anyOf: [ { type: "string" }, { $ref: "#/definitions/PermittedGroup" } ] } }, { $ref: "#/definitions/PermittedGroup" } ] } }, PermittedAttribute: { type: "object", patternProperties: { "^.*$": { anyOf: [ { type: "object", additionalProperties: false, properties: { allowed: { "function": true, title: "Set to a function to evaluate if this attribute is allowed in this context" }, boolean: { type: "boolean", title: "Set to true if this is a boolean attribute" }, deprecated: { title: "Set to true or string if this attribute is deprecated", oneOf: [ { type: "boolean" }, { type: "string" } ] }, list: { type: "boolean", title: "Set to true if this attribute is a list of space-separated tokens, each which must be valid by itself" }, "enum": { type: "array", title: "Exhaustive list of values (string or regex) this attribute accepts", uniqueItems: true, items: { anyOf: [ { type: "string" }, { regexp: true } ] } }, omit: { type: "boolean", title: "Set to true if this attribute can optionally omit its value" }, required: { title: "Set to true or a function to evaluate if this attribute is required", oneOf: [ { type: "boolean" }, { "function": true } ] } } }, { type: "array", uniqueItems: true, items: { type: "string" } }, { type: "null" } ] } } }, PermittedGroup: { type: "object", additionalProperties: false, properties: { exclude: { anyOf: [ { items: { type: "string" }, type: "array" }, { type: "string" } ] } } }, PermittedOrder: { type: "array", items: { type: "string" } }, RequiredAncestors: { type: "array", items: { type: "string" } }, RequiredContent: { type: "array", items: { type: "string" } } }; var schema = { $schema: $schema$1, $id: $id$1, type: type$1, properties: properties$1, patternProperties: patternProperties, definitions: definitions }; const ajvRegexpValidate = function(data, dataCxt) { const valid = data instanceof RegExp; if (!valid) { ajvRegexpValidate.errors = [ { instancePath: dataCxt?.instancePath, schemaPath: void 0, keyword: "type", message: "should be a regular expression", params: { keyword: "type" } } ]; } return valid; }; const ajvRegexpKeyword = { keyword: "regexp", schema: false, errors: true, validate: ajvRegexpValidate }; const ajvFunctionValidate = function(data, dataCxt) { const valid = typeof data === "function"; if (!valid) { ajvFunctionValidate.errors = [ { instancePath: ( /* istanbul ignore next */ dataCxt?.instancePath ), schemaPath: void 0, keyword: "type", message: "should be a function", params: { keyword: "type" } } ]; } return valid; }; const ajvFunctionKeyword = { keyword: "function", schema: false, errors: true, validate: ajvFunctionValidate }; function cyrb53(str) { const a = 2654435761; const b = 1597334677; const c = 2246822507; const d = 3266489909; const e = 4294967296; const f = 2097151; const seed = 0; let h1 = 3735928559 ^ seed; let h2 = 1103547991 ^ seed; for (let i = 0, ch; i < str.length; i++) { ch = str.charCodeAt(i); h1 = Math.imul(h1 ^ ch, a); h2 = Math.imul(h2 ^ ch, b); } h1 = Math.imul(h1 ^ h1 >>> 16, c) ^ Math.imul(h2 ^ h2 >>> 13, d); h2 = Math.imul(h2 ^ h2 >>> 16, c) ^ Math.imul(h1 ^ h1 >>> 13, d); return e * (f & h2) + (h1 >>> 0); } const computeHash = cyrb53; var TextContent$1 = /* @__PURE__ */ ((TextContent2) => { TextContent2["NONE"] = "none"; TextContent2["DEFAULT"] = "default"; TextContent2["REQUIRED"] = "required"; TextContent2["ACCESSIBLE"] = "accessible"; return TextContent2; })(TextContent$1 || {}); const MetaCopyableProperty = [ "metadata", "flow", "sectioning", "heading", "phrasing", "embedded", "interactive", "transparent", "focusable", "form", "formAssociated", "labelable", "attributes", "aria", "permittedContent", "permittedDescendants", "permittedOrder", "permittedParent", "requiredAncestors", "requiredContent" ]; function setMetaProperty(dst, key, value) { dst[key] = value; } function isSet(value) { return typeof value !== "undefined"; } function flag(value) { return value ? true : void 0; } function stripUndefined(src) { const entries = Object.entries(src).filter(([, value]) => isSet(value)); return Object.fromEntries(entries); } function migrateSingleAttribute(src, key) { const result = {}; result.deprecated = flag(src.deprecatedAttributes?.includes(key)); result.required = flag(src.requiredAttributes?.includes(key)); result.omit = void 0; const attr = src.attributes ? src.attributes[key] : void 0; if (typeof attr === "undefined") { return stripUndefined(result); } if (attr === null) { result.delete = true; return stripUndefined(result); } if (Array.isArray(attr)) { if (attr.length === 0) { result.boolean = true; } else { result.enum = attr.filter((it) => it !== ""); if (attr.includes("")) { result.omit = true; } } return stripUndefined(result); } else { return stripUndefined({ ...result, ...attr }); } } function migrateAttributes(src) { const keys = [ ...Object.keys(src.attributes ?? {}), ...src.requiredAttributes ?? [], ...src.deprecatedAttributes ?? [] /* eslint-disable-next-line sonarjs/no-alphabetical-sort -- not really needed in this case, this is a-z anyway */ ].sort(); const entries = keys.map((key) => { return [key, migrateSingleAttribute(src, key)]; }); return Object.fromEntries(entries); } function normalizeAriaImplicitRole(value) { if (!value) { return () => null; } if (typeof value === "string") { return () => value; } return value; } function normalizeAriaNaming(value) { if (!value) { return () => "allowed"; } if (typeof value === "string") { return () => value; } return value; } function migrateElement(src) { const implicitRole = normalizeAriaImplicitRole(src.implicitRole ?? src.aria?.implicitRole); const result = { ...src, ...{ formAssociated: void 0 }, attributes: migrateAttributes(src), textContent: src.textContent, focusable: src.focusable ?? false, implicitRole, templateRoot: src.templateRoot === true, aria: { implicitRole, naming: normalizeAriaNaming(src.aria?.naming) } }; delete result.deprecatedAttributes; delete result.requiredAttributes; if (!result.textContent) { delete result.textContent; } if (src.formAssociated) { result.formAssociated = { disablable: Boolean(src.formAssociated.disablable), listed: Boolean(src.formAssociated.listed) }; } else { delete result.formAssociated; } return result; } const dynamicKeys = [ "metadata", "flow", "sectioning", "heading", "phrasing", "embedded", "interactive", "labelable" ]; const schemaCache = /* @__PURE__ */ new Map(); function clone(src) { return JSON.parse(JSON.stringify(src)); } function overwriteMerge$1(_a, b) { return b; } class MetaTable { elements; schema; /** * @internal */ constructor() { this.elements = {}; this.schema = clone(schema); } /** * @internal */ init() { this.resolveGlobal(); } /** * Extend validation schema. * * @public */ extendValidationSchema(patch) { if (patch.properties) { this.schema = deepmerge(this.schema, { patternProperties: { "^[^$].*$": { properties: patch.properties } } }); } if (patch.definitions) { this.schema = deepmerge(this.schema, { definitions: patch.definitions }); } } /** * Load metadata table from object. * * @public * @param obj - Object with metadata to load * @param filename - Optional filename used when presenting validation error */ loadFromObject(obj, filename = null) { try { const validate = this.getSchemaValidator(); if (!validate(obj)) { throw new SchemaValidationError( filename, `Element metadata is not valid`, obj, this.schema, /* istanbul ignore next: AJV sets .errors when validate returns false */ validate.errors ?? [] ); } for (const [key, value] of Object.entries(obj)) { if (key === "$schema") continue; this.addEntry(key, migrateElement(value)); } } catch (err) { if (err instanceof InheritError) { err.filename = filename; throw err; } if (err instanceof SchemaValidationError) { throw err; } if (!filename) { throw err; } throw new UserError(`Failed to load element metadata from "${filename}"`, ensureError(err)); } } /** * Get [[MetaElement]] for the given tag. If no specific metadata is present * the global metadata is returned or null if no global is present. * * @public * @returns A shallow copy of metadata. */ getMetaFor(tagName) { const meta = this.elements[tagName.toLowerCase()] ?? this.elements["*"]; if (meta) { return { ...meta }; } else { return null; } } /** * Find all tags which has enabled given property. * * @public */ getTagsWithProperty(propName) { return this.entries.filter(([, entry]) => entry[propName]).map(([tagName]) => tagName); } /** * Find tag matching tagName or inheriting from it. * * @public */ getTagsDerivedFrom(tagName) { return this.entries.filter(([key, entry]) => key === tagName || entry.inherit === tagName).map(([tagName2]) => tagName2); } addEntry(tagName, entry) { let parent = this.elements[tagName]; if (entry.inherit) { const name = entry.inherit; parent = this.elements[name]; if (!parent) { throw new InheritError({ tagName, inherit: name }); } } const expanded = this.mergeElement(parent ?? {}, { ...entry, tagName }); expandRegex(expanded); this.elements[tagName] = expanded; } /** * Construct a new AJV schema validator. */ getSchemaValidator() { const hash = computeHash(JSON.stringify(this.schema)); const cached = schemaCache.get(hash); if (cached) { return cached; } else { const ajv = new Ajv({ strict: true, strictTuples: true, strictTypes: true }); ajv.addMetaSchema(ajvSchemaDraft); ajv.addKeyword(ajvFunctionKeyword); ajv.addKeyword(ajvRegexpKeyword); ajv.addKeyword({ keyword: "copyable" }); const validate = ajv.compile(this.schema); schemaCache.set(hash, validate); return validate; } } /** * @public */ getJSONSchema() { return this.schema; } /** * @internal */ get entries() { return Object.entries(this.elements); } /** * Finds the global element definition and merges each known element with the * global, e.g. to assign global attributes. */ resolveGlobal() { if (!this.elements["*"]) return; const global = this.elements["*"]; delete this.elements["*"]; delete global.tagName; delete global.void; for (const [tagName, entry] of this.entries) { this.elements[tagName] = this.mergeElement(global, entry); } } mergeElement(a, b) { const merged = deepmerge(a, b, { arrayMerge: overwriteMerge$1 }); const filteredAttrs = Object.entries( merged.attributes ).filter(([, attr]) => { const val = !attr.delete; delete attr.delete; return val; }); merged.attributes = Object.fromEntries(filteredAttrs); return merged; } /** * @internal */ resolve(node) { if (node.meta) { expandProperties(node, node.meta); } } } function expandProperties(node, entry) { for (const key of dynamicKeys) { const property = entry[key]; if (typeof property === "function") { setMetaProperty(entry, key, property(node._adapter)); } } if (typeof entry.focusable === "function") { setMetaProperty(entry, "focusable", entry.focusable(node._adapter)); } } function expandRegexValue(value) { if (value instanceof RegExp) { return value; } const match = /^\/(.*(?=\/))\/(i?)$/.exec(value); if (match) { const [, expr, flags] = match; if (expr.startsWith("^") || expr.endsWith("$")) { return new RegExp(expr, flags); } else { return new RegExp(`^${expr}$`, flags); } } else { return value; } } function expandRegex(entry) { for (const [name, values] of Object.entries(entry.attributes)) { if (values.enum) { entry.attributes[name].enum = values.enum.map(expandRegexValue); } } } class DynamicValue { expr; constructor(expr) { this.expr = expr; } toString() { return this.expr; } } function isStaticAttribute(attr) { return Boolean(attr?.isStatic); } function isDynamicAttribute(attr) { return Boolean(attr?.isDynamic); } class Attribute { /** Attribute name */ key; value; keyLocation; valueLocation; originalAttribute; /** * @param key - Attribute name. * @param value - Attribute value. Set to `null` for boolean attributes. * @param keyLocation - Source location of attribute name. * @param valueLocation - Source location of attribute value. * @param originalAttribute - If this attribute was dynamically added via a * transformation (e.g. vuejs `:id` generating the `id` attribute) this * parameter should be set to the attribute name of the source attribute (`:id`). */ constructor(key, value, keyLocation, valueLocation, originalAttribute) { this.key = key; this.value = value; this.keyLocation = keyLocation; this.valueLocation = valueLocation; this.originalAttribute = originalAttribute; if (typeof this.value === "undefined") { this.value = null; } } /** * Flag set to true if the attribute value is static. */ get isStatic() { return !this.isDynamic; } /** * Flag set to true if the attribute value is dynamic. */ get isDynamic() { return this.value instanceof DynamicValue; } /** * Test attribute value. * * @param pattern - Pattern to match value against. Can be a RegExp, literal * string or an array of strings (returns true if any value matches the * array). * @param dynamicMatches - If true `DynamicValue` will always match, if false * it never matches. * @returns `true` if attribute value matches pattern. */ valueMatches(pattern, dynamicMatches = true) { if (this.value === null) { return false; } if (this.value instanceof DynamicValue) { return dynamicMatches; } if (Array.isArray(pattern)) { return pattern.includes(this.value); } if (pattern instanceof RegExp) { return this.value.match(pattern) !== null; } else { return this.value === pattern; } } } function getCSSDeclarations(value) { return value.trim().split(";").filter(Boolean).map((it) => { const [property, value2] = it.split(":", 2); return [property.trim(), value2 ? value2.trim() : ""]; }); } function parseCssDeclaration(value) { if (!value || value instanceof DynamicValue) { return {}; } const pairs = getCSSDeclarations(value); return Object.fromEntries(pairs); } function sliceSize(size, begin, end) { if (typeof size !== "number") { return size; } if (typeof end !== "number") { return size - begin; } if (end < 0) { end = size + end; } return Math.min(size, end - begin); } function sliceLocation(location, begin, end, wrap) { if (!location) return null; const size = sliceSize(location.size, begin, end); const sliced = { filename: location.filename, offset: location.offset + begin, line: location.line, column: location.column + begin, size }; if (wrap) { let index = -1; const col = sliced.column; for (; ; ) { index = wrap.indexOf("\n", index + 1); if (index >= 0 && index < begin) { sliced.column = col - (index + 1); sliced.line++; } else { break; } } } return sliced; } var State = /* @__PURE__ */ ((State2) => { State2[State2["INITIAL"] = 1] = "INITIAL"; State2[State2["DOCTYPE"] = 2] = "DOCTYPE"; State2[State2["TEXT"] = 3] = "TEXT"; State2[State2["TAG"] = 4] = "TAG"; State2[State2["ATTR"] = 5] = "ATTR"; State2[State2["CDATA"] = 6] = "CDATA"; State2[State2["SCRIPT"] = 7] = "SCRIPT"; State2[State2["STYLE"] = 8] = "STYLE"; State2[State2["TEXTAREA"] = 9] = "TEXTAREA"; State2[State2["TITLE"] = 10] = "TITLE"; return State2; })(State || {}); var ContentModel = /* @__PURE__ */ ((ContentModel2) => { ContentModel2[ContentModel2["TEXT"] = 1] = "TEXT"; ContentModel2[ContentModel2["SCRIPT"] = 2] = "SCRIPT"; ContentModel2[ContentModel2["STYLE"] = 3] = "STYLE"; ContentModel2[ContentModel2["TEXTAREA"] = 4] = "TEXTAREA"; ContentModel2[ContentModel2["TITLE"] = 5] = "TITLE"; return ContentModel2; })(ContentModel || {}); class Context { contentModel; state; string; filename; offset; line; column; constructor(source) { this.state = State.INITIAL; this.string = source.data; this.filename = source.filename; this.offset = source.offset; this.line = source.line; this.column = source.column; this.contentModel = 1 /* TEXT */; } getTruncatedLine(n = 13) { return JSON.stringify(this.string.length > n ? `${this.string.slice(0, 10)}...` : this.string); } consume(n, state) { let consumed = this.string.slice(0, n); let offset; while ((offset = consumed.indexOf("\n")) >= 0) { this.line++; this.column = 1; consumed = consumed.substr(offset + 1); } this.column += consumed.length; this.offset += n; this.string = this.string.substr(n); this.state = state; } getLocation(size) { return { filename: this.filename, offset: this.offset, line: this.line, column: this.column, size }; } } function normalizeSource(source) { return { filename: "", offset: 0, line: 1, column: 1, ...source }; } var NodeType = /* @__PURE__ */ ((NodeType2) => { NodeType2[NodeType2["ELEMENT_NODE"] = 1] = "ELEMENT_NODE"; NodeType2[NodeType2["TEXT_NODE"] = 3] = "TEXT_NODE"; NodeType2[NodeType2["DOCUMENT_NODE"] = 9] = "DOCUMENT_NODE"; return NodeType2; })(NodeType || {}); const DOCUMENT_NODE_NAME = "#document"; const TEXT_CONTENT = /* @__PURE__ */ Symbol("textContent"); let counter = 0; class DOMNode { nodeName; nodeType; childNodes; location; /** * @internal */ unique; cache; /** * Set of disabled rules for this node. * * Rules disabled by using directives are added here. */ disabledRules; /** * Set of blocked rules for this node. * * Rules blocked by using directives are added here. */ blockedRules; /** * Create a new DOMNode. * * @internal * @param nodeType - What node type to create. * @param nodeName - What node name to use. For `HtmlElement` this corresponds * to the tagName but other node types have specific predefined values. * @param location - Source code location of this node. */ constructor(nodeType, nodeName, location) { this.nodeType = nodeType; this.nodeName = nodeName ?? DOCUMENT_NODE_NAME; this.location = location; this.disabledRules = /* @__PURE__ */ new Set(); this.blockedRules = /* @__PURE__ */ new Map(); this.childNodes = []; this.unique = counter++; this.cache = null; } /** * Enable cache for this node. * * Should not be called before the node and all children are fully constructed. * * @internal */ cacheEnable(enable = true) { this.cache = enable ? /* @__PURE__ */ new Map() : null; } cacheGet(key) { if (this.cache) { return this.cache.get(key); } else { return void 0; } } cacheSet(key, value) { if (this.cache) { this.cache.set(key, value); } return value; } /** * Remove a value by key from cache. * * @returns `true` if the entry existed and has been removed. */ cacheRemove(key) { if (this.cache) { return this.cache.delete(key); } else { return false; } } /** * Check if key exists in cache. */ cacheExists(key) { return Boolean(this.cache?.has(key)); } /** * Get the text (recursive) from all child nodes. */ get textContent() { const cached = this.cacheGet(TEXT_CONTENT); if (cached) { return cached; } const text = this.childNodes.map((node) => node.textContent).join(""); this.cacheSet(TEXT_CONTENT, text); return text; } append(node) { const oldParent = node._setParent(this); if (oldParent && this.isSameNode(oldParent)) { return; } this.childNodes.push(node); if (oldParent) { oldParent._removeChild(node); } } /** * Insert a node before a reference node. * * @internal */ insertBefore(node, reference) { const index = reference ? this.childNodes.findIndex((it) => it.isSameNode(reference)) : -1; if (index >= 0) { this.childNodes.splice(index, 0, node); } else { this.childNodes.push(node); } const oldParent = node._setParent(this); if (oldParent) { oldParent._removeChild(node); } } isRootElement() { return this.nodeType === NodeType.DOCUMENT_NODE; } /** * Tests if two nodes are the same (references the same object). * * @since v4.11.0 */ isSameNode(otherNode) { return this.unique === otherNode.unique; } /** * Returns a DOMNode representing the first direct child node or `null` if the * node has no children. */ get firstChild() { return this.childNodes[0] || null; } /** * Returns a DOMNode representing the last direct child node or `null` if the * node has no children. */ get lastChild() { return this.childNodes[this.childNodes.length - 1] || null; } /** * @internal */ removeChild(node) { this._removeChild(node); node._setParent(null); return node; } /** * Block a rule for this node. * * @internal */ blockRule(ruleId, blocker) { const current = this.blockedRules.get(ruleId); if (current) { current.push(blocker); } else { this.blockedRules.set(ruleId, [blocker]); } } /** * Blocks multiple rules. * * @internal */ blockRules(rules, blocker) { for (const rule of rules) { this.blockRule(rule, blocker); } } /** * Disable a rule for this node. * * @internal */ disableRule(ruleId) { this.disabledRules.add(ruleId); } /** * Disables multiple rules. * * @internal */ disableRules(rules) { for (const rule of rules) { this.disableRule(rule); } } /** * Enable a previously disabled rule for this node. */ enableRule(ruleId) { this.disabledRules.delete(ruleId); } /** * Enables multiple rules. */ enableRules(rules) { for (const rule of rules) { this.enableRule(rule); } } /** * Test if a rule is enabled for this node. * * @internal */ ruleEnabled(ruleId) { return !this.disabledRules.has(ruleId); } /** * Test if a rule is blocked for this node. * * @internal */ ruleBlockers(ruleId) { return this.blockedRules.get(ruleId) ?? []; } generateSelector() { return null; } /** * @internal * * @returns Old parent, if set. */ _setParent(_node) { return null; } _removeChild(node) { const index = this.childNodes.findIndex((it) => it.isSameNode(node)); if (index >= 0) { this.childNodes.splice(index, 1); } else { throw new Error("DOMException: _removeChild(..) could not find child to remove"); } } } function parse(text, baseLocation) { const tokens = []; const locations = baseLocation ? [] : null; for (let begin = 0; begin < text.length; ) { let end = text.indexOf(" ", begin); if (end === -1) { end = text.length; } const size = end - begin; if (size === 0) { begin++; continue; } const token = text.substring(begin, end); tokens.push(token); if (locations && baseLocation) { const location = sliceLocation(baseLocation, begin, end); locations.push(location); } begin += size + 1; } return { tokens, locations }; } class DOMTokenList extends Array { value; locations; constructor(value, location) { if (value && typeof value === "string") { const normalized = value.replace(/[\t\r\n]/g, " "); const { tokens, locations } = parse(normalized, location); super(...tokens); this.locations = locations; } else { super(0); this.locations = null; } if (value instanceof DynamicValue) { this.value = value.expr; } else { this.value = value ?? ""; } } item(n) { return this[n]; } location(n) { if (this.locations) { return this.locations[n]; } else { throw new Error("Trying to access DOMTokenList location when base location isn't set"); } } contains(token) { return this.includes(token); } *iterator() { for (let index = 0; index < this.length; index++) { const item = this.item(index); const location = this.location(index); yield { index, item, location }; } } } var Combinator = /* @__PURE__ */ ((Combinator2) => { Combinator2[Combinator2["DESCENDANT"] = 1] = "DESCENDANT"; Combinator2[Combinator2["CHILD"] = 2] = "CHILD"; Combinator2[Combinator2["ADJACENT_SIBLING"] = 3] = "ADJACENT_SIBLING"; Combinator2[Combinator2["GENERAL_SIBLING"] = 4] = "GENERAL_SIBLING"; Combinator2[Combinator2["SCOPE"] = 5] = "SCOPE"; return Combinator2; })(Combinator || {}); function parseCombinator(combinator, pattern) { if (pattern === ":scope") { return 5 /* SCOPE */; } switch (combinator) { case void 0: case null: case "": return 1 /* DESCENDANT */; case ">": return 2 /* CHILD */; case "+": return 3 /* ADJACENT_SIBLING */; case "~": return 4 /* GENERAL_SIBLING */; default: throw new Error(`Unknown combinator "${combinator}"`); } } function firstChild(node) { return node.previousSibling === null; } function lastChild(node) { return node.nextSibling === null; } const cache = {}; function getNthChild(node) { if (!node.parent) { return -1; } if (!cache[node.unique]) { const parent = node.parent; const index = parent.childElements.findIndex((cur) => { return cur.unique === node.unique; }); cache[node.unique] = index + 1; } return cache[node.unique]; } function nthChild(node, args) { if (!args) { throw new Error("Missing argument to nth-child"); } const n = parseInt(args.trim(), 10); const cur = getNthChild(node); return cur === n; } function scope$1(node) { return Boolean(this.scope && node.isSameNode(this.scope)); } const table = { "first-child": firstChild, "last-child": lastChild, "nth-child": nthChild, scope: scope$1 }; function factory(name, context) { const fn = table[name]; if (fn) { return fn.bind(context); } else { throw new Error(`Pseudo-class "${name}" is not implemented`); } } function stripslashes(value) { return value.replace(/\\(.)/g, "$1"); } class Condition { } class ClassCondition extends Condition { classname; constructor(classname) { super(); this.classname = classname; } match(node) { return node.classList.contains(this.classname); } } class IdCondition extends Condition { id; constructor(id) { super(); this.id = stripslashes(id); } match(node) { return node.id === this.id; } } class AttributeCondition extends Condition { key; op; value; constructor(attr) { super(); const [, key, op, value] = /^(.+?)(?:([~^$*|]?=)"([^"]+?)")?$/.exec(attr); this.key = key; this.op = op; this.value = typeof value === "string" ? stripslashes(value) : value; } match(node) { const attr = node.getAttribute(this.key, true); return attr.some((cur) => { switch (this.op) { case void 0: return true; /* attribute exists */ case "=": return cur.value === this.value; default: throw new Error(`Attribute selector operator ${this.op} is not implemented yet`); } }); } } class PseudoClassCondition extends Condition { name; args; constructor(pseudoclass, context) { super(); const match = /^([^(]+)(?:\((.*)\))?$/.exec(pseudoclass); if (!match) { throw new Error(`Missing pseudo-class after colon in selector pattern "${context}"`); } const [, name, args] = match; this.name = name; this.args = args; } match(node, context) { const fn = factory(this.name, context); return fn(node, this.args); } } function isDelimiter(ch) { return /[.#[:]/.test(ch); } function isQuotationMark(ch) { return /['"]/.test(ch); } function isPseudoElement(ch, buffer) { return ch === ":" && buffer === ":"; } function* splitCompound(pattern) { if (pattern === "") { return; } const end = pattern.length; let begin = 0; let cur = 1; let quoted = false; while (cur < end) { const ch = pattern[cur]; const buffer = pattern.slice(begin, cur); if (ch === "\\") { cur += 2; continue; } if (quoted) { if (ch === quoted) { quoted = false; } cur += 1; continue; } if (isQuotationMark(ch)) { quoted = ch; cur += 1; continue; } if (isPseudoElement(ch, buffer)) { cur += 1; continue; } if (isDelimiter(ch)) { begin = cur; yield buffer; } cur += 1; } const tail = pattern.slice(begin, cur); yield tail; } class Compound { combinator; tagName; selector; conditions; constructor(pattern) { const match = /^([~+\->]?)((?:[*]|[^.#[:]+)?)([^]*)$/.exec(pattern); if (!match) { throw new Error(`Failed to create selector pattern from "${pattern}"`); } match.shift(); this.selector = pattern; this.combinator = parseCombinator(match.shift(), pattern); this.tagName = match.shift() || "*"; this.conditions = Array.from(splitCompound(match[0]), (it) => this.createCondition(it)); } match(node, context) { return node.is(this.tagName) && this.conditions.every((cur) => cur.match(node, context)); } createCondition(pattern) { switch (pattern[0]) { case ".": re