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/esm/core.js CHANGED
@@ -1195,9 +1195,6 @@ function clone(value) {
1195
1195
  return JSON.parse(JSON.stringify(value));
1196
1196
  }
1197
1197
  }
1198
- function overwriteMerge$1(_a, b) {
1199
- return b;
1200
- }
1201
1198
  class MetaTable {
1202
1199
  elements;
1203
1200
  schema;
@@ -1370,15 +1367,22 @@ class MetaTable {
1370
1367
  }
1371
1368
  }
1372
1369
  mergeElement(a, b) {
1373
- const merged = deepmerge(a, b, { arrayMerge: overwriteMerge$1 });
1374
- const filteredAttrs = Object.entries(
1375
- merged.attributes
1376
- ).filter(([, attr]) => {
1377
- const val = !attr.delete;
1378
- delete attr.delete;
1379
- return val;
1380
- });
1381
- merged.attributes = Object.fromEntries(filteredAttrs);
1370
+ const merged = { ...a, ...b };
1371
+ const mergedAttrs = {
1372
+ ...a.attributes,
1373
+ ...b.attributes
1374
+ };
1375
+ for (const [name, attr] of Object.entries(mergedAttrs)) {
1376
+ if (attr.delete) {
1377
+ delete mergedAttrs[name];
1378
+ } else {
1379
+ delete attr.delete;
1380
+ }
1381
+ }
1382
+ merged.attributes = mergedAttrs;
1383
+ if (a.aria) {
1384
+ merged.aria = { ...a.aria, ...b.aria };
1385
+ }
1382
1386
  return merged;
1383
1387
  }
1384
1388
  /**
@@ -1959,71 +1963,68 @@ function factory(name, context) {
1959
1963
  function stripslashes(value) {
1960
1964
  return value.replaceAll(/\\(.)/g, "$1");
1961
1965
  }
1962
- class Condition {
1966
+ function createClassCondition(classname) {
1967
+ return {
1968
+ kind: "class",
1969
+ classname,
1970
+ match(node) {
1971
+ return node.classList.contains(classname);
1972
+ }
1973
+ };
1963
1974
  }
1964
- class ClassCondition extends Condition {
1965
- classname;
1966
- constructor(classname) {
1967
- super();
1968
- this.classname = classname;
1969
- }
1970
- match(node) {
1971
- return node.classList.contains(this.classname);
1972
- }
1975
+ function createIdCondition(raw) {
1976
+ const id = stripslashes(raw);
1977
+ return {
1978
+ kind: "id",
1979
+ id,
1980
+ match(node) {
1981
+ return node.id === id;
1982
+ }
1983
+ };
1973
1984
  }
1974
- class IdCondition extends Condition {
1975
- id;
1976
- constructor(id) {
1977
- super();
1978
- this.id = stripslashes(id);
1979
- }
1980
- match(node) {
1981
- return node.id === this.id;
1982
- }
1985
+ function createAttributeCondition(attr) {
1986
+ const match = /^(.+?)(?:([$*^|~]?=)"([^"]+?)")?$/.exec(attr);
1987
+ const key = match[1];
1988
+ const op = match[2];
1989
+ const rawValue = match[3];
1990
+ const value = typeof rawValue === "string" ? stripslashes(rawValue) : rawValue;
1991
+ return {
1992
+ kind: "attribute",
1993
+ key,
1994
+ op,
1995
+ value,
1996
+ match(node) {
1997
+ const attrs = node.getAttribute(key, true);
1998
+ return attrs.some((cur) => {
1999
+ switch (op) {
2000
+ case void 0:
2001
+ return true;
2002
+ /* attribute exists */
2003
+ case "=":
2004
+ return cur.value === value;
2005
+ default:
2006
+ throw new Error(`Attribute selector operator ${op} is not implemented yet`);
2007
+ }
2008
+ });
2009
+ }
2010
+ };
1983
2011
  }
