html-validate
Version:
Offline HTML5 validator and linter
2,125 lines (2,064 loc) • 396 kB
JavaScript
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