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.
- package/dist/cjs/core.js +234 -31
- package/dist/cjs/core.js.map +1 -1
- package/dist/cjs/elements.js +76 -5
- package/dist/cjs/elements.js.map +1 -1
- package/dist/cjs/tsdoc-metadata.json +1 -1
- package/dist/esm/core.js +234 -31
- package/dist/esm/core.js.map +1 -1
- package/dist/esm/elements.js +76 -5
- package/dist/esm/elements.js.map +1 -1
- package/dist/schema/elements.json +25 -1
- package/dist/tsdoc-metadata.json +1 -1
- package/dist/types/browser.d.ts +70 -0
- package/dist/types/index.d.ts +70 -0
- package/package.json +1 -1
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
|
-
|
|
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.
|
|
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
|
|
12870
|
+
return this.wouldCloseElement(token, active);
|
|
12764
12871
|
} else {
|
|
12765
|
-
|
|
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
|
-
|
|
12769
|
-
|
|
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
|
|
12823
|
-
|
|
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
|
-
|
|
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,
|