html-validate 10.13.0 → 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.
@@ -5,7 +5,7 @@
5
5
  "toolPackages": [
6
6
  {
7
7
  "packageName": "@microsoft/api-extractor",
8
- "packageVersion": "7.58.2"
8
+ "packageVersion": "7.58.7"
9
9
  }
10
10
  ]
11
11
  }
package/dist/esm/core.js CHANGED
@@ -582,12 +582,40 @@ const patternProperties = {
582
582
  },
583
583
  implicitClosed: {
584
584
  title: "List of elements which implicitly closes this element",
585
- description: "Some elements are automatically closed when another start tag occurs",
585
+ description: "Some elements are automatically closed when another start tag occurs. Entries may be explicit tag names or @category strings (e.g. \"@flow\").",
586
586
  type: "array",
587
587
  items: {
588
588
  type: "string"
589
589
  }
590
590
  },
591
+ implicitOpen: {
592
+ title: "Implicit-open rules for child elements",
593
+ description: "Describes intermediary elements (e.g. <head> or <body>) that should be implicitly opened when a child of a given category or tag is inserted directly under this element without a matching container being present.",
594
+ type: "array",
595
+ items: {
596
+ type: "object",
597
+ required: [
598
+ "for",
599
+ "open"
600
+ ],
601
+ additionalProperties: false,
602
+ properties: {
603
+ "for": {
604
+ title: "Selector list",
605
+ description: "Tag names or @category strings (e.g. \"@flow\") that trigger the implicit open.",
606
+ type: "array",
607
+ items: {
608
+ type: "string"
609
+ }
610
+ },
611
+ open: {
612
+ title: "Element to open",
613
+ description: "Tag name of the element to implicitly open.",
614
+ type: "string"
615
+ }
616
+ }
617
+ }
618
+ },
591
619
  optionalEnd: {
592
620
  title: "Mark element as having an optional end tag",
593
621
  description: "Elements whose end tag may be omitted per the HTML spec. Such an element is treated as implicitly closed at end-of-document and when a parent’s explicit end tag is encountered while it is still open.",
@@ -2083,6 +2111,26 @@ class Compound {
2083
2111
  }
2084
2112
  }
