html-validate 10.14.0 → 10.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cjs/core.js CHANGED
@@ -1204,9 +1204,6 @@ function clone(value) {
1204
1204
  return JSON.parse(JSON.stringify(value));
1205
1205
  }
1206
1206
  }
1207
- function overwriteMerge$1(_a, b) {
1208
- return b;
1209
- }
1210
1207
  class MetaTable {
1211
1208
  elements;
1212
1209
  schema;
@@ -1379,15 +1376,22 @@ class MetaTable {
1379
1376
  }
1380
1377
  }
1381
1378
  mergeElement(a, b) {
1382
- const merged = deepmerge(a, b, { arrayMerge: overwriteMerge$1 });
1383
- const filteredAttrs = Object.entries(
1384
- merged.attributes
1385
- ).filter(([, attr]) => {
1386
- const val = !attr.delete;
1387
- delete attr.delete;
1388
- return val;
1389
- });
1390
- merged.attributes = Object.fromEntries(filteredAttrs);
1379
+ const merged = { ...a, ...b };
1380
+ const mergedAttrs = {
1381
+ ...a.attributes,
1382
+ ...b.attributes
1383
+ };
1384
+ for (const [name, attr] of Object.entries(mergedAttrs)) {
1385
+ if (attr.delete) {
1386
+ delete mergedAttrs[name];
1387
+ } else {
1388
+ delete attr.delete;
1389
+ }
1390
+ }
1391
+ merged.attributes = mergedAttrs;
1392
+ if (a.aria) {
1393
+ merged.aria = { ...a.aria, ...b.aria };
1394
+ }
1391
1395
  return merged;
1392
1396
  }
1393
1397
  /**
@@ -1968,71 +1972,68 @@ function factory(name, context) {
1968
1972
  function stripslashes(value) {
1969
1973
  return value.replaceAll(/\\(.)/g, "$1");
1970
1974
  }
1971
- class Condition {
1975
+ function createClassCondition(classname) {
1976
+ return {
1977
+ kind: "class",
1978
+ classname,
1979
+ match(node) {
1980
+ return node.classList.contains(classname);
1981
+ }
1982
+ };
1972
1983
  }
1973
- class ClassCondition extends Condition {
1974
- classname;
1975
- constructor(classname) {
1976
- super();
1977
- this.classname = classname;
1978
- }
1979
- match(node) {
1980
- return node.classList.contains(this.classname);
1981
- }
1984
+ function createIdCondition(raw) {
1985
+ const id = stripslashes(raw);
1986
+ return {
1987
+ kind: "id",
1988
+ id,
1989
+ match(node) {
1990
+ return node.id === id;
1991
+ }
1992
+ };
1982
1993
  }
1983
- class IdCondition extends Condition {
1984
- id;
1985
- constructor(id) {
1986
- super();
1987
- this.id = stripslashes(id);
1988
- }
1989
- match(node) {
1990
- return node.id === this.id;
1991
- }
1994
+ function createAttributeCondition(attr) {
1995
+ const match = /^(.+?)(?:([$*^|~]?=)"([^"]+?)")?$/.exec(attr);
1996
+ const key = match[1];
1997
+ const op = match[2];
1998
+ const rawValue = match[3];
1999
+ const value = typeof rawValue === "string" ? stripslashes(rawValue) : rawValue;
2000
+ return {
2001
+ kind: "attribute",
2002
+ key,
2003
+ op,
2004
+ value,
2005
+ match(node) {
2006
+ const attrs = node.getAttribute(key, true);
2007
+ return attrs.some((cur) => {
2008
+ switch (op) {
2009
+ case void 0:
2010
+ return true;
2011
+ /* attribute exists */
2012
+ case "=":
2013
+ return cur.value === value;
2014
+ default:
2015
+ throw new Error(`Attribute selector operator ${op} is not implemented yet`);
2016
+ }
2017
+ });
2018
+ }
2019
+ };
1992
2020
  }
