html-validate 10.13.1 → 10.15.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,19 +2099,86 @@ 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
 
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 */;
2122
+ }
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
+ }
2162
+ }
2163
+
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
+ });
2180
+ }
2181
+
2114
2182
  function* ancestors$1(element) {
2115
2183
  let current = element.parent;
2116
2184
  while (current && !current.isRootElement()) {
@@ -2173,88 +2241,16 @@ function matchElement(element, compounds, context) {
2173
2241
  return false;
2174
2242
  }
2175
2243
 
2176
- const escapedCodepoints = /* @__PURE__ */ new Set(["9", "a", "d"]);
2177
- function* splitSelectorElements(selector) {
2178
- let begin = 0;
2179
- let end = 0;
2180
- function initialState(ch, p) {
2181
- if (ch === "\\") {
2182
- return 1 /* ESCAPED */;
2183
- }
2184
- if (ch === " ") {
2185
- end = p;
2186
- return 2 /* WHITESPACE */;
2187
- }
2188
- return 0 /* INITIAL */;
2189
- }
2190
- function escapedState(ch) {
2191
- if (escapedCodepoints.has(ch)) {
2192
- return 1 /* ESCAPED */;
2193
- }
2194
- return 0 /* INITIAL */;
2195
- }
2196
- function* whitespaceState(ch, p) {
2197
- if (ch === " ") {
2198
- return 2 /* WHITESPACE */;
2199
- }
2200
- yield selector.slice(begin, end);
2201
- begin = p;
2202
- end = p;
2203
- return 0 /* INITIAL */;
2244
+ class ComplexSelector {
2245
+ compounds;
2246
+ constructor(compounds) {
2247
+ this.compounds = compounds;
2204
2248
  }
2205
- let state = 0 /* INITIAL */;
2206
- for (let p = 0; p < selector.length; p++) {
2207
- const ch = selector[p];
2208
- switch (state) {
2209
- case 0 /* INITIAL */:
2210
- state = initialState(ch, p);
2211
- break;
2212
- case 1 /* ESCAPED */:
2213
- state = escapedState(ch);
2214
- break;
2215
- case 2 /* WHITESPACE */:
2216
- state = yield* whitespaceState(ch, p);
2217
- break;
2218
- }
2249
+ static fromString(selector) {
2250
+ return new ComplexSelector(getCompounds(selector));
2219
2251
  }
2220
- if (begin !== selector.length) {
2221
- yield selector.slice(begin);
2222
- }
2223
- }
2224
-
2225
- function unescapeCodepoint(value) {
2226
- const replacement = {
2227
- "\\9 ": " ",
2228
- "\\a ": "\n",
2229
- "\\d ": "\r"
2230
- };
2231
- return value.replaceAll(
2232
- /(\\[9ad] )/g,
2233
- (_, codepoint) => replacement[codepoint]
2234
- );
2235
- }
2236
- function escapeSelectorComponent(text) {
2237
- const codepoints = {
2238
- " ": "\\9 ",
2239
- "\n": "\\a ",
2240
- "\r": "\\d "
2241
- };
2242
- return text.toString().replaceAll(/([\t\n\r]|[^\w-])/gi, (_, ch) => {
2243
- if (codepoints[ch]) {
2244
- return codepoints[ch];
2245
- } else {
2246
- return `\\${ch}`;
2247
- }
2248
- });
2249
- }
2250
- function generateIdSelector(id) {
2251
- const escaped = escapeSelectorComponent(id);
2252
- return /^\d/.test(escaped) ? `[id="${escaped}"]` : `#${escaped}`;
2253
- }
2254
- class Selector {
2255
- pattern;
2256
- constructor(selector) {
2257
- this.pattern = Selector.parse(selector);
2252
+ static fromCompounds(compounds) {
2253
+ return new ComplexSelector(compounds);
2258
2254
  }
2259
2255
  /**
2260
2256
  * Match this selector against a HtmlElement.
@@ -2271,15 +2267,15 @@ class Selector {
2271
2267
  */
2272
2268
  matchElement(element) {
2273
2269
  const context = { scope: null };
2274
- return matchElement(element, this.pattern, context);
2270
+ return matchElement(element, this.compounds, context);
2275
2271
  }
2276
2272
  *matchInternal(root, level, context) {
2277
- if (level >= this.pattern.length) {
2273
+ if (level >= this.compounds.length) {
2278
2274
  yield root;
2279
2275
  return;
2280
2276
  }
2281
- const pattern = this.pattern[level];
2282
- const matches = Selector.findCandidates(root, pattern);
2277
+ const pattern = this.compounds[level];
2278
+ const matches = ComplexSelector.findCandidates(root, pattern);
2283
2279
  for (const node of matches) {
2284
2280
  if (!pattern.match(node, context)) {
2285
2281
  continue;
@@ -2287,12 +2283,6 @@ class Selector {
2287
2283
  yield* this.matchInternal(node, level + 1, context);
2288
2284
  }
2289
2285
  }
2290
- static parse(selector) {
2291
- selector = selector.replaceAll(/([+>~]) /g, "$1");
2292
- return Array.from(splitSelectorElements(selector), (element) => {
2293
- return new Compound(unescapeCodepoint(element));
2294
- });
2295
- }
2296
2286
  static findCandidates(root, pattern) {
2297
2287
  switch (pattern.combinator) {
2298
2288
  case Combinator.DESCENDANT:
@@ -2300,9 +2290,9 @@ class Selector {
2300
2290
  case Combinator.CHILD:
2301
2291
  return root.childElements.filter((node) => node.is(pattern.tagName));
2302
2292
  case Combinator.ADJACENT_SIBLING:
2303
- return Selector.findAdjacentSibling(root);
2293
+ return ComplexSelector.findAdjacentSibling(root);
2304
2294
  case Combinator.GENERAL_SIBLING:
2305
- return Selector.findGeneralSibling(root);
2295
+ return ComplexSelector.findGeneralSibling(root);
2306
2296
  case Combinator.SCOPE:
2307
2297
  return [root];
2308
2298
  }
@@ -2314,7 +2304,7 @@ class Selector {
2314
2304
  adjacent = false;
2315
2305
  return true;
2316
2306
  }
2317
- if (cur === node) {
2307
+ if (cur.isSameNode(node)) {
2318
2308
  adjacent = true;
2319
2309
  }
2320
2310
  return false;
@@ -2326,7 +2316,7 @@ class Selector {
2326
2316
  if (after) {
2327
2317
  return true;
2328
2318
  }
2329
- if (cur === node) {
2319
+ if (cur.isSameNode(node)) {
2330
2320
  after = true;
2331
2321
  }
2332
2322
  return false;
@@ -2334,6 +2324,31 @@ class Selector {
2334
2324
  }
2335
2325
  }
2336
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
+
2337
2352
  const TEXT_NODE_NAME = "#text";
2338
2353
  function isTextNode(node) {
2339
2354
  return node?.nodeType === NodeType.TEXT_NODE;
@@ -2369,6 +2384,7 @@ class TextNode extends DOMNode {
2369
2384
  }
2370
2385
  }
2371
2386
 
2387
+ const CHILD_ELEMENTS = /* @__PURE__ */ Symbol("childElements");
2372
2388
  const ROLE = /* @__PURE__ */ Symbol("role");
2373
2389
  const TABINDEX = /* @__PURE__ */ Symbol("tabindex");
2374
2390
  var NodeClosed = /* @__PURE__ */ ((NodeClosed2) => {
@@ -2535,7 +2551,11 @@ class HtmlElement extends DOMNode {
2535
2551
  * Similar to childNodes but only elements.
2536
2552
  */
2537
2553
  get childElements() {
2538
- return this.childNodes.filter(isElementNode);
2554
+ const cached = this.cacheGet(CHILD_ELEMENTS);
2555
+ if (cached !== void 0) {
2556
+ return cached;
2557
+ }
2558
+ return this.cacheSet(CHILD_ELEMENTS, this.childNodes.filter(isElementNode));
2539
2559
  }
2540
2560
  /**
2541
2561
  * Find the first ancestor matching a selector.
@@ -2642,7 +2662,7 @@ class HtmlElement extends DOMNode {
2642
2662
  */
2643
2663
  matches(selectorList) {
2644
2664
  return selectorList.split(",").some((it) => {
2645
- const selector = new Selector(it.trim());
2665
+ const selector = parseSelector(it.trim());
2646
2666
  return selector.matchElement(this);
2647
2667
  });
2648
2668
  }
@@ -2875,9 +2895,9 @@ class HtmlElement extends DOMNode {
2875
2895
  if (!selectorList) {
2876
2896
  return;
2877
2897
  }
2878
- for (const selector of selectorList.split(/(?<!\\),\s*/)) {
2879
- const pattern = new Selector(selector);
2880
- yield* pattern.match(this);
2898
+ for (const selectorString of selectorList.split(/(?<!\\),\s*/)) {
2899
+ const selector = parseSelector(selectorString);
2900
+ yield* selector.match(this);
2881
2901
  }
2882
2902
  }
2883
2903
  /**
@@ -2931,12 +2951,24 @@ class HtmlElement extends DOMNode {
2931
2951
  }
2932
2952
  return visit(this);
2933
2953
  }
2954
+ append(node) {
2955
+ super.append(node);
2956
+ this.cacheRemove(CHILD_ELEMENTS);
2957
+ }
2958
+ insertBefore(node, reference) {
2959
+ super.insertBefore(node, reference);
2960
+ this.cacheRemove(CHILD_ELEMENTS);
2961
+ }
2962
+ removeChild(node) {
2963
+ return super.removeChild(node);
2964
+ }
2934
2965
  /**
2935
2966
  * @internal
2936
2967
  */
2937
2968
  _setParent(node) {
2938
2969
  const oldParent = this._parent;
2939
2970
  this._parent = node instanceof HtmlElement ? node : null;
2971
+ oldParent?.cacheRemove(CHILD_ELEMENTS);
2940
2972
  return oldParent;
2941
2973
  }
2942
2974
  }
@@ -3718,6 +3750,7 @@ class Rule {
3718
3750
  severity;
3719
3751
  // rule severity
3720
3752
  event;
3753
+ tracker;
3721
3754
  /**
3722
3755
  * Rule name. Defaults to filename without extension but can be overwritten by
3723
3756
  * subclasses.
@@ -3737,6 +3770,7 @@ class Rule {
3737
3770
  this.blockers = [];
3738
3771
  this.severity = Severity.DISABLED;
3739
3772
  this.name = "";
3773
+ this.tracker = null;
3740
3774
  }
3741
3775
  getSeverity() {
3742
3776
  return this.severity;
@@ -3912,7 +3946,15 @@ class Rule {
3912
3946
  return this.parser.on(event, (_event, data) => {
3913
3947
  if (this.isEnabled() && filter(data)) {
3914
3948
  this.event = data;
3915
- 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
+ }
3916
3958
  }
3917
3959
  });
3918
3960
  }
@@ -3929,6 +3971,14 @@ class Rule {
3929
3971
  this.severity = severity;
3930
3972
  this.meta = meta;
3931
3973
  }
3974
+ /**
3975
+ * Set (or clear) the performance tracker.
3976
+ *
3977
+ * @internal
3978
+ */
3979
+ setTracker(tracker) {
3980
+ this.tracker = tracker;
3981
+ }
3932
3982
  /**
3933
3983
  * Validate rule options against schema. Throws error if object does not validate.
3934
3984
  *
@@ -12443,8 +12493,10 @@ class StaticConfigLoader extends ConfigLoader {
12443
12493
 
12444
12494
  class EventHandler {
12445
12495
  listeners;
12496
+ tracker;
12446
12497
  constructor() {
12447
12498
  this.listeners = {};
12499
+ this.tracker = null;
12448
12500
  }
12449
12501
  /**
12450
12502
  * Add an event listener.
@@ -12483,6 +12535,14 @@ class EventHandler {
12483
12535
  });
12484
12536
  return deregister;
12485
12537
  }
12538
+ /**
12539
+ * Set (or clear) the performance tracker.
12540
+ *
12541
+ * @internal
12542
+ */
12543
+ setTracker(tracker) {
12544
+ this.tracker = tracker;
12545
+ }
12486
12546
  /**
12487
12547
  * Trigger event causing all listeners to be called.
12488
12548
  *
@@ -12491,8 +12551,18 @@ class EventHandler {
12491
12551
  */
12492
12552
  /* eslint-disable-next-line @typescript-eslint/no-explicit-any -- technical debt, should be made typesafe */
12493
12553
  trigger(event, data) {
12494
- for (const listener of this.getCallbacks(event)) {
12495
- listener.call(null, event, data);
12554
+ const { tracker } = this;
12555
+ if (tracker) {
12556
+ const start = performance.now();
12557
+ for (const listener of this.getCallbacks(event)) {
12558
+ listener.call(null, event, data);
12559
+ }
12560
+ const end = performance.now();
12561
+ tracker.trackEvent(event, end - start);
12562
+ } else {
12563
+ for (const listener of this.getCallbacks(event)) {
12564
+ listener.call(null, event, data);
12565
+ }
12496
12566
  }
12497
12567
  }
12498
12568
  getCallbacks(event) {
@@ -12504,7 +12574,7 @@ class EventHandler {
12504
12574
  }
12505
12575
 
12506
12576
  const name = "html-validate";
12507
- const version = "10.13.1";
12577
+ const version = "10.15.0";
12508
12578
  const bugs = "https://gitlab.com/html-validate/html-validate/issues/new";
12509
12579
 
12510
12580
  function freeze(src) {
@@ -12804,32 +12874,92 @@ class Parser {
12804
12874
  * valid). The parser handles this by checking if the element on top of the
12805
12875
  * stack when is allowed to omit.
12806
12876
  */
12877
+ /**
12878
+ * Check whether a given element would be implicitly closed by an incoming
12879
+ * start tag. Used both in `closeOptional` and in the multi-level lookahead.
12880
+ */
12881
+ wouldCloseElement(token, element) {
12882
+ if (!element.meta) {
12883
+ return false;
12884
+ }
12885
+ const implicitClosed = element.meta.implicitClosed;
12886
+ if (!implicitClosed) {
12887
+ return false;
12888
+ }
12889
+ const tagName = token.data[2];
12890
+ const incomingMeta = this.metaTable.getMetaFor(tagName);
12891
+ return implicitClosed.some((entry) => {
12892
+ if (!entry.startsWith("@")) {
12893
+ return entry === tagName;
12894
+ }
12895
+ return incomingMeta ? matchesContentCategory(incomingMeta, entry) : false;
12896
+ });
12897
+ }
12898
+ /**
12899
+ * Walk up the active stack to find the parent element that will remain
12900
+ * after all multi-level implicit closes triggered by the incoming start tag.
12901
+ * For a single-level close this is equivalent to `getActive().parent`.
12902
+ */
12903
+ getParentAfterImplicitClose(token) {
12904
+ let current = this.dom.getActive();
12905
+ while (!current.isRootElement() && this.wouldCloseElement(token, current)) {
12906
+ const { parent } = current;
12907
+ if (!parent) {
12908
+ break;
12909
+ }
12910
+ current = parent;
12911
+ }
12912
+ return current;
12913
+ }
12807
12914
  closeOptional(token) {
12808
12915
  const active = this.dom.getActive();
12809
12916
  if (!active.meta) {
12810
12917
  return false;
12811
12918
  }
12812
- const tagName = token.data[2];
12813
12919
  const open = !token.data[1];
12814
- const implicitClosed = active.meta.implicitClosed;
12815
12920
  if (open) {
12816
- if (!implicitClosed) {
12817
- return false;
12818
- }
12819
- const incomingMeta = this.metaTable.getMetaFor(tagName);
12820
- return implicitClosed.some((entry) => {
12821
- if (!entry.startsWith("@")) {
12822
- return entry === tagName;
12823
- }
12824
- return incomingMeta ? matchesContentCategory(incomingMeta, entry) : false;
12825
- });
12921
+ return this.wouldCloseElement(token, active);
12826
12922
  } else {
12827
- if (active.is(tagName)) {
12923
+ return this.closeOptionalEndTag(token, active);
12924
+ }
12925
+ }
12926
+ /**
12927
+ * Returns `true` if the element’s end tag may be omitted, either because
12928
+ * its `implicitClosed` list includes its own tag name (e.g. `<li>`, `<td>`)
12929
+ * or because `optionalEnd` is set.
12930
+ */
12931
+ canOmitEndTag(element) {
12932
+ if (!element.meta) {
12933
+ return false;
12934
+ }
12935
+ const { implicitClosed, optionalEnd } = element.meta;
12936
+ return Boolean(implicitClosed?.includes(element.tagName)) || Boolean(optionalEnd);
12937
+ }
12938
+ /**
12939
+ * Check whether the active element can be implicitly closed by an incoming
12940
+ * end tag. The end tag may close a direct parent or any ancestor, as long as
12941
+ * every intermediate element can also have its end tag omitted.
12942
+ * This handles cases like `</table>` implicitly closing `<td>`, `<tr>`, `<tbody>`.
12943
+ */
12944
+ closeOptionalEndTag(token, active) {
12945
+ const tagName = token.data[2];
12946
+ if (active.is(tagName)) {
12947
+ return false;
12948
+ }
12949
+ if (!this.canOmitEndTag(active)) {
12950
+ return false;
12951
+ }
12952
+ let ancestor = active.parent;
12953
+ while (ancestor && !ancestor.isRootElement()) {
12954
+ if (ancestor.is(tagName)) {
12955
+ return true;
12956
+ }
12957
+ if (!this.canOmitEndTag(ancestor)) {
12828
12958
  return false;
12829
12959
  }
12830
- const canOmitEnd = Boolean(implicitClosed?.includes(active.tagName)) || Boolean(active.meta.optionalEnd);
12831
- return Boolean(active.parent && active.parent.is(tagName) && canOmitEnd);
12960
+ ancestor = ancestor.parent;
12832
12961
  }
12962
+ return false;
12833
12963
  }
12834
12964
  /**
12835
12965
  * Check whether an intermediary element (e.g. `<head>` or `<body>`) should
@@ -12918,8 +13048,21 @@ class Parser {
12918
13048
  );
12919
13049
  const endToken = tokens.at(-1);
12920
13050
  const isStartTag = !startToken.data[1];
12921
- const closeOptional = this.closeOptional(startToken);
12922
- const baseParent = closeOptional ? this.dom.getActive().parent : this.dom.getActive();
13051
+ let baseParent;
13052
+ if (isStartTag) {
13053
+ baseParent = this.getParentAfterImplicitClose(startToken);
13054
+ } else {
13055
+ const tagName = startToken.data[2];
13056
+ let cur = this.dom.getActive();
13057
+ while (!cur.isRootElement() && !cur.is(tagName)) {
13058
+ const { parent: parent2 } = cur;
13059
+ if (!parent2) {
13060
+ break;
13061
+ }
13062
+ cur = parent2;
13063
+ }
13064
+ baseParent = cur;
13065
+ }
12923
13066
  const implicitParent = isStartTag ? this.peekImplicitOpen(startToken, baseParent) : null;
12924
13067
  const parent = implicitParent ?? baseParent;
12925
13068
  const node = HtmlElement.fromTokens(
@@ -12931,7 +13074,7 @@ class Parser {
12931
13074
  );
12932
13075
  const isClosing = !isStartTag || node.closed !== NodeClosed.Open;
12933
13076
  const isForeign = node.meta?.foreign;
12934
- if (closeOptional) {
13077
+ while (this.closeOptional(startToken)) {
12935
13078
  const active = this.dom.getActive();
12936
13079
  active.closed = NodeClosed.ImplicitClosed;
12937
13080
  this.closeElement(source, node, active, startToken.location);
@@ -13365,6 +13508,78 @@ class Parser {
13365
13508
  }
13366
13509
  }
13367
13510
 
13511
+ class PerformanceTracker {
13512
+ eventData;
13513
+ ruleData;
13514
+ startTime;
13515
+ accConfigTime;
13516
+ accTransformTime;
13517
+ constructor() {
13518
+ this.eventData = /* @__PURE__ */ new Map();
13519
+ this.ruleData = /* @__PURE__ */ new Map();
13520
+ this.startTime = performance.now();
13521
+ this.accConfigTime = 0;
13522
+ this.accTransformTime = 0;
13523
+ }
13524
+ /**
13525
+ * Record a single event trigger with the time it took to run all listeners.
13526
+ */
13527
+ trackEvent(name, time) {
13528
+ const existing = this.eventData.get(name);
13529
+ if (existing) {
13530
+ existing.count += 1;
13531
+ existing.time += time;
13532
+ } else {
13533
+ this.eventData.set(name, { count: 1, time });
13534
+ }
13535
+ }
13536
+ /**
13537
+ * Record time spent loading configuration.
13538
+ */
13539
+ trackConfig(time) {
13540
+ this.accConfigTime += time;
13541
+ }
13542
+ /**
13543
+ * Record time spent in transformers.
13544
+ */
13545
+ trackTransform(time) {
13546
+ this.accTransformTime += time;
13547
+ }
13548
+ /**
13549
+ * Record a single rule callback invocation with its execution time.
13550
+ */
13551
+ trackRule(ruleName, time) {
13552
+ const existing = this.ruleData.get(ruleName);
13553
+ if (existing) {
13554
+ existing.count += 1;
13555
+ existing.time += time;
13556
+ } else {
13557
+ this.ruleData.set(ruleName, { count: 1, time });
13558
+ }
13559
+ }
13560
+ /**
13561
+ * Returns a snapshot of the recorded performance data, with both arrays
13562
+ * sorted by time (descending).
13563
+ */
13564
+ getResult() {
13565
+ const events = Array.from(
13566
+ this.eventData.entries(),
13567
+ ([event, { count, time }]) => ({ event, count, time })
13568
+ ).toSorted((a, b) => b.time - a.time);
13569
+ const rules = Array.from(
13570
+ this.ruleData.entries(),
13571
+ ([rule, { count, time }]) => ({ rule, count, time })
13572
+ ).toSorted((a, b) => b.time - a.time);
13573
+ return {
13574
+ events,
13575
+ rules,
13576
+ configTime: this.accConfigTime,
13577
+ transformTime: this.accTransformTime,
13578
+ totalTime: performance.now() - this.startTime
13579
+ };
13580
+ }
13581
+ }
13582
+
13368
13583
  const ruleIds = new Set(Object.keys(bundledRules));
13369
13584
  function ruleExists(ruleId) {
13370
13585
  return ruleIds.has(ruleId);
@@ -13412,10 +13627,12 @@ class Engine {
13412
13627
  config;
13413
13628
  ParserClass;
13414
13629
  availableRules;
13415
- constructor(config, ParserClass) {
13630
+ tracker;
13631
+ constructor(config, ParserClass, options) {
13416
13632
  this.report = new Reporter();
13417
13633
  this.config = config;
13418
13634
  this.ParserClass = ParserClass;
13635
+ this.tracker = options.tracker;
13419
13636
  const result = this.initPlugins(this.config);
13420
13637
  this.availableRules = {
13421
13638
  ...bundledRules,
@@ -13431,6 +13648,9 @@ class Engine {
13431
13648
  lint(sources) {
13432
13649
  for (const source of sources) {
13433
13650
  const parser = this.instantiateParser();
13651
+ if (this.tracker) {
13652
+ parser.getEventHandler().setTracker(this.tracker);
13653
+ }
13434
13654
  const { rules } = this.setupPlugins(source, this.config, parser);
13435
13655
  const noUnusedDisable = rules["no-unused-disable"];
13436
13656
  const directiveContext = {
@@ -13707,6 +13927,7 @@ class Engine {
13707
13927
  const rule = this.instantiateRule(ruleId, options);
13708
13928
  rule.name = ruleId;
13709
13929
  rule.init(parser, report, severity, meta);
13930
+ rule.setTracker(this.tracker);
13710
13931
  if (rule.setup) {
13711
13932
  rule.setup();
13712
13933
  }
@@ -15065,5 +15286,5 @@ const engines = {
15065
15286
 
15066
15287
  var workerPath = "./jest-worker.js";
15067
15288
 
15068
- 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 };
15289
+ 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 };
15069
15290
  //# sourceMappingURL=core.js.map