1984
- class AttributeCondition extends Condition {
1985
- key;
1986
- op;
1987
- value;
1988
- constructor(attr) {
1989
- super();
1990
- const [, key, op, value] = /^(.+?)(?:([$*^|~]?=)"([^"]+?)")?$/.exec(attr);
1991
- this.key = key;
1992
- this.op = op;
1993
- this.value = typeof value === "string" ? stripslashes(value) : value;
1994
- }
1995
- match(node) {
1996
- const attr = node.getAttribute(this.key, true);
1997
- return attr.some((cur) => {
1998
- switch (this.op) {
1999
- case void 0:
2000
- return true;
2001
- /* attribute exists */
2002
- case "=":
2003
- return cur.value === this.value;
2004
- default:
2005
- throw new Error(`Attribute selector operator ${this.op} is not implemented yet`);
2006
- }
2007
- });
2012
+ function createPseudoClassCondition(pseudoclass, context) {
2013
+ const match = /^([^(]+)(?:\((.*)\))?$/.exec(pseudoclass);
2014
+ if (!match) {
2015
+ throw new Error(`Missing pseudo-class after colon in selector pattern "${context}"`);
2008
2016
  }
2009
- }
2010
- class PseudoClassCondition extends Condition {
2011
- name;
2012
- args;
2013
- constructor(pseudoclass, context) {
2014
- super();
2015
- const match = /^([^(]+)(?:\((.*)\))?$/.exec(pseudoclass);
2016
- if (!match) {
2017
- throw new Error(`Missing pseudo-class after colon in selector pattern "${context}"`);
2017
+ const name = match[1];
2018
+ const args = match[2];
2019
+ return {
2020
+ kind: "pseudo",
2021
+ name,
2022
+ args,
2023
+ match(node, selectorContext) {
2024
+ const fn = factory(name, selectorContext);
2025
+ return fn(node, args);
2018
2026
  }
2019
- const [, name, args] = match;
2020
- this.name = name;
2021
- this.args = args;
2022
- }
2023
- match(node, context) {
2024
- const fn = factory(this.name, context);
2025
- return fn(node, this.args);
2026
- }
2027
+ };
2027
2028
  }
2028
2029
 
2029
2030
  function isDelimiter(ch) {
@@ -2098,37 +2099,84 @@ class Compound {
2098
2099
  createCondition(pattern) {
2099
2100
  switch (pattern[0]) {
2100
2101
  case ".":
2101
- return new ClassCondition(pattern.slice(1));
2102
+ return createClassCondition(pattern.slice(1));
2102
2103
  case "#":
2103
- return new IdCondition(pattern.slice(1));
2104
+ return createIdCondition(pattern.slice(1));
2104
2105
  case "[":
2105
- return new AttributeCondition(pattern.slice(1, -1));
2106
+ return createAttributeCondition(pattern.slice(1, -1));
2106
2107
  case ":":
2107
- return new PseudoClassCondition(pattern.slice(1), this.selector);
2108
+ return createPseudoClassCondition(pattern.slice(1), this.selector);
2108
2109
  default:
2109
2110
  throw new Error(`Failed to create selector condition for "${pattern}"`);
2110
2111
  }
2111
2112
  }
2112
2113
  }
2113
2114
 
2114
- const codepoints = {
2115
- " ": "\\9 ",
2116
- "\n": "\\a ",
2117
- "\r": "\\d "
2118
- };
2119
- function escapeSelectorComponent(text) {
2120
- return text.toString().replaceAll(/([\t\n\r]|[^\w-])/gi, (_, ch) => {
2121
- if (codepoints[ch]) {
2122
- return codepoints[ch];
2123
- } else {
2124
- return `\\${ch}`;
2115
+ const escapedCodepoints = /* @__PURE__ */ new Set(["9", "a", "d"]);
2116
+ function* splitSelectorElements(selector) {
2117
+ let begin = 0;
2118
+ let end = 0;
2119
+ function initialState(ch, p) {
2120
+ if (ch === "\\") {
2121
+ return 1 /* ESCAPED */;
2125
2122
  }
2126
- });
2123
+ if (ch === " ") {
2124
+ end = p;
2125
+ return 2 /* WHITESPACE */;
2126
+ }
2127
+ return 0 /* INITIAL */;
2128
+ }
2129
+ function escapedState(ch) {
2130
+ if (escapedCodepoints.has(ch)) {
2131
+ return 1 /* ESCAPED */;
2132
+ }
2133
+ return 0 /* INITIAL */;
2134
+ }
2135
+ function* whitespaceState(ch, p) {
2136
+ if (ch === " ") {
2137
+ return 2 /* WHITESPACE */;
2138
+ }
2139
+ yield selector.slice(begin, end);
2140
+ begin = p;
2141
+ end = p;
2142
+ return 0 /* INITIAL */;
2143
+ }
2144
+ let state = 0 /* INITIAL */;
2145
+ for (let p = 0; p < selector.length; p++) {
2146
+ const ch = selector[p];
2147
+ switch (state) {
2148
+ case 0 /* INITIAL */:
2149
+ state = initialState(ch, p);
2150
+ break;
2151
+ case 1 /* ESCAPED */:
2152
+ state = escapedState(ch);
2153
+ break;
2154
+ case 2 /* WHITESPACE */:
2155
+ state = yield* whitespaceState(ch, p);
2156
+ break;
2157
+ }
2158
+ }
2159
+ if (begin !== selector.length) {
2160
+ yield selector.slice(begin);
2161
+ }
2127
2162
  }
2128
2163
 
2129
- function generateIdSelector(id) {
2130
- const escaped = escapeSelectorComponent(id);
2131
- return /^\d/.test(escaped) ? `[id="${escaped}"]` : `#${escaped}`;
2164
+ function unescapeCodepoint(value) {
2165
+ const replacement = {
2166
+ "\\9 ": " ",
2167
+ "\\a ": "\n",
2168
+ "\\d ": "\r"
2169
+ };
2170
+ return value.replaceAll(
2171
+ /(\\[9ad] )/g,
2172
+ (_, codepoint) => replacement[codepoint]
2173
+ );
2174
+ }
2175
+ function getCompounds(selector) {
2176
+ selector = selector.replaceAll(/([+>~]) /g, "$1");
2177
+ return Array.from(splitSelectorElements(selector), (element) => {
2178
+ return new Compound(unescapeCodepoint(element));
2179
+ });
2132
2180
  }
2133
2181
 
2134
2182
  function* ancestors$1(element) {
@@ -2193,70 +2241,16 @@ function matchElement(element, compounds, context) {
2193
2241
  return false;
2194
2242
  }
2195
2243
 
2196
- const escapedCodepoints = /* @__PURE__ */ new Set(["9", "a", "d"]);
2197
- function* splitSelectorElements(selector) {
2198
- let begin = 0;
2199
- let end = 0;
2200
- function initialState(ch, p) {
2201
- if (ch === "\\") {
2202
- return 1 /* ESCAPED */;
2203
- }
2204
- if (ch === " ") {
2205
- end = p;
2206
- return 2 /* WHITESPACE */;
2207
- }
2208
- return 0 /* INITIAL */;
2244
+ class ComplexSelector {
2245
+ compounds;
2246
+ constructor(compounds) {
2247
+ this.compounds = compounds;
2209
2248
  }
2210
- function escapedState(ch) {
2211
- if (escapedCodepoints.has(ch)) {
2212
- return 1 /* ESCAPED */;
2213
- }
2214
- return 0 /* INITIAL */;
2249
+ static fromString(selector) {
2250
+ return new ComplexSelector(getCompounds(selector));
2215
2251
  }
2216
- function* whitespaceState(ch, p) {
2217
- if (ch === " ") {
2218
- return 2 /* WHITESPACE */;
2219
- }
2220
- yield selector.slice(begin, end);
2221
- begin = p;
2222
- end = p;
2223
- return 0 /* INITIAL */;
2224
- }
2225
- let state = 0 /* INITIAL */;
2226
- for (let p = 0; p < selector.length; p++) {
2227
- const ch = selector[p];
2228
- switch (state) {
2229
- case 0 /* INITIAL */:
2230
- state = initialState(ch, p);
2231
- break;
2232
- case 1 /* ESCAPED */:
2233
- state = escapedState(ch);
2234
- break;
2235
- case 2 /* WHITESPACE */:
2236
- state = yield* whitespaceState(ch, p);
2237
- break;
2238
- }
2239
- }
2240
- if (begin !== selector.length) {
2241
- yield selector.slice(begin);
2242
- }
2243
- }
2244
-
2245
- function unescapeCodepoint(value) {
2246
- const replacement = {
2247
- "\\9 ": " ",
2248
- "\\a ": "\n",
2249
- "\\d ": "\r"
2250
- };
2251
- return value.replaceAll(
2252
- /(\\[9ad] )/g,
2253
- (_, codepoint) => replacement[codepoint]
2254
- );
2255
- }
2256
- class Selector {
2257
- pattern;
2258
- constructor(selector) {
2259
- this.pattern = Selector.parse(selector);
2252
+ static fromCompounds(compounds) {
2253
+ return new ComplexSelector(compounds);
2260
2254
  }
2261
2255
  /**
2262
2256
  * Match this selector against a HtmlElement.
@@ -2273,15 +2267,15 @@ class Selector {
2273
2267
  */
2274
2268
  matchElement(element) {
2275
2269
  const context = { scope: null };
2276
- return matchElement(element, this.pattern, context);
2270
+ return matchElement(element, this.compounds, context);
2277
2271
  }
2278
2272
  *matchInternal(root, level, context) {
2279
- if (level >= this.pattern.length) {
2273
+ if (level >= this.compounds.length) {
2280
2274
  yield root;
2281
2275
  return;
2282
2276
  }
2283
- const pattern = this.pattern[level];
2284
- const matches = Selector.findCandidates(root, pattern);
2277
+ const pattern = this.compounds[level];
2278
+ const matches = ComplexSelector.findCandidates(root, pattern);
2285
2279
  for (const node of matches) {
2286
2280
  if (!pattern.match(node, context)) {
2287
2281
  continue;
@@ -2289,12 +2283,6 @@ class Selector {
2289
2283
  yield* this.matchInternal(node, level + 1, context);
2290
2284
  }
2291
2285
  }
2292
- static parse(selector) {
2293
- selector = selector.replaceAll(/([+>~]) /g, "$1");
2294
- return Array.from(splitSelectorElements(selector), (element) => {
2295
- return new Compound(unescapeCodepoint(element));
2296
- });
2297
- }
2298
2286
  static findCandidates(root, pattern) {
2299
2287
  switch (pattern.combinator) {
2300
2288
  case Combinator.DESCENDANT:
@@ -2302,9 +2290,9 @@ class Selector {
2302
2290
  case Combinator.CHILD:
2303
2291
  return root.childElements.filter((node) => node.is(pattern.tagName));
2304
2292
  case Combinator.ADJACENT_SIBLING:
2305
- return Selector.findAdjacentSibling(root);
2293
+ return ComplexSelector.findAdjacentSibling(root);
2306
2294
  case Combinator.GENERAL_SIBLING:
2307
- return Selector.findGeneralSibling(root);
2295
+ return ComplexSelector.findGeneralSibling(root);
2308
2296
  case Combinator.SCOPE:
2309
2297
  return [root];
2310
2298
  }
@@ -2316,7 +2304,7 @@ class Selector {
2316
2304
  adjacent = false;
2317
2305
  return true;
2318
2306
  }
2319
- if (cur === node) {
2307
+ if (cur.isSameNode(node)) {
2320
2308
  adjacent = true;
2321
2309
  }
2322
2310
  return false;
@@ -2328,7 +2316,7 @@ class Selector {
2328
2316
  if (after) {
2329
2317
  return true;
2330
2318
  }
2331
- if (cur === node) {
2319
+ if (cur.isSameNode(node)) {
2332
2320
  after = true;
2333
2321
  }
2334
2322
  return false;
@@ -2336,6 +2324,31 @@ class Selector {
2336
2324
  }
2337
2325
  }
2338
2326
 
2327
+ const codepoints = {
2328
+ " ": "\\9 ",
2329
+ "\n": "\\a ",
2330
+ "\r": "\\d "
2331
+ };
2332
+ function escapeSelectorComponent(text) {
2333
+ return text.toString().replaceAll(/([\t\n\r]|[^\w-])/gi, (_, ch) => {
2334
+ if (codepoints[ch]) {
2335
+ return codepoints[ch];
2336
+ } else {
2337
+ return `\\${ch}`;
2338
+ }
2339
+ });
2340
+ }
2341
+
2342
+ function generateIdSelector(id) {
2343
+ const escaped = escapeSelectorComponent(id);
2344
+ return /^\d/.test(escaped) ? `[id="${escaped}"]` : `#${escaped}`;
2345
+ }
2346
+
2347
+ function parseSelector(selector) {
2348
+ const compounds = getCompounds(selector);
2349
+ return ComplexSelector.fromCompounds(compounds);
2350
+ }
2351
+
2339
2352
  const TEXT_NODE_NAME = "#text";
2340
2353
  function isTextNode(node) {
2341
2354
  return node?.nodeType === NodeType.TEXT_NODE;
@@ -2649,7 +2662,7 @@ class HtmlElement extends DOMNode {
2649
2662
  */
2650
2663
  matches(selectorList) {
2651
2664
  return selectorList.split(",").some((it) => {
2652
- const selector = new Selector(it.trim());
2665
+ const selector = parseSelector(it.trim());
2653
2666
  return selector.matchElement(this);
2654
2667
  });
2655
2668
  }
@@ -2882,9 +2895,9 @@ class HtmlElement extends DOMNode {
2882
2895
  if (!selectorList) {
2883
2896
  return;
2884
2897
  }
2885
- for (const selector of selectorList.split(/(?<!\\),\s*/)) {
2886
- const pattern = new Selector(selector);
2887
- yield* pattern.match(this);
2898
+ for (const selectorString of selectorList.split(/(?<!\\),\s*/)) {
2899
+ const selector = parseSelector(selectorString);
2900
+ yield* selector.match(this);
2888
2901
  }
2889
2902
  }
2890
2903
  /**
@@ -3737,6 +3750,7 @@ class Rule {
3737
3750
  severity;
3738
3751
  // rule severity
3739
3752
  event;
3753
+ tracker;
3740
3754
  /**
3741
3755
  * Rule name. Defaults to filename without extension but can be overwritten by
3742
3756
  * subclasses.
@@ -3756,6 +3770,7 @@ class Rule {
3756
3770
  this.blockers = [];
3757
3771
  this.severity = Severity.DISABLED;
3758
3772
  this.name = "";
3773
+ this.tracker = null;
3759
3774
  }
3760
3775
  getSeverity() {
3761
3776
  return this.severity;
@@ -3931,7 +3946,15 @@ class Rule {
3931
3946
  return this.parser.on(event, (_event, data) => {
3932
3947
  if (this.isEnabled() && filter(data)) {
3933
3948
  this.event = data;
3934
- callback(data);
3949
+ const { tracker } = this;
3950
+ if (tracker) {
3951
+ const start = performance.now();
3952
+ callback(data);
3953
+ const end = performance.now();
3954
+ tracker.trackRule(this.name, end - start);
3955
+ } else {
3956
+ callback(data);
3957
+ }
3935
3958
  }
3936
3959
  });
3937
3960
  }
@@ -3948,6 +3971,14 @@ class Rule {
3948
3971
  this.severity = severity;
3949
3972
  this.meta = meta;
3950
3973
  }
3974
+ /**
3975
+ * Set (or clear) the performance tracker.
3976
+ *
3977
+ * @internal
3978
+ */
3979
+ setTracker(tracker) {
3980
+ this.tracker = tracker;
3981
+ }
3951
3982
  /**
3952
3983
  * Validate rule options against schema. Throws error if object does not validate.
3953
3984
  *
@@ -4330,24 +4361,27 @@ const defaults$A = {
4330
4361
  }
4331
4362
  };
4332
4363
  const allowlist = /* @__PURE__ */ new Set([
4333
- "main",
4334
- "nav",
4335
- "table",
4336
- "td",
4337
- "th",
4364
+ /* landmark elements */
4365
+ "article",
4338
4366
  "aside",
4339
- "header",
4340
4367
  "footer",
4368
+ "form",
4369
+ "header",
4370
+ "main",
4371
+ "nav",
4372
+ "search",
4341
4373
  "section",
4342
- "article",
4374
+ /* other allowed elements */
4375
+ "area",
4343
4376
  "dialog",
4344
- "form",
4377
+ "fieldset",
4378
+ "figure",
4345
4379
  "iframe",
4346
4380
  "img",
4347
- "area",
4348
- "fieldset",
4349
4381
  "summary",
4350
- "figure"
4382
+ "table",
4383
+ "td",
4384
+ "th"
4351
4385
  ]);
4352
4386
  function isValidUsage(target, meta) {
4353
4387
  const explicit = meta.attributes["aria-label"];
@@ -7677,7 +7711,7 @@ class InputMissingLabel extends Rule {
7677
7711
  if (hasAccessibleName(root, elem)) {
7678
7712
  return;
7679
7713
  }
7680
- let label = [];
7714
+ let label;
7681
7715
  if ((label = findLabelById(root, elem.id)).length > 0) {
7682
7716
  this.validateLabel(root, elem, label);
7683
7717
  return;
@@ -8391,7 +8425,8 @@ class NoImplicitInputType extends Rule {
8391
8425
  const defaults$g = {
8392
8426
  include: null,
8393
8427
  exclude: null,
8394
- allowedProperties: ["display"]
8428
+ allowedProperties: ["display"],
8429
+ allowVariables: true
8395
8430
  };
8396
8431
  class NoInlineStyle extends Rule {
8397
8432
  constructor(options) {
@@ -8430,17 +8465,26 @@ class NoInlineStyle extends Rule {
8430
8465
  type: "string"
8431
8466
  },
8432
8467
  type: "array"
8468
+ },
8469
+ allowVariables: {
8470
+ type: "boolean"
8433
8471
  }
8434
8472
  };
8435
8473
  }
8436
8474
  documentation() {
8475
+ const { allowVariables, allowedProperties } = this.options;
8437
8476
  const text = [
8438
8477
  "Inline style is not allowed.\n",
8439
8478
  "Inline style is a sign of unstructured CSS. Use class or ID with a separate stylesheet.\n"
8440
8479
  ];
8441
- if (this.options.allowedProperties.length > 0) {
8480
+ if (allowedProperties.length > 0 || allowVariables) {
8442
8481
  text.push("Under the current configuration the following CSS properties are allowed:\n");
8443
- text.push(this.options.allowedProperties.map((it) => `- \`${it}\``).join("\n"));
8482
+ }
8483
+ if (allowedProperties.length > 0) {
8484
+ text.push(allowedProperties.map((it) => `- \`${it}\``).join("\n"));
8485
+ }
8486
+ if (allowVariables) {
8487
+ text.push("- CSS variables (custom properties starting with `--`).\n");
8444
8488
  }
8445
8489
  return {
8446
8490
  description: text.join("\n"),
@@ -8475,13 +8519,16 @@ class NoInlineStyle extends Rule {
8475
8519
  return true;
8476
8520
  }
8477
8521
  allPropertiesAllowed(value) {
8478
- const allowProperties = this.options.allowedProperties;
8479
- if (allowProperties.length === 0) {
8522
+ const { allowedProperties, allowVariables } = this.options;
8523
+ if (allowedProperties.length === 0 && !allowVariables) {
8480
8524
  return false;
8481
8525
  }
8482
8526
  const declarations = Object.keys(parseCssDeclaration(value));
8483
8527
  return declarations.length > 0 && declarations.every((it) => {
8484
- return allowProperties.includes(it);
8528
+ if (allowVariables && it.startsWith("--")) {
8529
+ return true;
8530
+ }
8531
+ return allowedProperties.includes(it);
8485
8532
  });
8486
8533
  }
8487
8534
  }
@@ -8584,7 +8631,7 @@ class NoMultipleMain extends Rule {
8584
8631
  const defaults$f = {
8585
8632
  relaxed: false
8586
8633
  };
8587
- const textRegexp = /([<>]|&(?![\d#A-Za-z]+;))/g;
8634
+ const textRegexp = /(<|&(?![\d#A-Za-z]+;))/g;
8588
8635
  const unquotedAttrRegexp = /(["'<=>`]|&(?![\d#A-Za-z]+;))/g;
8589
8636
  const matchTemplate = /^(<%.*?%>|<\?.*?\?>|<\$.*?\$>)$/s;
8590
8637
  const replacementTable = {
@@ -8611,7 +8658,7 @@ class NoRawCharacters extends Rule {
8611
8658
  }
8612
8659
  documentation() {
8613
8660
  return {
8614
- description: `Some characters such as \`<\`, \`>\` and \`&\` hold special meaning in HTML and must be escaped using a character reference (html entity).`,
8661
+ description: `Some characters such as \`<\` and \`&\` hold special meaning in HTML and must be escaped using a character reference (HTML entity).`,
8615
8662
  url: "https://html-validate.org/rules/no-raw-characters.html"
8616
8663
  };
8617
8664
  }
@@ -10026,14 +10073,17 @@ class UnknownCharReference extends Rule {
10026
10073
  if (found && (terminated || !requireSemicolon)) {
10027
10074
  return;
10028
10075
  }
10029
- if (found && !terminated) {
10030
- const entityLocation2 = getLocation(location, entity, match);
10031
- const message2 = `Character reference "{{ entity }}" must be terminated by a semicolon`;
10032
- const context2 = {
10033
- entity: raw,
10034
- terminated: false
10035
- };
10036
- this.report(node, message2, entityLocation2, context2);
10076
+ if (!terminated) {
10077
+ const isKnownName = found || this.entities.includes(`${entity};`);
10078
+ if (isKnownName) {
10079
+ const entityLocation2 = getLocation(location, entity, match);
10080
+ const message2 = `Character reference "{{ entity }}" must be terminated by a semicolon`;
10081
+ const context2 = {
10082
+ entity: raw,
10083
+ terminated: false
10084
+ };
10085
+ this.report(node, message2, entityLocation2, context2);
10086
+ }
10037
10087
  return;
10038
10088
  }
10039
10089
  const entityLocation = getLocation(location, entity, match);
@@ -12462,8 +12512,10 @@ class StaticConfigLoader extends ConfigLoader {
12462
12512
 
12463
12513
  class EventHandler {
12464
12514
  listeners;
12515
+ tracker;
12465
12516
  constructor() {
12466
12517
  this.listeners = {};
12518
+ this.tracker = null;
12467
12519
  }
12468
12520
  /**
12469
12521
  * Add an event listener.
@@ -12502,6 +12554,14 @@ class EventHandler {
12502
12554
  });
12503
12555
  return deregister;
12504
12556
  }
12557
+ /**
12558
+ * Set (or clear) the performance tracker.
12559
+ *
12560
+ * @internal
12561
+ */
12562
+ setTracker(tracker) {
12563
+ this.tracker = tracker;
12564
+ }
12505
12565
  /**
12506
12566
  * Trigger event causing all listeners to be called.
12507
12567
  *
@@ -12510,8 +12570,18 @@ class EventHandler {
12510
12570
  */
12511
12571
  /* eslint-disable-next-line @typescript-eslint/no-explicit-any -- technical debt, should be made typesafe */
12512
12572
  trigger(event, data) {
12513
- for (const listener of this.getCallbacks(event)) {
12514
- listener.call(null, event, data);
12573
+ const { tracker } = this;
12574
+ if (tracker) {
12575
+ const start = performance.now();
12576
+ for (const listener of this.getCallbacks(event)) {
12577
+ listener.call(null, event, data);
12578
+ }
12579
+ const end = performance.now();
12580
+ tracker.trackEvent(event, end - start);
12581
+ } else {
12582
+ for (const listener of this.getCallbacks(event)) {
12583
+ listener.call(null, event, data);
12584
+ }
12515
12585
  }
12516
12586
  }
12517
12587
  getCallbacks(event) {
@@ -12523,7 +12593,7 @@ class EventHandler {
12523
12593
  }
12524
12594
 
12525
12595
  const name = "html-validate";
12526
- const version = "10.14.0";
12596
+ const version = "10.16.0";
12527
12597
  const bugs = "https://gitlab.com/html-validate/html-validate/issues/new";
12528
12598
 
12529
12599
  function freeze(src) {
@@ -13457,6 +13527,78 @@ class Parser {
13457
13527
  }
13458
13528
  }
13459
13529
 
13530
+ class PerformanceTracker {
13531
+ eventData;
13532
+ ruleData;
13533
+ startTime;
13534
+ accConfigTime;
13535
+ accTransformTime;
13536
+ constructor() {
13537
+ this.eventData = /* @__PURE__ */ new Map();
13538
+ this.ruleData = /* @__PURE__ */ new Map();
13539
+ this.startTime = performance.now();
13540
+ this.accConfigTime = 0;
13541
+ this.accTransformTime = 0;
13542
+ }
13543
+ /**
13544
+ * Record a single event trigger with the time it took to run all listeners.
13545
+ */
13546
+ trackEvent(name, time) {
13547
+ const existing = this.eventData.get(name);
13548
+ if (existing) {
13549
+ existing.count += 1;
13550
+ existing.time += time;
13551
+ } else {
13552
+ this.eventData.set(name, { count: 1, time });
13553
+ }
13554
+ }
13555
+ /**
13556
+ * Record time spent loading configuration.
13557
+ */
13558
+ trackConfig(time) {
13559
+ this.accConfigTime += time;
13560
+ }
13561
+ /**
13562
+ * Record time spent in transformers.
13563
+ */
13564
+ trackTransform(time) {
13565
+ this.accTransformTime += time;
13566
+ }
13567
+ /**
13568
+ * Record a single rule callback invocation with its execution time.
13569
+ */
13570
+ trackRule(ruleName, time) {
13571
+ const existing = this.ruleData.get(ruleName);
13572
+ if (existing) {
13573
+ existing.count += 1;
13574
+ existing.time += time;
13575
+ } else {
13576
+ this.ruleData.set(ruleName, { count: 1, time });
13577
+ }
13578
+ }
13579
+ /**
13580
+ * Returns a snapshot of the recorded performance data, with both arrays
13581
+ * sorted by time (descending).
13582
+ */
13583
+ getResult() {
13584
+ const events = Array.from(
13585
+ this.eventData.entries(),
13586
+ ([event, { count, time }]) => ({ event, count, time })
13587
+ ).toSorted((a, b) => b.time - a.time);
13588
+ const rules = Array.from(
13589
+ this.ruleData.entries(),
13590
+ ([rule, { count, time }]) => ({ rule, count, time })
13591
+ ).toSorted((a, b) => b.time - a.time);
13592
+ return {
13593
+ events,
13594
+ rules,
13595
+ configTime: this.accConfigTime,
13596
+ transformTime: this.accTransformTime,
13597
+ totalTime: performance.now() - this.startTime
13598
+ };
13599
+ }
13600
+ }
13601
+
13460
13602
  const ruleIds = new Set(Object.keys(bundledRules));
13461
13603
  function ruleExists(ruleId) {
13462
13604
  return ruleIds.has(ruleId);
@@ -13504,10 +13646,12 @@ class Engine {
13504
13646
  config;
13505
13647
  ParserClass;
13506
13648
  availableRules;
13507
- constructor(config, ParserClass) {
13649
+ tracker;
13650
+ constructor(config, ParserClass, options) {
13508
13651
  this.report = new Reporter();
13509
13652
  this.config = config;
13510
13653
  this.ParserClass = ParserClass;
13654
+ this.tracker = options.tracker;
13511
13655
  const result = this.initPlugins(this.config);
13512
13656
  this.availableRules = {
13513
13657
  ...bundledRules,
@@ -13523,6 +13667,9 @@ class Engine {
13523
13667
  lint(sources) {
13524
13668
  for (const source of sources) {
13525
13669
  const parser = this.instantiateParser();
13670
+ if (this.tracker) {
13671
+ parser.getEventHandler().setTracker(this.tracker);
13672
+ }
13526
13673
  const { rules } = this.setupPlugins(source, this.config, parser);
13527
13674
  const noUnusedDisable = rules["no-unused-disable"];
13528
13675
  const directiveContext = {
@@ -13799,6 +13946,7 @@ class Engine {
13799
13946
  const rule = this.instantiateRule(ruleId, options);
13800
13947
  rule.name = ruleId;
13801
13948
  rule.init(parser, report, severity, meta);
13949
+ rule.setTracker(this.tracker);
13802
13950
  if (rule.setup) {
13803
13951
  rule.setup();
13804
13952
  }
@@ -15157,5 +15305,5 @@ const engines = {
15157
15305
 
15158
15306
  var workerPath = "./jest-worker.js";
15159
15307
 
15160
- export { engines as $, Attribute as A, TextContent$1 as B, ConfigLoader as C, DOMNode as D, Engine as E, TextNode as F, ariaNaming as G, HtmlElement as H, classifyNodeText as I, presets as J, defineConfig as K, definePlugin as L, MetaCopyableProperty as M, NestedError as N, isUserError as O, Parser as P, keywordPatternMatcher as Q, Reporter as R, StaticConfigLoader as S, TextClassification as T, UserError as U, Validator as V, WrappedError as W, ruleExists as X, sliceLocation as Y, staticResolver as Z, walk as _, transformSourceSync as a, codeFrameColumns as a0, getEndLocation as a1, getStartLocation as a2, workerPath as a3, name as a4, bugs as a5, transformFilename as b, transformFilenameSync as c, configurationSchema as d, ConfigError as e, Config as f, compatibilityCheckImpl as g, ensureError as h, isThenable as i, getFormatter as j, deepmerge as k, ignore as l, DOMTokenList as m, normalizeSource as n, DOMTree as o, DynamicValue as p, EventHandler as q, MetaTable as r, NodeClosed as s, transformSource as t, NodeType as u, version as v, ResolvedConfig as w, Rule as x, SchemaValidationError as y, Severity as z };
15308
+ export { walk as $, Attribute as A, Severity as B, ConfigLoader as C, DOMNode as D, Engine as E, TextContent$1 as F, TextNode as G, HtmlElement as H, ariaNaming as I, classifyNodeText as J, presets as K, defineConfig as L, MetaCopyableProperty as M, NestedError as N, definePlugin as O, PerformanceTracker as P, isUserError as Q, Reporter as R, StaticConfigLoader as S, TextClassification as T, UserError as U, Validator as V, WrappedError as W, keywordPatternMatcher as X, ruleExists as Y, sliceLocation as Z, staticResolver as _, Parser as a, engines as a0, codeFrameColumns as a1, getEndLocation as a2, getStartLocation as a3, workerPath as a4, name as a5, bugs as a6, transformSourceSync as b, transformFilename as c, transformFilenameSync as d, configurationSchema as e, ConfigError as f, Config as g, compatibilityCheckImpl as h, isThenable as i, ensureError as j, getFormatter as k, deepmerge as l, ignore as m, normalizeSource as n, DOMTokenList as o, DOMTree as p, DynamicValue as q, EventHandler as r, MetaTable as s, transformSource as t, NodeClosed as u, version as v, NodeType as w, ResolvedConfig as x, Rule as y, SchemaValidationError as z };
15161
15309
  //# sourceMappingURL=core.js.map