1993
- class AttributeCondition extends Condition {
1994
- key;
1995
- op;
1996
- value;
1997
- constructor(attr) {
1998
- super();
1999
- const [, key, op, value] = /^(.+?)(?:([$*^|~]?=)"([^"]+?)")?$/.exec(attr);
2000
- this.key = key;
2001
- this.op = op;
2002
- this.value = typeof value === "string" ? stripslashes(value) : value;
2003
- }
2004
- match(node) {
2005
- const attr = node.getAttribute(this.key, true);
2006
- return attr.some((cur) => {
2007
- switch (this.op) {
2008
- case void 0:
2009
- return true;
2010
- /* attribute exists */
2011
- case "=":
2012
- return cur.value === this.value;
2013
- default:
2014
- throw new Error(`Attribute selector operator ${this.op} is not implemented yet`);
2015
- }
2016
- });
2021
+ function createPseudoClassCondition(pseudoclass, context) {
2022
+ const match = /^([^(]+)(?:\((.*)\))?$/.exec(pseudoclass);
2023
+ if (!match) {
2024
+ throw new Error(`Missing pseudo-class after colon in selector pattern "${context}"`);
2017
2025
  }
2018
- }
2019
- class PseudoClassCondition extends Condition {
2020
- name;
2021
- args;
2022
- constructor(pseudoclass, context) {
2023
- super();
2024
- const match = /^([^(]+)(?:\((.*)\))?$/.exec(pseudoclass);
2025
- if (!match) {
2026
- throw new Error(`Missing pseudo-class after colon in selector pattern "${context}"`);
2026
+ const name = match[1];
2027
+ const args = match[2];
2028
+ return {
2029
+ kind: "pseudo",
2030
+ name,
2031
+ args,
2032
+ match(node, selectorContext) {
2033
+ const fn = factory(name, selectorContext);
2034
+ return fn(node, args);
2027
2035
  }
2028
- const [, name, args] = match;
2029
- this.name = name;
2030
- this.args = args;
2031
- }
2032
- match(node, context) {
2033
- const fn = factory(this.name, context);
2034
- return fn(node, this.args);
2035
- }
2036
+ };
2036
2037
  }
2037
2038
 
2038
2039
  function isDelimiter(ch) {
@@ -2107,37 +2108,84 @@ class Compound {
2107
2108
  createCondition(pattern) {
2108
2109
  switch (pattern[0]) {
2109
2110
  case ".":
2110
- return new ClassCondition(pattern.slice(1));
2111
+ return createClassCondition(pattern.slice(1));
2111
2112
  case "#":
2112
- return new IdCondition(pattern.slice(1));
2113
+ return createIdCondition(pattern.slice(1));
2113
2114
  case "[":
2114
- return new AttributeCondition(pattern.slice(1, -1));
2115
+ return createAttributeCondition(pattern.slice(1, -1));
2115
2116
  case ":":
2116
- return new PseudoClassCondition(pattern.slice(1), this.selector);
2117
+ return createPseudoClassCondition(pattern.slice(1), this.selector);
2117
2118
  default:
2118
2119
  throw new Error(`Failed to create selector condition for "${pattern}"`);
2119
2120
  }
2120
2121
  }
2121
2122
  }
2122
2123
 
2123
- const codepoints = {
2124
- " ": "\\9 ",
2125
- "\n": "\\a ",
2126
- "\r": "\\d "
2127
- };
2128
- function escapeSelectorComponent(text) {
2129
- return text.toString().replaceAll(/([\t\n\r]|[^\w-])/gi, (_, ch) => {
2130
- if (codepoints[ch]) {
2131
- return codepoints[ch];
2132
- } else {
2133
- return `\\${ch}`;
2124
+ const escapedCodepoints = /* @__PURE__ */ new Set(["9", "a", "d"]);
2125
+ function* splitSelectorElements(selector) {
2126
+ let begin = 0;
2127
+ let end = 0;
2128
+ function initialState(ch, p) {
2129
+ if (ch === "\\") {
2130
+ return 1 /* ESCAPED */;
2134
2131
  }
2135
- });
2132
+ if (ch === " ") {
2133
+ end = p;
2134
+ return 2 /* WHITESPACE */;
2135
+ }
2136
+ return 0 /* INITIAL */;
2137
+ }
2138
+ function escapedState(ch) {
2139
+ if (escapedCodepoints.has(ch)) {
2140
+ return 1 /* ESCAPED */;
2141
+ }
2142
+ return 0 /* INITIAL */;
2143
+ }
2144
+ function* whitespaceState(ch, p) {
2145
+ if (ch === " ") {
2146
+ return 2 /* WHITESPACE */;
2147
+ }
2148
+ yield selector.slice(begin, end);
2149
+ begin = p;
2150
+ end = p;
2151
+ return 0 /* INITIAL */;
2152
+ }
2153
+ let state = 0 /* INITIAL */;
2154
+ for (let p = 0; p < selector.length; p++) {
2155
+ const ch = selector[p];
2156
+ switch (state) {
2157
+ case 0 /* INITIAL */:
2158
+ state = initialState(ch, p);
2159
+ break;
2160
+ case 1 /* ESCAPED */:
2161
+ state = escapedState(ch);
2162
+ break;
2163
+ case 2 /* WHITESPACE */:
2164
+ state = yield* whitespaceState(ch, p);
2165
+ break;
2166
+ }
2167
+ }
2168
+ if (begin !== selector.length) {
2169
+ yield selector.slice(begin);
2170
+ }
2136
2171
  }
2137
2172
 
2138
- function generateIdSelector(id) {
2139
- const escaped = escapeSelectorComponent(id);
2140
- return /^\d/.test(escaped) ? `[id="${escaped}"]` : `#${escaped}`;
2173
+ function unescapeCodepoint(value) {
2174
+ const replacement = {
2175
+ "\\9 ": " ",
2176
+ "\\a ": "\n",
2177
+ "\\d ": "\r"
2178
+ };
2179
+ return value.replaceAll(
2180
+ /(\\[9ad] )/g,
2181
+ (_, codepoint) => replacement[codepoint]
2182
+ );
2183
+ }
2184
+ function getCompounds(selector) {
2185
+ selector = selector.replaceAll(/([+>~]) /g, "$1");
2186
+ return Array.from(splitSelectorElements(selector), (element) => {
2187
+ return new Compound(unescapeCodepoint(element));
2188
+ });
2141
2189
  }
2142
2190
 
2143
2191
  function* ancestors$1(element) {
@@ -2202,70 +2250,16 @@ function matchElement(element, compounds, context) {
2202
2250
  return false;
2203
2251
  }
2204
2252
 
2205
- const escapedCodepoints = /* @__PURE__ */ new Set(["9", "a", "d"]);
2206
- function* splitSelectorElements(selector) {
2207
- let begin = 0;
2208
- let end = 0;
2209
- function initialState(ch, p) {
2210
- if (ch === "\\") {
2211
- return 1 /* ESCAPED */;
2212
- }
2213
- if (ch === " ") {
2214
- end = p;
2215
- return 2 /* WHITESPACE */;
2216
- }
2217
- return 0 /* INITIAL */;
2253
+ class ComplexSelector {
2254
+ compounds;
2255
+ constructor(compounds) {
2256
+ this.compounds = compounds;
2218
2257
  }
2219
- function escapedState(ch) {
2220
- if (escapedCodepoints.has(ch)) {
2221
- return 1 /* ESCAPED */;
2222
- }
2223
- return 0 /* INITIAL */;
2258
+ static fromString(selector) {
2259
+ return new ComplexSelector(getCompounds(selector));
2224
2260
  }
2225
- function* whitespaceState(ch, p) {
2226
- if (ch === " ") {
2227
- return 2 /* WHITESPACE */;
2228
- }
2229
- yield selector.slice(begin, end);
2230
- begin = p;
2231
- end = p;
2232
- return 0 /* INITIAL */;
2233
- }
2234
- let state = 0 /* INITIAL */;
2235
- for (let p = 0; p < selector.length; p++) {
2236
- const ch = selector[p];
2237
- switch (state) {
2238
- case 0 /* INITIAL */:
2239
- state = initialState(ch, p);
2240
- break;
2241
- case 1 /* ESCAPED */:
2242
- state = escapedState(ch);
2243
- break;
2244
- case 2 /* WHITESPACE */:
2245
- state = yield* whitespaceState(ch, p);
2246
- break;
2247
- }
2248
- }
2249
- if (begin !== selector.length) {
2250
- yield selector.slice(begin);
2251
- }
2252
- }
2253
-
2254
- function unescapeCodepoint(value) {
2255
- const replacement = {
2256
- "\\9 ": " ",
2257
- "\\a ": "\n",
2258
- "\\d ": "\r"
2259
- };
2260
- return value.replaceAll(
2261
- /(\\[9ad] )/g,
2262
- (_, codepoint) => replacement[codepoint]
2263
- );
2264
- }
2265
- class Selector {
2266
- pattern;
2267
- constructor(selector) {
2268
- this.pattern = Selector.parse(selector);
2261
+ static fromCompounds(compounds) {
2262
+ return new ComplexSelector(compounds);
2269
2263
  }
2270
2264
  /**
2271
2265
  * Match this selector against a HtmlElement.
@@ -2282,15 +2276,15 @@ class Selector {
2282
2276
  */
2283
2277
  matchElement(element) {
2284
2278
  const context = { scope: null };
2285
- return matchElement(element, this.pattern, context);
2279
+ return matchElement(element, this.compounds, context);
2286
2280
  }
2287
2281
  *matchInternal(root, level, context) {
2288
- if (level >= this.pattern.length) {
2282
+ if (level >= this.compounds.length) {
2289
2283
  yield root;
2290
2284
  return;
2291
2285
  }
2292
- const pattern = this.pattern[level];
2293
- const matches = Selector.findCandidates(root, pattern);
2286
+ const pattern = this.compounds[level];
2287
+ const matches = ComplexSelector.findCandidates(root, pattern);
2294
2288
  for (const node of matches) {
2295
2289
  if (!pattern.match(node, context)) {
2296
2290
  continue;
@@ -2298,12 +2292,6 @@ class Selector {
2298
2292
  yield* this.matchInternal(node, level + 1, context);
2299
2293
  }
2300
2294
  }
2301
- static parse(selector) {
2302
- selector = selector.replaceAll(/([+>~]) /g, "$1");
2303
- return Array.from(splitSelectorElements(selector), (element) => {
2304
- return new Compound(unescapeCodepoint(element));
2305
- });
2306
- }
2307
2295
  static findCandidates(root, pattern) {
2308
2296
  switch (pattern.combinator) {
2309
2297
  case Combinator.DESCENDANT:
@@ -2311,9 +2299,9 @@ class Selector {
2311
2299
  case Combinator.CHILD:
2312
2300
  return root.childElements.filter((node) => node.is(pattern.tagName));
2313
2301
  case Combinator.ADJACENT_SIBLING:
2314
- return Selector.findAdjacentSibling(root);
2302
+ return ComplexSelector.findAdjacentSibling(root);
2315
2303
  case Combinator.GENERAL_SIBLING:
2316
- return Selector.findGeneralSibling(root);
2304
+ return ComplexSelector.findGeneralSibling(root);
2317
2305
  case Combinator.SCOPE:
2318
2306
  return [root];
2319
2307
  }
@@ -2325,7 +2313,7 @@ class Selector {
2325
2313
  adjacent = false;
2326
2314
  return true;
2327
2315
  }
2328
- if (cur === node) {
2316
+ if (cur.isSameNode(node)) {
2329
2317
  adjacent = true;
2330
2318
  }
2331
2319
  return false;
@@ -2337,7 +2325,7 @@ class Selector {
2337
2325
  if (after) {
2338
2326
  return true;
2339
2327
  }
2340
- if (cur === node) {
2328
+ if (cur.isSameNode(node)) {
2341
2329
  after = true;
2342
2330
  }
2343
2331
  return false;
@@ -2345,6 +2333,31 @@ class Selector {
2345
2333
  }
2346
2334
  }
2347
2335
 
2336
+ const codepoints = {
2337
+ " ": "\\9 ",
2338
+ "\n": "\\a ",
2339
+ "\r": "\\d "
2340
+ };
2341
+ function escapeSelectorComponent(text) {
2342
+ return text.toString().replaceAll(/([\t\n\r]|[^\w-])/gi, (_, ch) => {
2343
+ if (codepoints[ch]) {
2344
+ return codepoints[ch];
2345
+ } else {
2346
+ return `\\${ch}`;
2347
+ }
2348
+ });
2349
+ }
2350
+
2351
+ function generateIdSelector(id) {
2352
+ const escaped = escapeSelectorComponent(id);
2353
+ return /^\d/.test(escaped) ? `[id="${escaped}"]` : `#${escaped}`;
2354
+ }
2355
+
2356
+ function parseSelector(selector) {
2357
+ const compounds = getCompounds(selector);
2358
+ return ComplexSelector.fromCompounds(compounds);
2359
+ }
2360
+
2348
2361
  const TEXT_NODE_NAME = "#text";
2349
2362
  function isTextNode(node) {
2350
2363
  return node?.nodeType === NodeType.TEXT_NODE;
@@ -2658,7 +2671,7 @@ class HtmlElement extends DOMNode {
2658
2671
  */
2659
2672
  matches(selectorList) {
2660
2673
  return selectorList.split(",").some((it) => {
2661
- const selector = new Selector(it.trim());
2674
+ const selector = parseSelector(it.trim());
2662
2675
  return selector.matchElement(this);
2663
2676
  });
2664
2677
  }
@@ -2891,9 +2904,9 @@ class HtmlElement extends DOMNode {
2891
2904
  if (!selectorList) {
2892
2905
  return;
2893
2906
  }
2894
- for (const selector of selectorList.split(/(?<!\\),\s*/)) {
2895
- const pattern = new Selector(selector);
2896
- yield* pattern.match(this);
2907
+ for (const selectorString of selectorList.split(/(?<!\\),\s*/)) {
2908
+ const selector = parseSelector(selectorString);
2909
+ yield* selector.match(this);
2897
2910
  }
2898
2911
  }
2899
2912
  /**
@@ -3746,6 +3759,7 @@ class Rule {
3746
3759
  severity;
3747
3760
  // rule severity
3748
3761
  event;
3762
+ tracker;
3749
3763
  /**
3750
3764
  * Rule name. Defaults to filename without extension but can be overwritten by
3751
3765
  * subclasses.
@@ -3765,6 +3779,7 @@ class Rule {
3765
3779
  this.blockers = [];
3766
3780
  this.severity = Severity.DISABLED;
3767
3781
  this.name = "";
3782
+ this.tracker = null;
3768
3783
  }
3769
3784
  getSeverity() {
3770
3785
  return this.severity;
@@ -3940,7 +3955,15 @@ class Rule {
3940
3955
  return this.parser.on(event, (_event, data) => {
3941
3956
  if (this.isEnabled() && filter(data)) {
3942
3957
  this.event = data;
3943
- callback(data);
3958
+ const { tracker } = this;
3959
+ if (tracker) {
3960
+ const start = performance.now();
3961
+ callback(data);
3962
+ const end = performance.now();
3963
+ tracker.trackRule(this.name, end - start);
3964
+ } else {
3965
+ callback(data);
3966
+ }
3944
3967
  }
3945
3968
  });
3946
3969
  }
@@ -3957,6 +3980,14 @@ class Rule {
3957
3980
  this.severity = severity;
3958
3981
  this.meta = meta;
3959
3982
  }
3983
+ /**
3984
+ * Set (or clear) the performance tracker.
3985
+ *
3986
+ * @internal
3987
+ */
3988
+ setTracker(tracker) {
3989
+ this.tracker = tracker;
3990
+ }
3960
3991
  /**
3961
3992
  * Validate rule options against schema. Throws error if object does not validate.
3962
3993
  *
@@ -4339,24 +4370,27 @@ const defaults$A = {
4339
4370
  }
4340
4371
  };
4341
4372
  const allowlist = /* @__PURE__ */ new Set([
4342
- "main",
4343
- "nav",
4344
- "table",
4345
- "td",
4346
- "th",
4373
+ /* landmark elements */
4374
+ "article",
4347
4375
  "aside",
4348
- "header",
4349
4376
  "footer",
4377
+ "form",
4378
+ "header",
4379
+ "main",
4380
+ "nav",
4381
+ "search",
4350
4382
  "section",
4351
- "article",
4383
+ /* other allowed elements */
4384
+ "area",
4352
4385
  "dialog",
4353
- "form",
4386
+ "fieldset",
4387
+ "figure",
4354
4388
  "iframe",
4355
4389
  "img",
4356
- "area",
4357
- "fieldset",
4358
4390
  "summary",
4359
- "figure"
4391
+ "table",
4392
+ "td",
4393
+ "th"
4360
4394
  ]);
4361
4395
  function isValidUsage(target, meta) {
4362
4396
  const explicit = meta.attributes["aria-label"];
@@ -7686,7 +7720,7 @@ class InputMissingLabel extends Rule {
7686
7720
  if (hasAccessibleName(root, elem)) {
7687
7721
  return;
7688
7722
  }
7689
- let label = [];
7723
+ let label;
7690
7724
  if ((label = findLabelById(root, elem.id)).length > 0) {
7691
7725
  this.validateLabel(root, elem, label);
7692
7726
  return;
@@ -8400,7 +8434,8 @@ class NoImplicitInputType extends Rule {
8400
8434
  const defaults$g = {
8401
8435
  include: null,
8402
8436
  exclude: null,
8403
- allowedProperties: ["display"]
8437
+ allowedProperties: ["display"],
8438
+ allowVariables: true
8404
8439
  };
8405
8440
  class NoInlineStyle extends Rule {
8406
8441
  constructor(options) {
@@ -8439,17 +8474,26 @@ class NoInlineStyle extends Rule {
8439
8474
  type: "string"
8440
8475
  },
8441
8476
  type: "array"
8477
+ },
8478
+ allowVariables: {
8479
+ type: "boolean"
8442
8480
  }
8443
8481
  };
8444
8482
  }
8445
8483
  documentation() {
8484
+ const { allowVariables, allowedProperties } = this.options;
8446
8485
  const text = [
8447
8486
  "Inline style is not allowed.\n",
8448
8487
  "Inline style is a sign of unstructured CSS. Use class or ID with a separate stylesheet.\n"
8449
8488
  ];
8450
- if (this.options.allowedProperties.length > 0) {
8489
+ if (allowedProperties.length > 0 || allowVariables) {
8451
8490
  text.push("Under the current configuration the following CSS properties are allowed:\n");
8452
- text.push(this.options.allowedProperties.map((it) => `- \`${it}\``).join("\n"));
8491
+ }
8492
+ if (allowedProperties.length > 0) {
8493
+ text.push(allowedProperties.map((it) => `- \`${it}\``).join("\n"));
8494
+ }
8495
+ if (allowVariables) {
8496
+ text.push("- CSS variables (custom properties starting with `--`).\n");
8453
8497
  }
8454
8498
  return {
8455
8499
  description: text.join("\n"),
@@ -8484,13 +8528,16 @@ class NoInlineStyle extends Rule {
8484
8528
  return true;
8485
8529
  }
8486
8530
  allPropertiesAllowed(value) {
8487
- const allowProperties = this.options.allowedProperties;
8488
- if (allowProperties.length === 0) {
8531
+ const { allowedProperties, allowVariables } = this.options;
8532
+ if (allowedProperties.length === 0 && !allowVariables) {
8489
8533
  return false;
8490
8534
  }
8491
8535
  const declarations = Object.keys(parseCssDeclaration(value));
8492
8536
  return declarations.length > 0 && declarations.every((it) => {
8493
- return allowProperties.includes(it);
8537
+ if (allowVariables && it.startsWith("--")) {
8538
+ return true;
8539
+ }
8540
+ return allowedProperties.includes(it);
8494
8541
  });
8495
8542
  }
8496
8543
  }
@@ -8593,7 +8640,7 @@ class NoMultipleMain extends Rule {
8593
8640
  const defaults$f = {
8594
8641
  relaxed: false
8595
8642
  };
8596
- const textRegexp = /([<>]|&(?![\d#A-Za-z]+;))/g;
8643
+ const textRegexp = /(<|&(?![\d#A-Za-z]+;))/g;
8597
8644
  const unquotedAttrRegexp = /(["'<=>`]|&(?![\d#A-Za-z]+;))/g;
8598
8645
  const matchTemplate = /^(<%.*?%>|<\?.*?\?>|<\$.*?\$>)$/s;
8599
8646
  const replacementTable = {
@@ -8620,7 +8667,7 @@ class NoRawCharacters extends Rule {
8620
8667
  }
8621
8668
  documentation() {
8622
8669
  return {
8623
- description: `Some characters such as \`<\`, \`>\` and \`&\` hold special meaning in HTML and must be escaped using a character reference (html entity).`,
8670
+ description: `Some characters such as \`<\` and \`&\` hold special meaning in HTML and must be escaped using a character reference (HTML entity).`,
8624
8671
  url: "https://html-validate.org/rules/no-raw-characters.html"
8625
8672
  };
8626
8673
  }
@@ -10035,14 +10082,17 @@ class UnknownCharReference extends Rule {
10035
10082
  if (found && (terminated || !requireSemicolon)) {
10036
10083
  return;
10037
10084
  }
10038
- if (found && !terminated) {
10039
- const entityLocation2 = getLocation(location, entity, match);
10040
- const message2 = `Character reference "{{ entity }}" must be terminated by a semicolon`;
10041
- const context2 = {
10042
- entity: raw,
10043
- terminated: false
10044
- };
10045
- this.report(node, message2, entityLocation2, context2);
10085
+ if (!terminated) {
10086
+ const isKnownName = found || this.entities.includes(`${entity};`);
10087
+ if (isKnownName) {
10088
+ const entityLocation2 = getLocation(location, entity, match);
10089
+ const message2 = `Character reference "{{ entity }}" must be terminated by a semicolon`;
10090
+ const context2 = {
10091
+ entity: raw,
10092
+ terminated: false
10093
+ };
10094
+ this.report(node, message2, entityLocation2, context2);
10095
+ }
10046
10096
  return;
10047
10097
  }
10048
10098
  const entityLocation = getLocation(location, entity, match);
@@ -12471,8 +12521,10 @@ class StaticConfigLoader extends ConfigLoader {
12471
12521
 
12472
12522
  class EventHandler {
12473
12523
  listeners;
12524
+ tracker;
12474
12525
  constructor() {
12475
12526
  this.listeners = {};
12527
+ this.tracker = null;
12476
12528
  }
12477
12529
  /**
12478
12530
  * Add an event listener.
@@ -12511,6 +12563,14 @@ class EventHandler {
12511
12563
  });
12512
12564
  return deregister;
12513
12565
  }
12566
+ /**
12567
+ * Set (or clear) the performance tracker.
12568
+ *
12569
+ * @internal
12570
+ */
12571
+ setTracker(tracker) {
12572
+ this.tracker = tracker;
12573
+ }
12514
12574
  /**
12515
12575
  * Trigger event causing all listeners to be called.
12516
12576
  *
@@ -12519,8 +12579,18 @@ class EventHandler {
12519
12579
  */
12520
12580
  /* eslint-disable-next-line @typescript-eslint/no-explicit-any -- technical debt, should be made typesafe */
12521
12581
  trigger(event, data) {
12522
- for (const listener of this.getCallbacks(event)) {
12523
- listener.call(null, event, data);
12582
+ const { tracker } = this;
12583
+ if (tracker) {
12584
+ const start = performance.now();
12585
+ for (const listener of this.getCallbacks(event)) {
12586
+ listener.call(null, event, data);
12587
+ }
12588
+ const end = performance.now();
12589
+ tracker.trackEvent(event, end - start);
12590
+ } else {
12591
+ for (const listener of this.getCallbacks(event)) {
12592
+ listener.call(null, event, data);
12593
+ }
12524
12594
  }
12525
12595
  }
12526
12596
  getCallbacks(event) {
@@ -12532,7 +12602,7 @@ class EventHandler {
12532
12602
  }
12533
12603
 
12534
12604
  const name = "html-validate";
12535
- const version = "10.14.0";
12605
+ const version = "10.16.0";
12536
12606
  const bugs = "https://gitlab.com/html-validate/html-validate/issues/new";
12537
12607
 
12538
12608
  function freeze(src) {
@@ -13466,6 +13536,78 @@ class Parser {
13466
13536
  }
13467
13537
  }
13468
13538
 
13539
+ class PerformanceTracker {
13540
+ eventData;
13541
+ ruleData;
13542
+ startTime;
13543
+ accConfigTime;
13544
+ accTransformTime;
13545
+ constructor() {
13546
+ this.eventData = /* @__PURE__ */ new Map();
13547
+ this.ruleData = /* @__PURE__ */ new Map();
13548
+ this.startTime = performance.now();
13549
+ this.accConfigTime = 0;
13550
+ this.accTransformTime = 0;
13551
+ }
13552
+ /**
13553
+ * Record a single event trigger with the time it took to run all listeners.
13554
+ */
13555
+ trackEvent(name, time) {
13556
+ const existing = this.eventData.get(name);
13557
+ if (existing) {
13558
+ existing.count += 1;
13559
+ existing.time += time;
13560
+ } else {
13561
+ this.eventData.set(name, { count: 1, time });
13562
+ }
13563
+ }
13564
+ /**
13565
+ * Record time spent loading configuration.
13566
+ */
13567
+ trackConfig(time) {
13568
+ this.accConfigTime += time;
13569
+ }
13570
+ /**
13571
+ * Record time spent in transformers.
13572
+ */
13573
+ trackTransform(time) {
13574
+ this.accTransformTime += time;
13575
+ }
13576
+ /**
13577
+ * Record a single rule callback invocation with its execution time.
13578
+ */
13579
+ trackRule(ruleName, time) {
13580
+ const existing = this.ruleData.get(ruleName);
13581
+ if (existing) {
13582
+ existing.count += 1;
13583
+ existing.time += time;
13584
+ } else {
13585
+ this.ruleData.set(ruleName, { count: 1, time });
13586
+ }
13587
+ }
13588
+ /**
13589
+ * Returns a snapshot of the recorded performance data, with both arrays
13590
+ * sorted by time (descending).
13591
+ */
13592
+ getResult() {
13593
+ const events = Array.from(
13594
+ this.eventData.entries(),
13595
+ ([event, { count, time }]) => ({ event, count, time })
13596
+ ).toSorted((a, b) => b.time - a.time);
13597
+ const rules = Array.from(
13598
+ this.ruleData.entries(),
13599
+ ([rule, { count, time }]) => ({ rule, count, time })
13600
+ ).toSorted((a, b) => b.time - a.time);
13601
+ return {
13602
+ events,
13603
+ rules,
13604
+ configTime: this.accConfigTime,
13605
+ transformTime: this.accTransformTime,
13606
+ totalTime: performance.now() - this.startTime
13607
+ };
13608
+ }
13609
+ }
13610
+
13469
13611
  const ruleIds = new Set(Object.keys(bundledRules));
13470
13612
  function ruleExists(ruleId) {
13471
13613
  return ruleIds.has(ruleId);
@@ -13513,10 +13655,12 @@ class Engine {
13513
13655
  config;
13514
13656
  ParserClass;
13515
13657
  availableRules;
13516
- constructor(config, ParserClass) {
13658
+ tracker;
13659
+ constructor(config, ParserClass, options) {
13517
13660
  this.report = new Reporter();
13518
13661
  this.config = config;
13519
13662
  this.ParserClass = ParserClass;
13663
+ this.tracker = options.tracker;
13520
13664
  const result = this.initPlugins(this.config);
13521
13665
  this.availableRules = {
13522
13666
  ...bundledRules,
@@ -13532,6 +13676,9 @@ class Engine {
13532
13676
  lint(sources) {
13533
13677
  for (const source of sources) {
13534
13678
  const parser = this.instantiateParser();
13679
+ if (this.tracker) {
13680
+ parser.getEventHandler().setTracker(this.tracker);
13681
+ }
13535
13682
  const { rules } = this.setupPlugins(source, this.config, parser);
13536
13683
  const noUnusedDisable = rules["no-unused-disable"];
13537
13684
  const directiveContext = {
@@ -13808,6 +13955,7 @@ class Engine {
13808
13955
  const rule = this.instantiateRule(ruleId, options);
13809
13956
  rule.name = ruleId;
13810
13957
  rule.init(parser, report, severity, meta);
13958
+ rule.setTracker(this.tracker);
13811
13959
  if (rule.setup) {
13812
13960
  rule.setup();
13813
13961
  }
@@ -15183,6 +15331,7 @@ exports.NestedError = NestedError;
15183
15331
  exports.NodeClosed = NodeClosed;
15184
15332
  exports.NodeType = NodeType;
15185
15333
  exports.Parser = Parser;
15334
+ exports.PerformanceTracker = PerformanceTracker;
15186
15335
  exports.Reporter = Reporter;
15187
15336
  exports.ResolvedConfig = ResolvedConfig;
15188
15337
  exports.Rule = Rule;