html-validate 10.13.1 → 10.15.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/browser.js +1 -0
- package/dist/cjs/browser.js.map +1 -1
- package/dist/cjs/cli.js +50 -4
- package/dist/cjs/cli.js.map +1 -1
- package/dist/cjs/core-browser.js +60 -11
- package/dist/cjs/core-browser.js.map +1 -1
- package/dist/cjs/core-nodejs.js +60 -11
- package/dist/cjs/core-nodejs.js.map +1 -1
- package/dist/cjs/core.js +420 -198
- package/dist/cjs/core.js.map +1 -1
- package/dist/cjs/elements.js +71 -4
- package/dist/cjs/elements.js.map +1 -1
- package/dist/cjs/html-validate.js +4 -1
- package/dist/cjs/html-validate.js.map +1 -1
- package/dist/cjs/index.js +1 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/tsdoc-metadata.json +1 -1
- package/dist/esm/browser.js +1 -1
- package/dist/esm/cli.js +51 -5
- package/dist/esm/cli.js.map +1 -1
- package/dist/esm/core-browser.js +61 -12
- package/dist/esm/core-browser.js.map +1 -1
- package/dist/esm/core-nodejs.js +61 -12
- package/dist/esm/core-nodejs.js.map +1 -1
- package/dist/esm/core.js +420 -199
- package/dist/esm/core.js.map +1 -1
- package/dist/esm/elements.js +71 -4
- package/dist/esm/elements.js.map +1 -1
- package/dist/esm/html-validate.js +5 -2
- package/dist/esm/html-validate.js.map +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/matcher-utils.js +1 -1
- package/dist/esm/matchers.js +1 -1
- package/dist/tsdoc-metadata.json +1 -1
- package/dist/types/browser.d.ts +42 -0
- package/dist/types/index.d.ts +42 -0
- package/package.json +1 -1
package/dist/cjs/core.js
CHANGED
|
@@ -1204,9 +1204,6 @@ function clone(value) {
|
|
|
1204
1204
|
return JSON.parse(JSON.stringify(value));
|
|
1205
1205
|
}
|
|
1206
1206
|
}
|
|
1207
|
-
function overwriteMerge$1(_a, b) {
|
|
1208
|
-
return b;
|
|
1209
|
-
}
|
|
1210
1207
|
class MetaTable {
|
|
1211
1208
|
elements;
|
|
1212
1209
|
schema;
|
|
@@ -1379,15 +1376,22 @@ class MetaTable {
|
|
|
1379
1376
|
}
|
|
1380
1377
|
}
|
|
1381
1378
|
mergeElement(a, b) {
|
|
1382
|
-
const merged =
|
|
1383
|
-
const
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1379
|
+
const merged = { ...a, ...b };
|
|
1380
|
+
const mergedAttrs = {
|
|
1381
|
+
...a.attributes,
|
|
1382
|
+
...b.attributes
|
|
1383
|
+
};
|
|
1384
|
+
for (const [name, attr] of Object.entries(mergedAttrs)) {
|
|
1385
|
+
if (attr.delete) {
|
|
1386
|
+
delete mergedAttrs[name];
|
|
1387
|
+
} else {
|
|
1388
|
+
delete attr.delete;
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
merged.attributes = mergedAttrs;
|
|
1392
|
+
if (a.aria) {
|
|
1393
|
+
merged.aria = { ...a.aria, ...b.aria };
|
|
1394
|
+
}
|
|
1391
1395
|
return merged;
|
|
1392
1396
|
}
|
|
1393
1397
|
/**
|
|
@@ -1968,71 +1972,68 @@ function factory(name, context) {
|
|
|
1968
1972
|
function stripslashes(value) {
|
|
1969
1973
|
return value.replaceAll(/\\(.)/g, "$1");
|
|
1970
1974
|
}
|
|
1971
|
-
|
|
1975
|
+
function createClassCondition(classname) {
|
|
1976
|
+
return {
|
|
1977
|
+
kind: "class",
|
|
1978
|
+
classname,
|
|
1979
|
+
match(node) {
|
|
1980
|
+
return node.classList.contains(classname);
|
|
1981
|
+
}
|
|
1982
|
+
};
|
|
1972
1983
|
}
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
}
|
|
1984
|
+
function createIdCondition(raw) {
|
|
1985
|
+
const id = stripslashes(raw);
|
|
1986
|
+
return {
|
|
1987
|
+
kind: "id",
|
|
1988
|
+
id,
|
|
1989
|
+
match(node) {
|
|
1990
|
+
return node.id === id;
|
|
1991
|
+
}
|
|
1992
|
+
};
|
|
1982
1993
|
}
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1994
|
+
function createAttributeCondition(attr) {
|
|
1995
|
+
const match = /^(.+?)(?:([$*^|~]?=)"([^"]+?)")?$/.exec(attr);
|
|
1996
|
+
const key = match[1];
|
|
1997
|
+
const op = match[2];
|
|
1998
|
+
const rawValue = match[3];
|
|
1999
|
+
const value = typeof rawValue === "string" ? stripslashes(rawValue) : rawValue;
|
|
2000
|
+
return {
|
|
2001
|
+
kind: "attribute",
|
|
2002
|
+
key,
|
|
2003
|
+
op,
|
|
2004
|
+
value,
|
|
2005
|
+
match(node) {
|
|
2006
|
+
const attrs = node.getAttribute(key, true);
|
|
2007
|
+
return attrs.some((cur) => {
|
|
2008
|
+
switch (op) {
|
|
2009
|
+
case void 0:
|
|
2010
|
+
return true;
|
|
2011
|
+
/* attribute exists */
|
|
2012
|
+
case "=":
|
|
2013
|
+
return cur.value === value;
|
|
2014
|
+
default:
|
|
2015
|
+
throw new Error(`Attribute selector operator ${op} is not implemented yet`);
|
|
2016
|
+
}
|
|
2017
|
+
});
|
|
2018
|
+
}
|
|
2019
|
+
};
|
|
1992
2020
|
}
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
constructor(attr) {
|
|
1998
|
-
super();
|
|
1999
|
-
const [, key, op, value] = /^(.+?)(?:([$*^|~]?=)"([^"]+?)")?$/.exec(attr);
|
|
2000
|
-
this.key = key;
|
|
2001
|
-
this.op = op;
|
|
2002
|
-
this.value = typeof value === "string" ? stripslashes(value) : value;
|
|
2003
|
-
}
|
|
2004
|
-
match(node) {
|
|
2005
|
-
const attr = node.getAttribute(this.key, true);
|
|
2006
|
-
return attr.some((cur) => {
|
|
2007
|
-
switch (this.op) {
|
|
2008
|
-
case void 0:
|
|
2009
|
-
return true;
|
|
2010
|
-
/* attribute exists */
|
|
2011
|
-
case "=":
|
|
2012
|
-
return cur.value === this.value;
|
|
2013
|
-
default:
|
|
2014
|
-
throw new Error(`Attribute selector operator ${this.op} is not implemented yet`);
|
|
2015
|
-
}
|
|
2016
|
-
});
|
|
2021
|
+
function createPseudoClassCondition(pseudoclass, context) {
|
|
2022
|
+
const match = /^([^(]+)(?:\((.*)\))?$/.exec(pseudoclass);
|
|
2023
|
+
if (!match) {
|
|
2024
|
+
throw new Error(`Missing pseudo-class after colon in selector pattern "${context}"`);
|
|
2017
2025
|
}
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2026
|
+
const name = match[1];
|
|
2027
|
+
const args = match[2];
|
|
2028
|
+
return {
|
|
2029
|
+
kind: "pseudo",
|
|
2030
|
+
name,
|
|
2031
|
+
args,
|
|
2032
|
+
match(node, selectorContext) {
|
|
2033
|
+
const fn = factory(name, selectorContext);
|
|
2034
|
+
return fn(node, args);
|
|
2027
2035
|
}
|
|
2028
|
-
|
|
2029
|
-
this.name = name;
|
|
2030
|
-
this.args = args;
|
|
2031
|
-
}
|
|
2032
|
-
match(node, context) {
|
|
2033
|
-
const fn = factory(this.name, context);
|
|
2034
|
-
return fn(node, this.args);
|
|
2035
|
-
}
|
|
2036
|
+
};
|
|
2036
2037
|
}
|
|
2037
2038
|
|
|
2038
2039
|
function isDelimiter(ch) {
|
|
@@ -2107,19 +2108,86 @@ class Compound {
|
|
|
2107
2108
|
createCondition(pattern) {
|
|
2108
2109
|
switch (pattern[0]) {
|
|
2109
2110
|
case ".":
|
|
2110
|
-
return
|
|
2111
|
+
return createClassCondition(pattern.slice(1));
|
|
2111
2112
|
case "#":
|
|
2112
|
-
return
|
|
2113
|
+
return createIdCondition(pattern.slice(1));
|
|
2113
2114
|
case "[":
|
|
2114
|
-
return
|
|
2115
|
+
return createAttributeCondition(pattern.slice(1, -1));
|
|
2115
2116
|
case ":":
|
|
2116
|
-
return
|
|
2117
|
+
return createPseudoClassCondition(pattern.slice(1), this.selector);
|
|
2117
2118
|
default:
|
|
2118
2119
|
throw new Error(`Failed to create selector condition for "${pattern}"`);
|
|
2119
2120
|
}
|
|
2120
2121
|
}
|
|
2121
2122
|
}
|
|
2122
2123
|
|
|
2124
|
+
const escapedCodepoints = /* @__PURE__ */ new Set(["9", "a", "d"]);
|
|
2125
|
+
function* splitSelectorElements(selector) {
|
|
2126
|
+
let begin = 0;
|
|
2127
|
+
let end = 0;
|
|
2128
|
+
function initialState(ch, p) {
|
|
2129
|
+
if (ch === "\\") {
|
|
2130
|
+
return 1 /* ESCAPED */;
|
|
2131
|
+
}
|
|
2132
|
+
if (ch === " ") {
|
|
2133
|
+
end = p;
|
|
2134
|
+
return 2 /* WHITESPACE */;
|
|
2135
|
+
}
|
|
2136
|
+
return 0 /* INITIAL */;
|
|
2137
|
+
}
|
|
2138
|
+
function escapedState(ch) {
|
|
2139
|
+
if (escapedCodepoints.has(ch)) {
|
|
2140
|
+
return 1 /* ESCAPED */;
|
|
2141
|
+
}
|
|
2142
|
+
return 0 /* INITIAL */;
|
|
2143
|
+
}
|
|
2144
|
+
function* whitespaceState(ch, p) {
|
|
2145
|
+
if (ch === " ") {
|
|
2146
|
+
return 2 /* WHITESPACE */;
|
|
2147
|
+
}
|
|
2148
|
+
yield selector.slice(begin, end);
|
|
2149
|
+
begin = p;
|
|
2150
|
+
end = p;
|
|
2151
|
+
return 0 /* INITIAL */;
|
|
2152
|
+
}
|
|
2153
|
+
let state = 0 /* INITIAL */;
|
|
2154
|
+
for (let p = 0; p < selector.length; p++) {
|
|
2155
|
+
const ch = selector[p];
|
|
2156
|
+
switch (state) {
|
|
2157
|
+
case 0 /* INITIAL */:
|
|
2158
|
+
state = initialState(ch, p);
|
|
2159
|
+
break;
|
|
2160
|
+
case 1 /* ESCAPED */:
|
|
2161
|
+
state = escapedState(ch);
|
|
2162
|
+
break;
|
|
2163
|
+
case 2 /* WHITESPACE */:
|
|
2164
|
+
state = yield* whitespaceState(ch, p);
|
|
2165
|
+
break;
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
if (begin !== selector.length) {
|
|
2169
|
+
yield selector.slice(begin);
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
function unescapeCodepoint(value) {
|
|
2174
|
+
const replacement = {
|
|
2175
|
+
"\\9 ": " ",
|
|
2176
|
+
"\\a ": "\n",
|
|
2177
|
+
"\\d ": "\r"
|
|
2178
|
+
};
|
|
2179
|
+
return value.replaceAll(
|
|
2180
|
+
/(\\[9ad] )/g,
|
|
2181
|
+
(_, codepoint) => replacement[codepoint]
|
|
2182
|
+
);
|
|
2183
|
+
}
|
|
2184
|
+
function getCompounds(selector) {
|
|
2185
|
+
selector = selector.replaceAll(/([+>~]) /g, "$1");
|
|
2186
|
+
return Array.from(splitSelectorElements(selector), (element) => {
|
|
2187
|
+
return new Compound(unescapeCodepoint(element));
|
|
2188
|
+
});
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2123
2191
|
function* ancestors$1(element) {
|
|
2124
2192
|
let current = element.parent;
|
|
2125
2193
|
while (current && !current.isRootElement()) {
|
|
@@ -2182,88 +2250,16 @@ function matchElement(element, compounds, context) {
|
|
|
2182
2250
|
return false;
|
|
2183
2251
|
}
|
|
2184
2252
|
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
function initialState(ch, p) {
|
|
2190
|
-
if (ch === "\\") {
|
|
2191
|
-
return 1 /* ESCAPED */;
|
|
2192
|
-
}
|
|
2193
|
-
if (ch === " ") {
|
|
2194
|
-
end = p;
|
|
2195
|
-
return 2 /* WHITESPACE */;
|
|
2196
|
-
}
|
|
2197
|
-
return 0 /* INITIAL */;
|
|
2198
|
-
}
|
|
2199
|
-
function escapedState(ch) {
|
|
2200
|
-
if (escapedCodepoints.has(ch)) {
|
|
2201
|
-
return 1 /* ESCAPED */;
|
|
2202
|
-
}
|
|
2203
|
-
return 0 /* INITIAL */;
|
|
2204
|
-
}
|
|
2205
|
-
function* whitespaceState(ch, p) {
|
|
2206
|
-
if (ch === " ") {
|
|
2207
|
-
return 2 /* WHITESPACE */;
|
|
2208
|
-
}
|
|
2209
|
-
yield selector.slice(begin, end);
|
|
2210
|
-
begin = p;
|
|
2211
|
-
end = p;
|
|
2212
|
-
return 0 /* INITIAL */;
|
|
2253
|
+
class ComplexSelector {
|
|
2254
|
+
compounds;
|
|
2255
|
+
constructor(compounds) {
|
|
2256
|
+
this.compounds = compounds;
|
|
2213
2257
|
}
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
const ch = selector[p];
|
|
2217
|
-
switch (state) {
|
|
2218
|
-
case 0 /* INITIAL */:
|
|
2219
|
-
state = initialState(ch, p);
|
|
2220
|
-
break;
|
|
2221
|
-
case 1 /* ESCAPED */:
|
|
2222
|
-
state = escapedState(ch);
|
|
2223
|
-
break;
|
|
2224
|
-
case 2 /* WHITESPACE */:
|
|
2225
|
-
state = yield* whitespaceState(ch, p);
|
|
2226
|
-
break;
|
|
2227
|
-
}
|
|
2258
|
+
static fromString(selector) {
|
|
2259
|
+
return new ComplexSelector(getCompounds(selector));
|
|
2228
2260
|
}
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
}
|
|
2232
|
-
}
|
|
2233
|
-
|
|
2234
|
-
function unescapeCodepoint(value) {
|
|
2235
|
-
const replacement = {
|
|
2236
|
-
"\\9 ": " ",
|
|
2237
|
-
"\\a ": "\n",
|
|
2238
|
-
"\\d ": "\r"
|
|
2239
|
-
};
|
|
2240
|
-
return value.replaceAll(
|
|
2241
|
-
/(\\[9ad] )/g,
|
|
2242
|
-
(_, codepoint) => replacement[codepoint]
|
|
2243
|
-
);
|
|
2244
|
-
}
|
|
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
|
-
class Selector {
|
|
2264
|
-
pattern;
|
|
2265
|
-
constructor(selector) {
|
|
2266
|
-
this.pattern = Selector.parse(selector);
|
|
2261
|
+
static fromCompounds(compounds) {
|
|
2262
|
+
return new ComplexSelector(compounds);
|
|
2267
2263
|
}
|
|
2268
2264
|
/**
|
|
2269
2265
|
* Match this selector against a HtmlElement.
|
|
@@ -2280,15 +2276,15 @@ class Selector {
|
|
|
2280
2276
|
*/
|
|
2281
2277
|
matchElement(element) {
|
|
2282
2278
|
const context = { scope: null };
|
|
2283
|
-
return matchElement(element, this.
|
|
2279
|
+
return matchElement(element, this.compounds, context);
|
|
2284
2280
|
}
|
|
2285
2281
|
*matchInternal(root, level, context) {
|
|
2286
|
-
if (level >= this.
|
|
2282
|
+
if (level >= this.compounds.length) {
|
|
2287
2283
|
yield root;
|
|
2288
2284
|
return;
|
|
2289
2285
|
}
|
|
2290
|
-
const pattern = this.
|
|
2291
|
-
const matches =
|
|
2286
|
+
const pattern = this.compounds[level];
|
|
2287
|
+
const matches = ComplexSelector.findCandidates(root, pattern);
|
|
2292
2288
|
for (const node of matches) {
|
|
2293
2289
|
if (!pattern.match(node, context)) {
|
|
2294
2290
|
continue;
|
|
@@ -2296,12 +2292,6 @@ class Selector {
|
|
|
2296
2292
|
yield* this.matchInternal(node, level + 1, context);
|
|
2297
2293
|
}
|
|
2298
2294
|
}
|
|
2299
|
-
static parse(selector) {
|
|
2300
|
-
selector = selector.replaceAll(/([+>~]) /g, "$1");
|
|
2301
|
-
return Array.from(splitSelectorElements(selector), (element) => {
|
|
2302
|
-
return new Compound(unescapeCodepoint(element));
|
|
2303
|
-
});
|
|
2304
|
-
}
|
|
2305
2295
|
static findCandidates(root, pattern) {
|
|
2306
2296
|
switch (pattern.combinator) {
|
|
2307
2297
|
case Combinator.DESCENDANT:
|
|
@@ -2309,9 +2299,9 @@ class Selector {
|
|
|
2309
2299
|
case Combinator.CHILD:
|
|
2310
2300
|
return root.childElements.filter((node) => node.is(pattern.tagName));
|
|
2311
2301
|
case Combinator.ADJACENT_SIBLING:
|
|
2312
|
-
return
|
|
2302
|
+
return ComplexSelector.findAdjacentSibling(root);
|
|
2313
2303
|
case Combinator.GENERAL_SIBLING:
|
|
2314
|
-
return
|
|
2304
|
+
return ComplexSelector.findGeneralSibling(root);
|
|
2315
2305
|
case Combinator.SCOPE:
|
|
2316
2306
|
return [root];
|
|
2317
2307
|
}
|
|
@@ -2323,7 +2313,7 @@ class Selector {
|
|
|
2323
2313
|
adjacent = false;
|
|
2324
2314
|
return true;
|
|
2325
2315
|
}
|
|
2326
|
-
if (cur
|
|
2316
|
+
if (cur.isSameNode(node)) {
|
|
2327
2317
|
adjacent = true;
|
|
2328
2318
|
}
|
|
2329
2319
|
return false;
|
|
@@ -2335,7 +2325,7 @@ class Selector {
|
|
|
2335
2325
|
if (after) {
|
|
2336
2326
|
return true;
|
|
2337
2327
|
}
|
|
2338
|
-
if (cur
|
|
2328
|
+
if (cur.isSameNode(node)) {
|
|
2339
2329
|
after = true;
|
|
2340
2330
|
}
|
|
2341
2331
|
return false;
|
|
@@ -2343,6 +2333,31 @@ class Selector {
|
|
|
2343
2333
|
}
|
|
2344
2334
|
}
|
|
2345
2335
|
|
|
2336
|
+
const codepoints = {
|
|
2337
|
+
" ": "\\9 ",
|
|
2338
|
+
"\n": "\\a ",
|
|
2339
|
+
"\r": "\\d "
|
|
2340
|
+
};
|
|
2341
|
+
function escapeSelectorComponent(text) {
|
|
2342
|
+
return text.toString().replaceAll(/([\t\n\r]|[^\w-])/gi, (_, ch) => {
|
|
2343
|
+
if (codepoints[ch]) {
|
|
2344
|
+
return codepoints[ch];
|
|
2345
|
+
} else {
|
|
2346
|
+
return `\\${ch}`;
|
|
2347
|
+
}
|
|
2348
|
+
});
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
function generateIdSelector(id) {
|
|
2352
|
+
const escaped = escapeSelectorComponent(id);
|
|
2353
|
+
return /^\d/.test(escaped) ? `[id="${escaped}"]` : `#${escaped}`;
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
function parseSelector(selector) {
|
|
2357
|
+
const compounds = getCompounds(selector);
|
|
2358
|
+
return ComplexSelector.fromCompounds(compounds);
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2346
2361
|
const TEXT_NODE_NAME = "#text";
|
|
2347
2362
|
function isTextNode(node) {
|
|
2348
2363
|
return node?.nodeType === NodeType.TEXT_NODE;
|
|
@@ -2378,6 +2393,7 @@ class TextNode extends DOMNode {
|
|
|
2378
2393
|
}
|
|
2379
2394
|
}
|
|
2380
2395
|
|
|
2396
|
+
const CHILD_ELEMENTS = /* @__PURE__ */ Symbol("childElements");
|
|
2381
2397
|
const ROLE = /* @__PURE__ */ Symbol("role");
|
|
2382
2398
|
const TABINDEX = /* @__PURE__ */ Symbol("tabindex");
|
|
2383
2399
|
var NodeClosed = /* @__PURE__ */ ((NodeClosed2) => {
|
|
@@ -2544,7 +2560,11 @@ class HtmlElement extends DOMNode {
|
|
|
2544
2560
|
* Similar to childNodes but only elements.
|
|
2545
2561
|
*/
|
|
2546
2562
|
get childElements() {
|
|
2547
|
-
|
|
2563
|
+
const cached = this.cacheGet(CHILD_ELEMENTS);
|
|
2564
|
+
if (cached !== void 0) {
|
|
2565
|
+
return cached;
|
|
2566
|
+
}
|
|
2567
|
+
return this.cacheSet(CHILD_ELEMENTS, this.childNodes.filter(isElementNode));
|
|
2548
2568
|
}
|
|
2549
2569
|
/**
|
|
2550
2570
|
* Find the first ancestor matching a selector.
|
|
@@ -2651,7 +2671,7 @@ class HtmlElement extends DOMNode {
|
|
|
2651
2671
|
*/
|
|
2652
2672
|
matches(selectorList) {
|
|
2653
2673
|
return selectorList.split(",").some((it) => {
|
|
2654
|
-
const selector =
|
|
2674
|
+
const selector = parseSelector(it.trim());
|
|
2655
2675
|
return selector.matchElement(this);
|
|
2656
2676
|
});
|
|
2657
2677
|
}
|
|
@@ -2884,9 +2904,9 @@ class HtmlElement extends DOMNode {
|
|
|
2884
2904
|
if (!selectorList) {
|
|
2885
2905
|
return;
|
|
2886
2906
|
}
|
|
2887
|
-
for (const
|
|
2888
|
-
const
|
|
2889
|
-
yield*
|
|
2907
|
+
for (const selectorString of selectorList.split(/(?<!\\),\s*/)) {
|
|
2908
|
+
const selector = parseSelector(selectorString);
|
|
2909
|
+
yield* selector.match(this);
|
|
2890
2910
|
}
|
|
2891
2911
|
}
|
|
2892
2912
|
/**
|
|
@@ -2940,12 +2960,24 @@ class HtmlElement extends DOMNode {
|
|
|
2940
2960
|
}
|
|
2941
2961
|
return visit(this);
|
|
2942
2962
|
}
|
|
2963
|
+
append(node) {
|
|
2964
|
+
super.append(node);
|
|
2965
|
+
this.cacheRemove(CHILD_ELEMENTS);
|
|
2966
|
+
}
|
|
2967
|
+
insertBefore(node, reference) {
|
|
2968
|
+
super.insertBefore(node, reference);
|
|
2969
|
+
this.cacheRemove(CHILD_ELEMENTS);
|
|
2970
|
+
}
|
|
2971
|
+
removeChild(node) {
|
|
2972
|
+
return super.removeChild(node);
|
|
2973
|
+
}
|
|
2943
2974
|
/**
|
|
2944
2975
|
* @internal
|
|
2945
2976
|
*/
|
|
2946
2977
|
_setParent(node) {
|
|
2947
2978
|
const oldParent = this._parent;
|
|
2948
2979
|
this._parent = node instanceof HtmlElement ? node : null;
|
|
2980
|
+
oldParent?.cacheRemove(CHILD_ELEMENTS);
|
|
2949
2981
|
return oldParent;
|
|
2950
2982
|
}
|
|
2951
2983
|
}
|
|
@@ -3727,6 +3759,7 @@ class Rule {
|
|
|
3727
3759
|
severity;
|
|
3728
3760
|
// rule severity
|
|
3729
3761
|
event;
|
|
3762
|
+
tracker;
|
|
3730
3763
|
/**
|
|
3731
3764
|
* Rule name. Defaults to filename without extension but can be overwritten by
|
|
3732
3765
|
* subclasses.
|
|
@@ -3746,6 +3779,7 @@ class Rule {
|
|
|
3746
3779
|
this.blockers = [];
|
|
3747
3780
|
this.severity = Severity.DISABLED;
|
|
3748
3781
|
this.name = "";
|
|
3782
|
+
this.tracker = null;
|
|
3749
3783
|
}
|
|
3750
3784
|
getSeverity() {
|
|
3751
3785
|
return this.severity;
|
|
@@ -3921,7 +3955,15 @@ class Rule {
|
|
|
3921
3955
|
return this.parser.on(event, (_event, data) => {
|
|
3922
3956
|
if (this.isEnabled() && filter(data)) {
|
|
3923
3957
|
this.event = data;
|
|
3924
|
-
|
|
3958
|
+
const { tracker } = this;
|
|
3959
|
+
if (tracker) {
|
|
3960
|
+
const start = performance.now();
|
|
3961
|
+
callback(data);
|
|
3962
|
+
const end = performance.now();
|
|
3963
|
+
tracker.trackRule(this.name, end - start);
|
|
3964
|
+
} else {
|
|
3965
|
+
callback(data);
|
|
3966
|
+
}
|
|
3925
3967
|
}
|
|
3926
3968
|
});
|
|
3927
3969
|
}
|
|
@@ -3938,6 +3980,14 @@ class Rule {
|
|
|
3938
3980
|
this.severity = severity;
|
|
3939
3981
|
this.meta = meta;
|
|
3940
3982
|
}
|
|
3983
|
+
/**
|
|
3984
|
+
* Set (or clear) the performance tracker.
|
|
3985
|
+
*
|
|
3986
|
+
* @internal
|
|
3987
|
+
*/
|
|
3988
|
+
setTracker(tracker) {
|
|
3989
|
+
this.tracker = tracker;
|
|
3990
|
+
}
|
|
3941
3991
|
/**
|
|
3942
3992
|
* Validate rule options against schema. Throws error if object does not validate.
|
|
3943
3993
|
*
|
|
@@ -12452,8 +12502,10 @@ class StaticConfigLoader extends ConfigLoader {
|
|
|
12452
12502
|
|
|
12453
12503
|
class EventHandler {
|
|
12454
12504
|
listeners;
|
|
12505
|
+
tracker;
|
|
12455
12506
|
constructor() {
|
|
12456
12507
|
this.listeners = {};
|
|
12508
|
+
this.tracker = null;
|
|
12457
12509
|
}
|
|
12458
12510
|
/**
|
|
12459
12511
|
* Add an event listener.
|
|
@@ -12492,6 +12544,14 @@ class EventHandler {
|
|
|
12492
12544
|
});
|
|
12493
12545
|
return deregister;
|
|
12494
12546
|
}
|
|
12547
|
+
/**
|
|
12548
|
+
* Set (or clear) the performance tracker.
|
|
12549
|
+
*
|
|
12550
|
+
* @internal
|
|
12551
|
+
*/
|
|
12552
|
+
setTracker(tracker) {
|
|
12553
|
+
this.tracker = tracker;
|
|
12554
|
+
}
|
|
12495
12555
|
/**
|
|
12496
12556
|
* Trigger event causing all listeners to be called.
|
|
12497
12557
|
*
|
|
@@ -12500,8 +12560,18 @@ class EventHandler {
|
|
|
12500
12560
|
*/
|
|
12501
12561
|
/* eslint-disable-next-line @typescript-eslint/no-explicit-any -- technical debt, should be made typesafe */
|
|
12502
12562
|
trigger(event, data) {
|
|
12503
|
-
|
|
12504
|
-
|
|
12563
|
+
const { tracker } = this;
|
|
12564
|
+
if (tracker) {
|
|
12565
|
+
const start = performance.now();
|
|
12566
|
+
for (const listener of this.getCallbacks(event)) {
|
|
12567
|
+
listener.call(null, event, data);
|
|
12568
|
+
}
|
|
12569
|
+
const end = performance.now();
|
|
12570
|
+
tracker.trackEvent(event, end - start);
|
|
12571
|
+
} else {
|
|
12572
|
+
for (const listener of this.getCallbacks(event)) {
|
|
12573
|
+
listener.call(null, event, data);
|
|
12574
|
+
}
|
|
12505
12575
|
}
|
|
12506
12576
|
}
|
|
12507
12577
|
getCallbacks(event) {
|
|
@@ -12513,7 +12583,7 @@ class EventHandler {
|
|
|
12513
12583
|
}
|
|
12514
12584
|
|
|
12515
12585
|
const name = "html-validate";
|
|
12516
|
-
const version = "10.
|
|
12586
|
+
const version = "10.15.0";
|
|
12517
12587
|
const bugs = "https://gitlab.com/html-validate/html-validate/issues/new";
|
|
12518
12588
|
|
|
12519
12589
|
function freeze(src) {
|
|
@@ -12813,32 +12883,92 @@ class Parser {
|
|
|
12813
12883
|
* valid). The parser handles this by checking if the element on top of the
|
|
12814
12884
|
* stack when is allowed to omit.
|
|
12815
12885
|
*/
|
|
12886
|
+
/**
|
|
12887
|
+
* Check whether a given element would be implicitly closed by an incoming
|
|
12888
|
+
* start tag. Used both in `closeOptional` and in the multi-level lookahead.
|
|
12889
|
+
*/
|
|
12890
|
+
wouldCloseElement(token, element) {
|
|
12891
|
+
if (!element.meta) {
|
|
12892
|
+
return false;
|
|
12893
|
+
}
|
|
12894
|
+
const implicitClosed = element.meta.implicitClosed;
|
|
12895
|
+
if (!implicitClosed) {
|
|
12896
|
+
return false;
|
|
12897
|
+
}
|
|
12898
|
+
const tagName = token.data[2];
|
|
12899
|
+
const incomingMeta = this.metaTable.getMetaFor(tagName);
|
|
12900
|
+
return implicitClosed.some((entry) => {
|
|
12901
|
+
if (!entry.startsWith("@")) {
|
|
12902
|
+
return entry === tagName;
|
|
12903
|
+
}
|
|
12904
|
+
return incomingMeta ? matchesContentCategory(incomingMeta, entry) : false;
|
|
12905
|
+
});
|
|
12906
|
+
}
|
|
12907
|
+
/**
|
|
12908
|
+
* Walk up the active stack to find the parent element that will remain
|
|
12909
|
+
* after all multi-level implicit closes triggered by the incoming start tag.
|
|
12910
|
+
* For a single-level close this is equivalent to `getActive().parent`.
|
|
12911
|
+
*/
|
|
12912
|
+
getParentAfterImplicitClose(token) {
|
|
12913
|
+
let current = this.dom.getActive();
|
|
12914
|
+
while (!current.isRootElement() && this.wouldCloseElement(token, current)) {
|
|
12915
|
+
const { parent } = current;
|
|
12916
|
+
if (!parent) {
|
|
12917
|
+
break;
|
|
12918
|
+
}
|
|
12919
|
+
current = parent;
|
|
12920
|
+
}
|
|
12921
|
+
return current;
|
|
12922
|
+
}
|
|
12816
12923
|
closeOptional(token) {
|
|
12817
12924
|
const active = this.dom.getActive();
|
|
12818
12925
|
if (!active.meta) {
|
|
12819
12926
|
return false;
|
|
12820
12927
|
}
|
|
12821
|
-
const tagName = token.data[2];
|
|
12822
12928
|
const open = !token.data[1];
|
|
12823
|
-
const implicitClosed = active.meta.implicitClosed;
|
|
12824
12929
|
if (open) {
|
|
12825
|
-
|
|
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
|
-
});
|
|
12930
|
+
return this.wouldCloseElement(token, active);
|
|
12835
12931
|
} else {
|
|
12836
|
-
|
|
12932
|
+
return this.closeOptionalEndTag(token, active);
|
|
12933
|
+
}
|
|
12934
|
+
}
|
|
12935
|
+
/**
|
|
12936
|
+
* Returns `true` if the element’s end tag may be omitted, either because
|
|
12937
|
+
* its `implicitClosed` list includes its own tag name (e.g. `<li>`, `<td>`)
|
|
12938
|
+
* or because `optionalEnd` is set.
|
|
12939
|
+
*/
|
|
12940
|
+
canOmitEndTag(element) {
|
|
12941
|
+
if (!element.meta) {
|
|
12942
|
+
return false;
|
|
12943
|
+
}
|
|
12944
|
+
const { implicitClosed, optionalEnd } = element.meta;
|
|
12945
|
+
return Boolean(implicitClosed?.includes(element.tagName)) || Boolean(optionalEnd);
|
|
12946
|
+
}
|
|
12947
|
+
/**
|
|
12948
|
+
* Check whether the active element can be implicitly closed by an incoming
|
|
12949
|
+
* end tag. The end tag may close a direct parent or any ancestor, as long as
|
|
12950
|
+
* every intermediate element can also have its end tag omitted.
|
|
12951
|
+
* This handles cases like `</table>` implicitly closing `<td>`, `<tr>`, `<tbody>`.
|
|
12952
|
+
*/
|
|
12953
|
+
closeOptionalEndTag(token, active) {
|
|
12954
|
+
const tagName = token.data[2];
|
|
12955
|
+
if (active.is(tagName)) {
|
|
12956
|
+
return false;
|
|
12957
|
+
}
|
|
12958
|
+
if (!this.canOmitEndTag(active)) {
|
|
12959
|
+
return false;
|
|
12960
|
+
}
|
|
12961
|
+
let ancestor = active.parent;
|
|
12962
|
+
while (ancestor && !ancestor.isRootElement()) {
|
|
12963
|
+
if (ancestor.is(tagName)) {
|
|
12964
|
+
return true;
|
|
12965
|
+
}
|
|
12966
|
+
if (!this.canOmitEndTag(ancestor)) {
|
|
12837
12967
|
return false;
|
|
12838
12968
|
}
|
|
12839
|
-
|
|
12840
|
-
return Boolean(active.parent && active.parent.is(tagName) && canOmitEnd);
|
|
12969
|
+
ancestor = ancestor.parent;
|
|
12841
12970
|
}
|
|
12971
|
+
return false;
|
|
12842
12972
|
}
|
|
12843
12973
|
/**
|
|
12844
12974
|
* Check whether an intermediary element (e.g. `<head>` or `<body>`) should
|
|
@@ -12927,8 +13057,21 @@ class Parser {
|
|
|
12927
13057
|
);
|
|
12928
13058
|
const endToken = tokens.at(-1);
|
|
12929
13059
|
const isStartTag = !startToken.data[1];
|
|
12930
|
-
|
|
12931
|
-
|
|
13060
|
+
let baseParent;
|
|
13061
|
+
if (isStartTag) {
|
|
13062
|
+
baseParent = this.getParentAfterImplicitClose(startToken);
|
|
13063
|
+
} else {
|
|
13064
|
+
const tagName = startToken.data[2];
|
|
13065
|
+
let cur = this.dom.getActive();
|
|
13066
|
+
while (!cur.isRootElement() && !cur.is(tagName)) {
|
|
13067
|
+
const { parent: parent2 } = cur;
|
|
13068
|
+
if (!parent2) {
|
|
13069
|
+
break;
|
|
13070
|
+
}
|
|
13071
|
+
cur = parent2;
|
|
13072
|
+
}
|
|
13073
|
+
baseParent = cur;
|
|
13074
|
+
}
|
|
12932
13075
|
const implicitParent = isStartTag ? this.peekImplicitOpen(startToken, baseParent) : null;
|
|
12933
13076
|
const parent = implicitParent ?? baseParent;
|
|
12934
13077
|
const node = HtmlElement.fromTokens(
|
|
@@ -12940,7 +13083,7 @@ class Parser {
|
|
|
12940
13083
|
);
|
|
12941
13084
|
const isClosing = !isStartTag || node.closed !== NodeClosed.Open;
|
|
12942
13085
|
const isForeign = node.meta?.foreign;
|
|
12943
|
-
|
|
13086
|
+
while (this.closeOptional(startToken)) {
|
|
12944
13087
|
const active = this.dom.getActive();
|
|
12945
13088
|
active.closed = NodeClosed.ImplicitClosed;
|
|
12946
13089
|
this.closeElement(source, node, active, startToken.location);
|
|
@@ -13374,6 +13517,78 @@ class Parser {
|
|
|
13374
13517
|
}
|
|
13375
13518
|
}
|
|
13376
13519
|
|
|
13520
|
+
class PerformanceTracker {
|
|
13521
|
+
eventData;
|
|
13522
|
+
ruleData;
|
|
13523
|
+
startTime;
|
|
13524
|
+
accConfigTime;
|
|
13525
|
+
accTransformTime;
|
|
13526
|
+
constructor() {
|
|
13527
|
+
this.eventData = /* @__PURE__ */ new Map();
|
|
13528
|
+
this.ruleData = /* @__PURE__ */ new Map();
|
|
13529
|
+
this.startTime = performance.now();
|
|
13530
|
+
this.accConfigTime = 0;
|
|
13531
|
+
this.accTransformTime = 0;
|
|
13532
|
+
}
|
|
13533
|
+
/**
|
|
13534
|
+
* Record a single event trigger with the time it took to run all listeners.
|
|
13535
|
+
*/
|
|
13536
|
+
trackEvent(name, time) {
|
|
13537
|
+
const existing = this.eventData.get(name);
|
|
13538
|
+
if (existing) {
|
|
13539
|
+
existing.count += 1;
|
|
13540
|
+
existing.time += time;
|
|
13541
|
+
} else {
|
|
13542
|
+
this.eventData.set(name, { count: 1, time });
|
|
13543
|
+
}
|
|
13544
|
+
}
|
|
13545
|
+
/**
|
|
13546
|
+
* Record time spent loading configuration.
|
|
13547
|
+
*/
|
|
13548
|
+
trackConfig(time) {
|
|
13549
|
+
this.accConfigTime += time;
|
|
13550
|
+
}
|
|
13551
|
+
/**
|
|
13552
|
+
* Record time spent in transformers.
|
|
13553
|
+
*/
|
|
13554
|
+
trackTransform(time) {
|
|
13555
|
+
this.accTransformTime += time;
|
|
13556
|
+
}
|
|
13557
|
+
/**
|
|
13558
|
+
* Record a single rule callback invocation with its execution time.
|
|
13559
|
+
*/
|
|
13560
|
+
trackRule(ruleName, time) {
|
|
13561
|
+
const existing = this.ruleData.get(ruleName);
|
|
13562
|
+
if (existing) {
|
|
13563
|
+
existing.count += 1;
|
|
13564
|
+
existing.time += time;
|
|
13565
|
+
} else {
|
|
13566
|
+
this.ruleData.set(ruleName, { count: 1, time });
|
|
13567
|
+
}
|
|
13568
|
+
}
|
|
13569
|
+
/**
|
|
13570
|
+
* Returns a snapshot of the recorded performance data, with both arrays
|
|
13571
|
+
* sorted by time (descending).
|
|
13572
|
+
*/
|
|
13573
|
+
getResult() {
|
|
13574
|
+
const events = Array.from(
|
|
13575
|
+
this.eventData.entries(),
|
|
13576
|
+
([event, { count, time }]) => ({ event, count, time })
|
|
13577
|
+
).toSorted((a, b) => b.time - a.time);
|
|
13578
|
+
const rules = Array.from(
|
|
13579
|
+
this.ruleData.entries(),
|
|
13580
|
+
([rule, { count, time }]) => ({ rule, count, time })
|
|
13581
|
+
).toSorted((a, b) => b.time - a.time);
|
|
13582
|
+
return {
|
|
13583
|
+
events,
|
|
13584
|
+
rules,
|
|
13585
|
+
configTime: this.accConfigTime,
|
|
13586
|
+
transformTime: this.accTransformTime,
|
|
13587
|
+
totalTime: performance.now() - this.startTime
|
|
13588
|
+
};
|
|
13589
|
+
}
|
|
13590
|
+
}
|
|
13591
|
+
|
|
13377
13592
|
const ruleIds = new Set(Object.keys(bundledRules));
|
|
13378
13593
|
function ruleExists(ruleId) {
|
|
13379
13594
|
return ruleIds.has(ruleId);
|
|
@@ -13421,10 +13636,12 @@ class Engine {
|
|
|
13421
13636
|
config;
|
|
13422
13637
|
ParserClass;
|
|
13423
13638
|
availableRules;
|
|
13424
|
-
|
|
13639
|
+
tracker;
|
|
13640
|
+
constructor(config, ParserClass, options) {
|
|
13425
13641
|
this.report = new Reporter();
|
|
13426
13642
|
this.config = config;
|
|
13427
13643
|
this.ParserClass = ParserClass;
|
|
13644
|
+
this.tracker = options.tracker;
|
|
13428
13645
|
const result = this.initPlugins(this.config);
|
|
13429
13646
|
this.availableRules = {
|
|
13430
13647
|
...bundledRules,
|
|
@@ -13440,6 +13657,9 @@ class Engine {
|
|
|
13440
13657
|
lint(sources) {
|
|
13441
13658
|
for (const source of sources) {
|
|
13442
13659
|
const parser = this.instantiateParser();
|
|
13660
|
+
if (this.tracker) {
|
|
13661
|
+
parser.getEventHandler().setTracker(this.tracker);
|
|
13662
|
+
}
|
|
13443
13663
|
const { rules } = this.setupPlugins(source, this.config, parser);
|
|
13444
13664
|
const noUnusedDisable = rules["no-unused-disable"];
|
|
13445
13665
|
const directiveContext = {
|
|
@@ -13716,6 +13936,7 @@ class Engine {
|
|
|
13716
13936
|
const rule = this.instantiateRule(ruleId, options);
|
|
13717
13937
|
rule.name = ruleId;
|
|
13718
13938
|
rule.init(parser, report, severity, meta);
|
|
13939
|
+
rule.setTracker(this.tracker);
|
|
13719
13940
|
if (rule.setup) {
|
|
13720
13941
|
rule.setup();
|
|
13721
13942
|
}
|
|
@@ -15091,6 +15312,7 @@ exports.NestedError = NestedError;
|
|
|
15091
15312
|
exports.NodeClosed = NodeClosed;
|
|
15092
15313
|
exports.NodeType = NodeType;
|
|
15093
15314
|
exports.Parser = Parser;
|
|
15315
|
+
exports.PerformanceTracker = PerformanceTracker;
|
|
15094
15316
|
exports.Reporter = Reporter;
|
|
15095
15317
|
exports.ResolvedConfig = ResolvedConfig;
|
|
15096
15318
|
exports.Rule = Rule;
|