html-validate 10.13.1 → 10.14.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
@@ -2120,6 +2120,26 @@ class Compound {
2120
2120
  }
2121
2121
  }
2122
2122
 
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}`;
2134
+ }
2135
+ });
2136
+ }
2137
+
2138
+ function generateIdSelector(id) {
2139
+ const escaped = escapeSelectorComponent(id);
2140
+ return /^\d/.test(escaped) ? `[id="${escaped}"]` : `#${escaped}`;
2141
+ }
2142
+
2123
2143
  function* ancestors$1(element) {
2124
2144
  let current = element.parent;
2125
2145
  while (current && !current.isRootElement()) {
@@ -2242,24 +2262,6 @@ function unescapeCodepoint(value) {
2242
2262
  (_, codepoint) => replacement[codepoint]
2243
2263
  );
2244
2264
  }
2245
- function escapeSelectorComponent(text) {
2246
- const codepoints = {
2247
- " ": "\\9 ",
2248
- "\n": "\\a ",
2249
- "\r": "\\d "
2250
- };
2251
- return text.toString().replaceAll(/([\t\n\r]|[^\w-])/gi, (_, ch) => {
2252
- if (codepoints[ch]) {
2253
- return codepoints[ch];
2254
- } else {
2255
- return `\\${ch}`;
2256
- }
2257
- });
2258
- }
2259
- function generateIdSelector(id) {
2260
- const escaped = escapeSelectorComponent(id);
2261
- return /^\d/.test(escaped) ? `[id="${escaped}"]` : `#${escaped}`;
2262
- }
2263
2265
  class Selector {
2264
2266
  pattern;
2265
2267
  constructor(selector) {
@@ -2378,6 +2380,7 @@ class TextNode extends DOMNode {
2378
2380
  }
2379
2381
  }
2380
2382
 
2383
+ const CHILD_ELEMENTS = /* @__PURE__ */ Symbol("childElements");
2381
2384
  const ROLE = /* @__PURE__ */ Symbol("role");
2382
2385
  const TABINDEX = /* @__PURE__ */ Symbol("tabindex");
2383
2386
  var NodeClosed = /* @__PURE__ */ ((NodeClosed2) => {
@@ -2544,7 +2547,11 @@ class HtmlElement extends DOMNode {
2544
2547
  * Similar to childNodes but only elements.
2545
2548
  */
2546
2549
  get childElements() {
2547
- return this.childNodes.filter(isElementNode);
2550
+ const cached = this.cacheGet(CHILD_ELEMENTS);
2551
+ if (cached !== void 0) {
2552
+ return cached;
2553
+ }
2554
+ return this.cacheSet(CHILD_ELEMENTS, this.childNodes.filter(isElementNode));
2548
2555
  }
2549
2556
  /**
2550
2557
  * Find the first ancestor matching a selector.
@@ -2940,12 +2947,24 @@ class HtmlElement extends DOMNode {
2940
2947
  }
2941
2948
  return visit(this);
2942
2949
  }
2950
+ append(node) {
2951
+ super.append(node);
2952
+ this.cacheRemove(CHILD_ELEMENTS);
2953
+ }
2954
+ insertBefore(node, reference) {
2955
+ super.insertBefore(node, reference);
2956
+ this.cacheRemove(CHILD_ELEMENTS);
2957
+ }
2958
+ removeChild(node) {
2959
+ return super.removeChild(node);
2960
+ }
2943
2961
  /**
2944
2962
  * @internal
2945
2963
  */
2946
2964
  _setParent(node) {
2947
2965
  const oldParent = this._parent;
2948
2966
  this._parent = node instanceof HtmlElement ? node : null;
2967
+ oldParent?.cacheRemove(CHILD_ELEMENTS);
2949
2968
  return oldParent;
2950
2969
  }
2951
2970
  }
@@ -12513,7 +12532,7 @@ class EventHandler {
12513
12532
  }
12514
12533
 
12515
12534
  const name = "html-validate";
12516
- const version = "10.13.1";
12535
+ const version = "10.14.0";
12517
12536
  const bugs = "https://gitlab.com/html-validate/html-validate/issues/new";
12518
12537
 
12519
12538
  function freeze(src) {
@@ -12813,32 +12832,92 @@ class Parser {
12813
12832
  * valid). The parser handles this by checking if the element on top of the
12814
12833
  * stack when is allowed to omit.
12815
12834
  */
12835
+ /**
12836
+ * Check whether a given element would be implicitly closed by an incoming
12837
+ * start tag. Used both in `closeOptional` and in the multi-level lookahead.
12838
+ */
12839
+ wouldCloseElement(token, element) {
12840
+ if (!element.meta) {
12841
+ return false;
12842
+ }
12843
+ const implicitClosed = element.meta.implicitClosed;
12844
+ if (!implicitClosed) {
12845
+ return false;
12846
+ }
12847
+ const tagName = token.data[2];
12848
+ const incomingMeta = this.metaTable.getMetaFor(tagName);
12849
+ return implicitClosed.some((entry) => {
12850
+ if (!entry.startsWith("@")) {
12851
+ return entry === tagName;
12852
+ }
12853
+ return incomingMeta ? matchesContentCategory(incomingMeta, entry) : false;
12854
+ });
12855
+ }
12856
+ /**
12857
+ * Walk up the active stack to find the parent element that will remain
12858
+ * after all multi-level implicit closes triggered by the incoming start tag.
12859
+ * For a single-level close this is equivalent to `getActive().parent`.
12860
+ */
12861
+ getParentAfterImplicitClose(token) {
12862
+ let current = this.dom.getActive();
12863
+ while (!current.isRootElement() && this.wouldCloseElement(token, current)) {
12864
+ const { parent } = current;
12865
+ if (!parent) {
12866
+ break;
12867
+ }
12868
+ current = parent;
12869
+ }
12870
+ return current;
12871
+ }
12816
12872
  closeOptional(token) {
12817
12873
  const active = this.dom.getActive();
12818
12874
  if (!active.meta) {
12819
12875
  return false;
12820
12876
  }
12821
- const tagName = token.data[2];
12822
12877
  const open = !token.data[1];
12823
- const implicitClosed = active.meta.implicitClosed;
12824
12878
  if (open) {
12825
- if (!implicitClosed) {
12826
- return false;
12827
- }
12828
- const incomingMeta = this.metaTable.getMetaFor(tagName);
12829
- return implicitClosed.some((entry) => {
12830
- if (!entry.startsWith("@")) {
12831
- return entry === tagName;
12832
- }
12833
- return incomingMeta ? matchesContentCategory(incomingMeta, entry) : false;
12834
- });
12879
+ return this.wouldCloseElement(token, active);
12835
12880
  } else {
12836
- if (active.is(tagName)) {
12881
+ return this.closeOptionalEndTag(token, active);
12882
+ }
12883
+ }
12884
+ /**
12885
+ * Returns `true` if the element’s end tag may be omitted, either because
12886
+ * its `implicitClosed` list includes its own tag name (e.g. `<li>`, `<td>`)
12887
+ * or because `optionalEnd` is set.
12888
+ */
12889
+ canOmitEndTag(element) {
12890
+ if (!element.meta) {
12891
+ return false;
12892
+ }
12893
+ const { implicitClosed, optionalEnd } = element.meta;
12894
+ return Boolean(implicitClosed?.includes(element.tagName)) || Boolean(optionalEnd);
12895
+ }
12896
+ /**
12897
+ * Check whether the active element can be implicitly closed by an incoming
12898
+ * end tag. The end tag may close a direct parent or any ancestor, as long as
12899
+ * every intermediate element can also have its end tag omitted.
12900
+ * This handles cases like `</table>` implicitly closing `<td>`, `<tr>`, `<tbody>`.
12901
+ */
12902
+ closeOptionalEndTag(token, active) {
12903
+ const tagName = token.data[2];
12904
+ if (active.is(tagName)) {
12905
+ return false;
12906
+ }
12907
+ if (!this.canOmitEndTag(active)) {
12908
+ return false;
12909
+ }
12910
+ let ancestor = active.parent;
12911
+ while (ancestor && !ancestor.isRootElement()) {
12912
+ if (ancestor.is(tagName)) {
12913
+ return true;
12914
+ }
12915
+ if (!this.canOmitEndTag(ancestor)) {
12837
12916
  return false;
12838
12917
  }
12839
- const canOmitEnd = Boolean(implicitClosed?.includes(active.tagName)) || Boolean(active.meta.optionalEnd);
12840
- return Boolean(active.parent && active.parent.is(tagName) && canOmitEnd);
12918
+ ancestor = ancestor.parent;
12841
12919
  }
12920
+ return false;
12842
12921
  }
12843
12922
  /**
12844
12923
  * Check whether an intermediary element (e.g. `<head>` or `<body>`) should
@@ -12927,8 +13006,21 @@ class Parser {
12927
13006
  );
12928
13007
  const endToken = tokens.at(-1);
12929
13008
  const isStartTag = !startToken.data[1];
12930
- const closeOptional = this.closeOptional(startToken);
12931
- const baseParent = closeOptional ? this.dom.getActive().parent : this.dom.getActive();
13009
+ let baseParent;
13010
+ if (isStartTag) {
13011
+ baseParent = this.getParentAfterImplicitClose(startToken);
13012
+ } else {
13013
+ const tagName = startToken.data[2];
13014
+ let cur = this.dom.getActive();
13015
+ while (!cur.isRootElement() && !cur.is(tagName)) {
13016
+ const { parent: parent2 } = cur;
13017
+ if (!parent2) {
13018
+ break;
13019
+ }
13020
+ cur = parent2;
13021
+ }
13022
+ baseParent = cur;
13023
+ }
12932
13024
  const implicitParent = isStartTag ? this.peekImplicitOpen(startToken, baseParent) : null;
12933
13025
  const parent = implicitParent ?? baseParent;
12934
13026
  const node = HtmlElement.fromTokens(
@@ -12940,7 +13032,7 @@ class Parser {
12940
13032
  );
12941
13033
  const isClosing = !isStartTag || node.closed !== NodeClosed.Open;
12942
13034
  const isForeign = node.meta?.foreign;
12943
- if (closeOptional) {
13035
+ while (this.closeOptional(startToken)) {
12944
13036
  const active = this.dom.getActive();
12945
13037
  active.closed = NodeClosed.ImplicitClosed;
12946
13038
  this.closeElement(source, node, active, startToken.location);