2085
2113
 
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}`;
2125
+ }
2126
+ });
2127
+ }
2128
+
2129
+ function generateIdSelector(id) {
2130
+ const escaped = escapeSelectorComponent(id);
2131
+ return /^\d/.test(escaped) ? `[id="${escaped}"]` : `#${escaped}`;
2132
+ }
2133
+
2086
2134
  function* ancestors$1(element) {
2087
2135
  let current = element.parent;
2088
2136
  while (current && !current.isRootElement()) {
@@ -2205,24 +2253,6 @@ function unescapeCodepoint(value) {
2205
2253
  (_, codepoint) => replacement[codepoint]
2206
2254
  );
2207
2255
  }
2208
- function escapeSelectorComponent(text) {
2209
- const codepoints = {
2210
- " ": "\\9 ",
2211
- "\n": "\\a ",
2212
- "\r": "\\d "
2213
- };
2214
- return text.toString().replaceAll(/([\t\n\r]|[^\w-])/gi, (_, ch) => {
2215
- if (codepoints[ch]) {
2216
- return codepoints[ch];
2217
- } else {
2218
- return `\\${ch}`;
2219
- }
2220
- });
2221
- }
2222
- function generateIdSelector(id) {
2223
- const escaped = escapeSelectorComponent(id);
2224
- return /^\d/.test(escaped) ? `[id="${escaped}"]` : `#${escaped}`;
2225
- }
2226
2256
  class Selector {
2227
2257
  pattern;
2228
2258
  constructor(selector) {
@@ -2341,6 +2371,7 @@ class TextNode extends DOMNode {
2341
2371
  }
2342
2372
  }
2343
2373
 
2374
+ const CHILD_ELEMENTS = /* @__PURE__ */ Symbol("childElements");
2344
2375
  const ROLE = /* @__PURE__ */ Symbol("role");
2345
2376
  const TABINDEX = /* @__PURE__ */ Symbol("tabindex");
2346
2377
  var NodeClosed = /* @__PURE__ */ ((NodeClosed2) => {
@@ -2507,7 +2538,11 @@ class HtmlElement extends DOMNode {
2507
2538
  * Similar to childNodes but only elements.
2508
2539
  */
2509
2540
  get childElements() {
2510
- return this.childNodes.filter(isElementNode);
2541
+ const cached = this.cacheGet(CHILD_ELEMENTS);
2542
+ if (cached !== void 0) {
2543
+ return cached;
2544
+ }
2545
+ return this.cacheSet(CHILD_ELEMENTS, this.childNodes.filter(isElementNode));
2511
2546
  }
2512
2547
  /**
2513
2548
  * Find the first ancestor matching a selector.
@@ -2903,12 +2938,24 @@ class HtmlElement extends DOMNode {
2903
2938
  }
2904
2939
  return visit(this);
2905
2940
  }
2941
+ append(node) {
2942
+ super.append(node);
2943
+ this.cacheRemove(CHILD_ELEMENTS);
2944
+ }
2945
+ insertBefore(node, reference) {
2946
+ super.insertBefore(node, reference);
2947
+ this.cacheRemove(CHILD_ELEMENTS);
2948
+ }
2949
+ removeChild(node) {
2950
+ return super.removeChild(node);
2951
+ }
2906
2952
  /**
2907
2953
  * @internal
2908
2954
  */
2909
2955
  _setParent(node) {
2910
2956
  const oldParent = this._parent;
2911
2957
  this._parent = node instanceof HtmlElement ? node : null;
2958
+ oldParent?.cacheRemove(CHILD_ELEMENTS);
2912
2959
  return oldParent;
2913
2960
  }
2914
2961
  }
@@ -12476,7 +12523,7 @@ class EventHandler {
12476
12523
  }
12477
12524
 
12478
12525
  const name = "html-validate";
12479
- const version = "10.13.0";
12526
+ const version = "10.14.0";
12480
12527
  const bugs = "https://gitlab.com/html-validate/html-validate/issues/new";
12481
12528
 
12482
12529
  function freeze(src) {
@@ -12665,6 +12712,31 @@ class ParserError extends Error {
12665
12712
  }
12666
12713
  }
12667
12714
 
12715
+ function isStaticTrue(value) {
12716
+ return value === true;
12717
+ }
12718
+ function matchesContentCategory(meta, category) {
12719
+ switch (category) {
12720
+ case "@meta":
12721
+ return isStaticTrue(meta.metadata);
12722
+ case "@flow":
12723
+ return isStaticTrue(meta.flow);
12724
+ case "@flow-not-meta":
12725
+ return isStaticTrue(meta.flow) && !isStaticTrue(meta.metadata);
12726
+ case "@sectioning":
12727
+ return isStaticTrue(meta.sectioning);
12728
+ case "@heading":
12729
+ return isStaticTrue(meta.heading);
12730
+ case "@phrasing":
12731
+ return isStaticTrue(meta.phrasing);
12732
+ case "@embedded":
12733
+ return isStaticTrue(meta.embedded);
12734
+ case "@interactive":
12735
+ return isStaticTrue(meta.interactive);
12736
+ default:
12737
+ return false;
12738
+ }
12739
+ }
12668
12740
  function isAttrValueToken(token) {
12669
12741
  return token?.type === TokenType.ATTR_VALUE;
12670
12742
  }
@@ -12751,23 +12823,128 @@ class Parser {
12751
12823
  * valid). The parser handles this by checking if the element on top of the
12752
12824
  * stack when is allowed to omit.
12753
12825
  */
12826
+ /**
12827
+ * Check whether a given element would be implicitly closed by an incoming
12828
+ * start tag. Used both in `closeOptional` and in the multi-level lookahead.
12829
+ */
12830
+ wouldCloseElement(token, element) {
12831
+ if (!element.meta) {
12832
+ return false;
12833
+ }
12834
+ const implicitClosed = element.meta.implicitClosed;
12835
+ if (!implicitClosed) {
12836
+ return false;
12837
+ }
12838
+ const tagName = token.data[2];
12839
+ const incomingMeta = this.metaTable.getMetaFor(tagName);
12840
+ return implicitClosed.some((entry) => {
12841
+ if (!entry.startsWith("@")) {
12842
+ return entry === tagName;
12843
+ }
12844
+ return incomingMeta ? matchesContentCategory(incomingMeta, entry) : false;
12845
+ });
12846
+ }
12847
+ /**
12848
+ * Walk up the active stack to find the parent element that will remain
12849
+ * after all multi-level implicit closes triggered by the incoming start tag.
12850
+ * For a single-level close this is equivalent to `getActive().parent`.
12851
+ */
12852
+ getParentAfterImplicitClose(token) {
12853
+ let current = this.dom.getActive();
12854
+ while (!current.isRootElement() && this.wouldCloseElement(token, current)) {
12855
+ const { parent } = current;
12856
+ if (!parent) {
12857
+ break;
12858
+ }
12859
+ current = parent;
12860
+ }
12861
+ return current;
12862
+ }
12754
12863
  closeOptional(token) {
12755
12864
  const active = this.dom.getActive();
12756
12865
  if (!active.meta) {
12757
12866
  return false;
12758
12867
  }
12759
- const tagName = token.data[2];
12760
12868
  const open = !token.data[1];
12761
- const implicitClosed = active.meta.implicitClosed;
12762
12869
  if (open) {
12763
- return Boolean(implicitClosed?.includes(tagName));
12870
+ return this.wouldCloseElement(token, active);
12764
12871
  } else {
12765
- if (active.is(tagName)) {
12872
+ return this.closeOptionalEndTag(token, active);
12873
+ }
12874
+ }
12875
+ /**
12876
+ * Returns `true` if the element’s end tag may be omitted, either because
12877
+ * its `implicitClosed` list includes its own tag name (e.g. `<li>`, `<td>`)
12878
+ * or because `optionalEnd` is set.
12879
+ */
12880
+ canOmitEndTag(element) {
12881
+ if (!element.meta) {
12882
+ return false;
12883
+ }
12884
+ const { implicitClosed, optionalEnd } = element.meta;
12885
+ return Boolean(implicitClosed?.includes(element.tagName)) || Boolean(optionalEnd);
12886
+ }
12887
+ /**
12888
+ * Check whether the active element can be implicitly closed by an incoming
12889
+ * end tag. The end tag may close a direct parent or any ancestor, as long as
12890
+ * every intermediate element can also have its end tag omitted.
12891
+ * This handles cases like `</table>` implicitly closing `<td>`, `<tr>`, `<tbody>`.
12892
+ */
12893
+ closeOptionalEndTag(token, active) {
12894
+ const tagName = token.data[2];
12895
+ if (active.is(tagName)) {
12896
+ return false;
12897
+ }
12898
+ if (!this.canOmitEndTag(active)) {
12899
+ return false;
12900
+ }
12901
+ let ancestor = active.parent;
12902
+ while (ancestor && !ancestor.isRootElement()) {
12903
+ if (ancestor.is(tagName)) {
12904
+ return true;
12905
+ }
12906
+ if (!this.canOmitEndTag(ancestor)) {
12766
12907
  return false;
12767
12908
  }
12768
- const canOmitEnd = Boolean(implicitClosed?.includes(active.tagName)) || Boolean(active.meta.optionalEnd);
12769
- return Boolean(active.parent && active.parent.is(tagName) && canOmitEnd);
12909
+ ancestor = ancestor.parent;
12910
+ }
12911
+ return false;
12912
+ }
12913
+ /**
12914
+ * Check whether an intermediary element (e.g. `<head>` or `<body>`) should
12915
+ * be implicitly opened before the incoming element is inserted under
12916
+ * `parent`.
12917
+ *
12918
+ * If the parent's metadata defines an `implicitOpen` rule that matches the
12919
+ * incoming element, a new `HtmlElement` for the intermediary is created and
12920
+ * returned (with `parent` as its parent). The caller is responsible for
12921
+ * pushing it onto the active stack and firing the relevant events.
12922
+ *
12923
+ * Returns `null` when no implicit open is required.
12924
+ */
12925
+ peekImplicitOpen(token, parent) {
12926
+ if (!parent?.meta?.implicitOpen) {
12927
+ return null;
12928
+ }
12929
+ const tagName = token.data[2];
12930
+ const incomingMeta = this.metaTable.getMetaFor(tagName);
12931
+ for (const entry of parent.meta.implicitOpen) {
12932
+ const matches = entry.for.some((selector) => {
12933
+ if (!selector.startsWith("@")) {
12934
+ return selector === tagName;
12935
+ }
12936
+ return incomingMeta ? matchesContentCategory(incomingMeta, selector) : false;
12937
+ });
12938
+ if (matches) {
12939
+ const intermediaryMeta = this.metaTable.getMetaFor(entry.open);
12940
+ return HtmlElement.createElement(entry.open, token.location, {
12941
+ closed: NodeClosed.Open,
12942
+ meta: intermediaryMeta,
12943
+ parent
12944
+ });
12945
+ }
12770
12946
  }
12947
+ return null;
12771
12948
  }
12772
12949
  /**
12773
12950
  * @internal
@@ -12819,8 +12996,24 @@ class Parser {
12819
12996
  this.consumeUntil(tokenStream, TokenType.TAG_CLOSE, startToken.location)
12820
12997
  );
12821
12998
  const endToken = tokens.at(-1);
12822
- const closeOptional = this.closeOptional(startToken);
12823
- const parent = closeOptional ? this.dom.getActive().parent : this.dom.getActive();
12999
+ const isStartTag = !startToken.data[1];
13000
+ let baseParent;
13001
+ if (isStartTag) {
13002
+ baseParent = this.getParentAfterImplicitClose(startToken);
13003
+ } else {
13004
+ const tagName = startToken.data[2];
13005
+ let cur = this.dom.getActive();
13006
+ while (!cur.isRootElement() && !cur.is(tagName)) {
13007
+ const { parent: parent2 } = cur;
13008
+ if (!parent2) {
13009
+ break;
13010
+ }
13011
+ cur = parent2;
13012
+ }
13013
+ baseParent = cur;
13014
+ }
13015
+ const implicitParent = isStartTag ? this.peekImplicitOpen(startToken, baseParent) : null;
13016
+ const parent = implicitParent ?? baseParent;
12824
13017
  const node = HtmlElement.fromTokens(
12825
13018
  startToken,
12826
13019
  endToken,
@@ -12828,16 +13021,26 @@ class Parser {
12828
13021
  this.metaTable,
12829
13022
  this.currentNamespace
12830
13023
  );
12831
- const isStartTag = !startToken.data[1];
12832
13024
  const isClosing = !isStartTag || node.closed !== NodeClosed.Open;
12833
13025
  const isForeign = node.meta?.foreign;
12834
- if (closeOptional) {
13026
+ while (this.closeOptional(startToken)) {
12835
13027
  const active = this.dom.getActive();
12836
13028
  active.closed = NodeClosed.ImplicitClosed;
12837
13029
  this.closeElement(source, node, active, startToken.location);
12838
13030
  this.dom.popActive();
12839
13031
  }
12840
13032
  if (isStartTag) {
13033
+ if (implicitParent) {
13034
+ this.dom.pushActive(implicitParent);
13035
+ this.trigger("tag:start", {
13036
+ target: implicitParent,
13037
+ location: startToken.location
13038
+ });
13039
+ this.trigger("tag:ready", {
13040
+ target: implicitParent,
13041
+ location: startToken.location
13042
+ });
13043
+ }
12841
13044
  this.dom.pushActive(node);
12842
13045
  this.trigger("tag:start", {
12843
13046
  target: node,