html-validate 10.13.0 → 10.13.1

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
@@ -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.",
@@ -12476,7 +12504,7 @@ class EventHandler {
12476
12504
  }
12477
12505
 
12478
12506
  const name = "html-validate";
12479
- const version = "10.13.0";
12507
+ const version = "10.13.1";
12480
12508
  const bugs = "https://gitlab.com/html-validate/html-validate/issues/new";
12481
12509
 
12482
12510
  function freeze(src) {
@@ -12665,6 +12693,31 @@ class ParserError extends Error {
12665
12693
  }
12666
12694
  }
12667
12695
 
12696
+ function isStaticTrue(value) {
12697
+ return value === true;
12698
+ }
12699
+ function matchesContentCategory(meta, category) {
12700
+ switch (category) {
12701
+ case "@meta":
12702
+ return isStaticTrue(meta.metadata);
12703
+ case "@flow":
12704
+ return isStaticTrue(meta.flow);
12705
+ case "@flow-not-meta":
12706
+ return isStaticTrue(meta.flow) && !isStaticTrue(meta.metadata);
12707
+ case "@sectioning":
12708
+ return isStaticTrue(meta.sectioning);
12709
+ case "@heading":
12710
+ return isStaticTrue(meta.heading);
12711
+ case "@phrasing":
12712
+ return isStaticTrue(meta.phrasing);
12713
+ case "@embedded":
12714
+ return isStaticTrue(meta.embedded);
12715
+ case "@interactive":
12716
+ return isStaticTrue(meta.interactive);
12717
+ default:
12718
+ return false;
12719
+ }
12720
+ }
12668
12721
  function isAttrValueToken(token) {
12669
12722
  return token?.type === TokenType.ATTR_VALUE;
12670
12723
  }
@@ -12760,7 +12813,16 @@ class Parser {
12760
12813
  const open = !token.data[1];
12761
12814
  const implicitClosed = active.meta.implicitClosed;
12762
12815
  if (open) {
12763
- return Boolean(implicitClosed?.includes(tagName));
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
+ });
12764
12826
  } else {
12765
12827
  if (active.is(tagName)) {
12766
12828
  return false;
@@ -12769,6 +12831,42 @@ class Parser {
12769
12831
  return Boolean(active.parent && active.parent.is(tagName) && canOmitEnd);
12770
12832
  }
12771
12833
  }
12834
+ /**
12835
+ * Check whether an intermediary element (e.g. `<head>` or `<body>`) should
12836
+ * be implicitly opened before the incoming element is inserted under
12837
+ * `parent`.
12838
+ *
12839
+ * If the parent's metadata defines an `implicitOpen` rule that matches the
12840
+ * incoming element, a new `HtmlElement` for the intermediary is created and
12841
+ * returned (with `parent` as its parent). The caller is responsible for
12842
+ * pushing it onto the active stack and firing the relevant events.
12843
+ *
12844
+ * Returns `null` when no implicit open is required.
12845
+ */
12846
+ peekImplicitOpen(token, parent) {
12847
+ if (!parent?.meta?.implicitOpen) {
12848
+ return null;
12849
+ }
12850
+ const tagName = token.data[2];
12851
+ const incomingMeta = this.metaTable.getMetaFor(tagName);
12852
+ for (const entry of parent.meta.implicitOpen) {
12853
+ const matches = entry.for.some((selector) => {
12854
+ if (!selector.startsWith("@")) {
12855
+ return selector === tagName;
12856
+ }
12857
+ return incomingMeta ? matchesContentCategory(incomingMeta, selector) : false;
12858
+ });
12859
+ if (matches) {
12860
+ const intermediaryMeta = this.metaTable.getMetaFor(entry.open);
12861
+ return HtmlElement.createElement(entry.open, token.location, {
12862
+ closed: NodeClosed.Open,
12863
+ meta: intermediaryMeta,
12864
+ parent
12865
+ });
12866
+ }
12867
+ }
12868
+ return null;
12869
+ }
12772
12870
  /**
12773
12871
  * @internal
12774
12872
  */
@@ -12819,8 +12917,11 @@ class Parser {
12819
12917
  this.consumeUntil(tokenStream, TokenType.TAG_CLOSE, startToken.location)
12820
12918
  );
12821
12919
  const endToken = tokens.at(-1);
12920
+ const isStartTag = !startToken.data[1];
12822
12921
  const closeOptional = this.closeOptional(startToken);
12823
- const parent = closeOptional ? this.dom.getActive().parent : this.dom.getActive();
12922
+ const baseParent = closeOptional ? this.dom.getActive().parent : this.dom.getActive();
12923
+ const implicitParent = isStartTag ? this.peekImplicitOpen(startToken, baseParent) : null;
12924
+ const parent = implicitParent ?? baseParent;
12824
12925
  const node = HtmlElement.fromTokens(
12825
12926
  startToken,
12826
12927
  endToken,
@@ -12828,7 +12929,6 @@ class Parser {
12828
12929
  this.metaTable,
12829
12930
  this.currentNamespace
12830
12931
  );
12831
- const isStartTag = !startToken.data[1];
12832
12932
  const isClosing = !isStartTag || node.closed !== NodeClosed.Open;
12833
12933
  const isForeign = node.meta?.foreign;
12834
12934
  if (closeOptional) {
@@ -12838,6 +12938,17 @@ class Parser {
12838
12938
  this.dom.popActive();
12839
12939
  }
12840
12940
  if (isStartTag) {
12941
+ if (implicitParent) {
12942
+ this.dom.pushActive(implicitParent);
12943
+ this.trigger("tag:start", {
12944
+ target: implicitParent,
12945
+ location: startToken.location
12946
+ });
12947
+ this.trigger("tag:ready", {
12948
+ target: implicitParent,
12949
+ location: startToken.location
12950
+ });
12951
+ }
12841
12952
  this.dom.pushActive(node);
12842
12953
  this.trigger("tag:start", {
12843
12954
  target: node,