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/cjs/core.js
CHANGED
|
@@ -591,12 +591,40 @@ const patternProperties = {
|
|
|
591
591
|
},
|
|
592
592
|
implicitClosed: {
|
|
593
593
|
title: "List of elements which implicitly closes this element",
|
|
594
|
-
description: "Some elements are automatically closed when another start tag occurs",
|
|
594
|
+
description: "Some elements are automatically closed when another start tag occurs. Entries may be explicit tag names or @category strings (e.g. \"@flow\").",
|
|
595
595
|
type: "array",
|
|
596
596
|
items: {
|
|
597
597
|
type: "string"
|
|
598
598
|
}
|
|
599
599
|
},
|
|
600
|
+
implicitOpen: {
|
|
601
|
+
title: "Implicit-open rules for child elements",
|
|
602
|
+
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.",
|
|
603
|
+
type: "array",
|
|
604
|
+
items: {
|
|
605
|
+
type: "object",
|
|
606
|
+
required: [
|
|
607
|
+
"for",
|
|
608
|
+
"open"
|
|
609
|
+
],
|
|
610
|
+
additionalProperties: false,
|
|
611
|
+
properties: {
|
|
612
|
+
"for": {
|
|
613
|
+
title: "Selector list",
|
|
614
|
+
description: "Tag names or @category strings (e.g. \"@flow\") that trigger the implicit open.",
|
|
615
|
+
type: "array",
|
|
616
|
+
items: {
|
|
617
|
+
type: "string"
|
|
618
|
+
}
|
|
619
|
+
},
|
|
620
|
+
open: {
|
|
621
|
+
title: "Element to open",
|
|
622
|
+
description: "Tag name of the element to implicitly open.",
|
|
623
|
+
type: "string"
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
},
|
|
600
628
|
optionalEnd: {
|
|
601
629
|
title: "Mark element as having an optional end tag",
|
|
602
630
|
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.",
|
|
@@ -2092,6 +2120,26 @@ class Compound {
|
|
|
2092
2120
|
}
|
|
2093
2121
|
}
|
|
2094
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
|
+
|
|
2095
2143
|
function* ancestors$1(element) {
|
|
2096
2144
|
let current = element.parent;
|
|
2097
2145
|
while (current && !current.isRootElement()) {
|
|
@@ -2214,24 +2262,6 @@ function unescapeCodepoint(value) {
|
|
|
2214
2262
|
(_, codepoint) => replacement[codepoint]
|
|
2215
2263
|
);
|
|
2216
2264
|
}
|
|
2217
|
-
function escapeSelectorComponent(text) {
|
|
2218
|
-
const codepoints = {
|
|
2219
|
-
" ": "\\9 ",
|
|
2220
|
-
"\n": "\\a ",
|
|
2221
|
-
"\r": "\\d "
|
|
2222
|
-
};
|
|
2223
|
-
return text.toString().replaceAll(/([\t\n\r]|[^\w-])/gi, (_, ch) => {
|
|
2224
|
-
if (codepoints[ch]) {
|
|
2225
|
-
return codepoints[ch];
|
|
2226
|
-
} else {
|
|
2227
|
-
return `\\${ch}`;
|
|
2228
|
-
}
|
|
2229
|
-
});
|
|
2230
|
-
}
|
|
2231
|
-
function generateIdSelector(id) {
|
|
2232
|
-
const escaped = escapeSelectorComponent(id);
|
|
2233
|
-
return /^\d/.test(escaped) ? `[id="${escaped}"]` : `#${escaped}`;
|
|
2234
|
-
}
|
|
2235
2265
|
class Selector {
|
|
2236
2266
|
pattern;
|
|
2237
2267
|
constructor(selector) {
|
|
@@ -2350,6 +2380,7 @@ class TextNode extends DOMNode {
|
|
|
2350
2380
|
}
|
|
2351
2381
|
}
|
|
2352
2382
|
|
|
2383
|
+
const CHILD_ELEMENTS = /* @__PURE__ */ Symbol("childElements");
|
|
2353
2384
|
const ROLE = /* @__PURE__ */ Symbol("role");
|
|
2354
2385
|
const TABINDEX = /* @__PURE__ */ Symbol("tabindex");
|
|
2355
2386
|
var NodeClosed = /* @__PURE__ */ ((NodeClosed2) => {
|
|
@@ -2516,7 +2547,11 @@ class HtmlElement extends DOMNode {
|
|
|
2516
2547
|
* Similar to childNodes but only elements.
|
|
2517
2548
|
*/
|
|
2518
2549
|
get childElements() {
|
|
2519
|
-
|
|
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));
|
|
2520
2555
|
}
|
|
2521
2556
|
/**
|
|
2522
2557
|
* Find the first ancestor matching a selector.
|
|
@@ -2912,12 +2947,24 @@ class HtmlElement extends DOMNode {
|
|
|
2912
2947
|
}
|
|
2913
2948
|
return visit(this);
|
|
2914
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
|
+
}
|
|
2915
2961
|
/**
|
|
2916
2962
|
* @internal
|
|
2917
2963
|
*/
|
|
2918
2964
|
_setParent(node) {
|
|
2919
2965
|
const oldParent = this._parent;
|
|
2920
2966
|
this._parent = node instanceof HtmlElement ? node : null;
|
|
2967
|
+
oldParent?.cacheRemove(CHILD_ELEMENTS);
|
|
2921
2968
|
return oldParent;
|
|
2922
2969
|
}
|
|
2923
2970
|
}
|
|
@@ -12485,7 +12532,7 @@ class EventHandler {
|
|
|
12485
12532
|
}
|
|
12486
12533
|
|
|
12487
12534
|
const name = "html-validate";
|
|
12488
|
-
const version = "10.
|
|
12535
|
+
const version = "10.14.0";
|
|
12489
12536
|
const bugs = "https://gitlab.com/html-validate/html-validate/issues/new";
|
|
12490
12537
|
|
|
12491
12538
|
function freeze(src) {
|
|
@@ -12674,6 +12721,31 @@ class ParserError extends Error {
|
|
|
12674
12721
|
}
|
|
12675
12722
|
}
|
|
12676
12723
|
|
|
12724
|
+
function isStaticTrue(value) {
|
|
12725
|
+
return value === true;
|
|
12726
|
+
}
|
|
12727
|
+
function matchesContentCategory(meta, category) {
|
|
12728
|
+
switch (category) {
|
|
12729
|
+
case "@meta":
|
|
12730
|
+
return isStaticTrue(meta.metadata);
|
|
12731
|
+
case "@flow":
|
|
12732
|
+
return isStaticTrue(meta.flow);
|
|
12733
|
+
case "@flow-not-meta":
|
|
12734
|
+
return isStaticTrue(meta.flow) && !isStaticTrue(meta.metadata);
|
|
12735
|
+
case "@sectioning":
|
|
12736
|
+
return isStaticTrue(meta.sectioning);
|
|
12737
|
+
case "@heading":
|
|
12738
|
+
return isStaticTrue(meta.heading);
|
|
12739
|
+
case "@phrasing":
|
|
12740
|
+
return isStaticTrue(meta.phrasing);
|
|
12741
|
+
case "@embedded":
|
|
12742
|
+
return isStaticTrue(meta.embedded);
|
|
12743
|
+
case "@interactive":
|
|
12744
|
+
return isStaticTrue(meta.interactive);
|
|
12745
|
+
default:
|
|
12746
|
+
return false;
|
|
12747
|
+
}
|
|
12748
|
+
}
|
|
12677
12749
|
function isAttrValueToken(token) {
|
|
12678
12750
|
return token?.type === TokenType.ATTR_VALUE;
|
|
12679
12751
|
}
|
|
@@ -12760,23 +12832,128 @@ class Parser {
|
|
|
12760
12832
|
* valid). The parser handles this by checking if the element on top of the
|
|
12761
12833
|
* stack when is allowed to omit.
|
|
12762
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
|
+
}
|
|
12763
12872
|
closeOptional(token) {
|
|
12764
12873
|
const active = this.dom.getActive();
|
|
12765
12874
|
if (!active.meta) {
|
|
12766
12875
|
return false;
|
|
12767
12876
|
}
|
|
12768
|
-
const tagName = token.data[2];
|
|
12769
12877
|
const open = !token.data[1];
|
|
12770
|
-
const implicitClosed = active.meta.implicitClosed;
|
|
12771
12878
|
if (open) {
|
|
12772
|
-
return
|
|
12879
|
+
return this.wouldCloseElement(token, active);
|
|
12773
12880
|
} else {
|
|
12774
|
-
|
|
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)) {
|
|
12775
12916
|
return false;
|
|
12776
12917
|
}
|
|
12777
|
-
|
|
12778
|
-
|
|
12918
|
+
ancestor = ancestor.parent;
|
|
12919
|
+
}
|
|
12920
|
+
return false;
|
|
12921
|
+
}
|
|
12922
|
+
/**
|
|
12923
|
+
* Check whether an intermediary element (e.g. `<head>` or `<body>`) should
|
|
12924
|
+
* be implicitly opened before the incoming element is inserted under
|
|
12925
|
+
* `parent`.
|
|
12926
|
+
*
|
|
12927
|
+
* If the parent's metadata defines an `implicitOpen` rule that matches the
|
|
12928
|
+
* incoming element, a new `HtmlElement` for the intermediary is created and
|
|
12929
|
+
* returned (with `parent` as its parent). The caller is responsible for
|
|
12930
|
+
* pushing it onto the active stack and firing the relevant events.
|
|
12931
|
+
*
|
|
12932
|
+
* Returns `null` when no implicit open is required.
|
|
12933
|
+
*/
|
|
12934
|
+
peekImplicitOpen(token, parent) {
|
|
12935
|
+
if (!parent?.meta?.implicitOpen) {
|
|
12936
|
+
return null;
|
|
12937
|
+
}
|
|
12938
|
+
const tagName = token.data[2];
|
|
12939
|
+
const incomingMeta = this.metaTable.getMetaFor(tagName);
|
|
12940
|
+
for (const entry of parent.meta.implicitOpen) {
|
|
12941
|
+
const matches = entry.for.some((selector) => {
|
|
12942
|
+
if (!selector.startsWith("@")) {
|
|
12943
|
+
return selector === tagName;
|
|
12944
|
+
}
|
|
12945
|
+
return incomingMeta ? matchesContentCategory(incomingMeta, selector) : false;
|
|
12946
|
+
});
|
|
12947
|
+
if (matches) {
|
|
12948
|
+
const intermediaryMeta = this.metaTable.getMetaFor(entry.open);
|
|
12949
|
+
return HtmlElement.createElement(entry.open, token.location, {
|
|
12950
|
+
closed: NodeClosed.Open,
|
|
12951
|
+
meta: intermediaryMeta,
|
|
12952
|
+
parent
|
|
12953
|
+
});
|
|
12954
|
+
}
|
|
12779
12955
|
}
|
|
12956
|
+
return null;
|
|
12780
12957
|
}
|
|
12781
12958
|
/**
|
|
12782
12959
|
* @internal
|
|
@@ -12828,8 +13005,24 @@ class Parser {
|
|
|
12828
13005
|
this.consumeUntil(tokenStream, TokenType.TAG_CLOSE, startToken.location)
|
|
12829
13006
|
);
|
|
12830
13007
|
const endToken = tokens.at(-1);
|
|
12831
|
-
const
|
|
12832
|
-
|
|
13008
|
+
const isStartTag = !startToken.data[1];
|
|
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
|
+
}
|
|
13024
|
+
const implicitParent = isStartTag ? this.peekImplicitOpen(startToken, baseParent) : null;
|
|
13025
|
+
const parent = implicitParent ?? baseParent;
|
|
12833
13026
|
const node = HtmlElement.fromTokens(
|
|
12834
13027
|
startToken,
|
|
12835
13028
|
endToken,
|
|
@@ -12837,16 +13030,26 @@ class Parser {
|
|
|
12837
13030
|
this.metaTable,
|
|
12838
13031
|
this.currentNamespace
|
|
12839
13032
|
);
|
|
12840
|
-
const isStartTag = !startToken.data[1];
|
|
12841
13033
|
const isClosing = !isStartTag || node.closed !== NodeClosed.Open;
|
|
12842
13034
|
const isForeign = node.meta?.foreign;
|
|
12843
|
-
|
|
13035
|
+
while (this.closeOptional(startToken)) {
|
|
12844
13036
|
const active = this.dom.getActive();
|
|
12845
13037
|
active.closed = NodeClosed.ImplicitClosed;
|
|
12846
13038
|
this.closeElement(source, node, active, startToken.location);
|
|
12847
13039
|
this.dom.popActive();
|
|
12848
13040
|
}
|
|
12849
13041
|
if (isStartTag) {
|
|
13042
|
+
if (implicitParent) {
|
|
13043
|
+
this.dom.pushActive(implicitParent);
|
|
13044
|
+
this.trigger("tag:start", {
|
|
13045
|
+
target: implicitParent,
|
|
13046
|
+
location: startToken.location
|
|
13047
|
+
});
|
|
13048
|
+
this.trigger("tag:ready", {
|
|
13049
|
+
target: implicitParent,
|
|
13050
|
+
location: startToken.location
|
|
13051
|
+
});
|
|
13052
|
+
}
|
|
12850
13053
|
this.dom.pushActive(node);
|
|
12851
13054
|
this.trigger("tag:start", {
|
|
12852
13055
|
target: node,
|