html-validate 8.20.1 → 8.22.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 -1
- package/dist/cjs/cli.js.map +1 -1
- package/dist/cjs/core-nodejs.js.map +1 -1
- package/dist/cjs/core.js +1781 -1613
- package/dist/cjs/core.js.map +1 -1
- package/dist/cjs/elements.js +4 -6
- package/dist/cjs/elements.js.map +1 -1
- package/dist/cjs/html-validate.js +0 -1
- package/dist/cjs/html-validate.js.map +1 -1
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/jest-diff.js.map +1 -1
- package/dist/cjs/jest.js +0 -1
- package/dist/cjs/jest.js.map +1 -1
- package/dist/cjs/matcher-utils.js.map +1 -1
- package/dist/cjs/matchers.js.map +1 -1
- package/dist/cjs/tsdoc-metadata.json +1 -1
- package/dist/cjs/vitest.js +0 -1
- package/dist/cjs/vitest.js.map +1 -1
- package/dist/es/browser.js +1 -2
- package/dist/es/browser.js.map +1 -1
- package/dist/es/cli.js.map +1 -1
- package/dist/es/core-browser.js +1 -1
- package/dist/es/core-nodejs.js +1 -1
- package/dist/es/core-nodejs.js.map +1 -1
- package/dist/es/core.js +1781 -1614
- package/dist/es/core.js.map +1 -1
- package/dist/es/elements.js +4 -6
- package/dist/es/elements.js.map +1 -1
- package/dist/es/html-validate.js +1 -2
- package/dist/es/html-validate.js.map +1 -1
- package/dist/es/index.js +1 -2
- package/dist/es/index.js.map +1 -1
- package/dist/es/jest-diff.js.map +1 -1
- package/dist/es/jest.js +0 -1
- package/dist/es/jest.js.map +1 -1
- package/dist/es/matcher-utils.js.map +1 -1
- package/dist/es/matchers-jestonly.js +1 -1
- package/dist/es/matchers.js.map +1 -1
- package/dist/es/vitest.js +0 -1
- package/dist/es/vitest.js.map +1 -1
- package/dist/tsdoc-metadata.json +1 -1
- package/dist/types/browser.d.ts +65 -18
- package/dist/types/index.d.ts +65 -18
- package/package.json +23 -27
package/dist/cjs/core.js
CHANGED
|
@@ -5,7 +5,6 @@ var elements = require('./elements.js');
|
|
|
5
5
|
var betterAjvErrors = require('@sidvind/better-ajv-errors');
|
|
6
6
|
var utils_naturalJoin = require('./utils/natural-join.js');
|
|
7
7
|
var fs = require('fs');
|
|
8
|
-
var codeFrame = require('@babel/code-frame');
|
|
9
8
|
var kleur = require('kleur');
|
|
10
9
|
var stylish$1 = require('@html-validate/stylish');
|
|
11
10
|
var semver = require('semver');
|
|
@@ -1279,14 +1278,12 @@ class MetaTable {
|
|
|
1279
1278
|
* @returns A shallow copy of metadata.
|
|
1280
1279
|
*/
|
|
1281
1280
|
getMetaFor(tagName) {
|
|
1282
|
-
|
|
1283
|
-
if (
|
|
1284
|
-
return { ...
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
return { ...this.elements["*"] };
|
|
1281
|
+
const meta = this.elements[tagName.toLowerCase()] ?? this.elements["*"];
|
|
1282
|
+
if (meta) {
|
|
1283
|
+
return { ...meta };
|
|
1284
|
+
} else {
|
|
1285
|
+
return null;
|
|
1288
1286
|
}
|
|
1289
|
-
return null;
|
|
1290
1287
|
}
|
|
1291
1288
|
/**
|
|
1292
1289
|
* Find all tags which has enabled given property.
|
|
@@ -1294,7 +1291,7 @@ class MetaTable {
|
|
|
1294
1291
|
* @public
|
|
1295
1292
|
*/
|
|
1296
1293
|
getTagsWithProperty(propName) {
|
|
1297
|
-
return
|
|
1294
|
+
return this.entries.filter(([, entry]) => entry[propName]).map(([tagName]) => tagName);
|
|
1298
1295
|
}
|
|
1299
1296
|
/**
|
|
1300
1297
|
* Find tag matching tagName or inheriting from it.
|
|
@@ -1302,10 +1299,10 @@ class MetaTable {
|
|
|
1302
1299
|
* @public
|
|
1303
1300
|
*/
|
|
1304
1301
|
getTagsDerivedFrom(tagName) {
|
|
1305
|
-
return
|
|
1302
|
+
return this.entries.filter(([key, entry]) => key === tagName || entry.inherit === tagName).map(([tagName2]) => tagName2);
|
|
1306
1303
|
}
|
|
1307
1304
|
addEntry(tagName, entry) {
|
|
1308
|
-
let parent = this.elements[tagName]
|
|
1305
|
+
let parent = this.elements[tagName];
|
|
1309
1306
|
if (entry.inherit) {
|
|
1310
1307
|
const name = entry.inherit;
|
|
1311
1308
|
parent = this.elements[name];
|
|
@@ -1316,7 +1313,7 @@ class MetaTable {
|
|
|
1316
1313
|
});
|
|
1317
1314
|
}
|
|
1318
1315
|
}
|
|
1319
|
-
const expanded = this.mergeElement(parent, { ...entry, tagName });
|
|
1316
|
+
const expanded = this.mergeElement(parent ?? {}, { ...entry, tagName });
|
|
1320
1317
|
expandRegex(expanded);
|
|
1321
1318
|
this.elements[tagName] = expanded;
|
|
1322
1319
|
}
|
|
@@ -1345,6 +1342,12 @@ class MetaTable {
|
|
|
1345
1342
|
getJSONSchema() {
|
|
1346
1343
|
return this.schema;
|
|
1347
1344
|
}
|
|
1345
|
+
/**
|
|
1346
|
+
* @internal
|
|
1347
|
+
*/
|
|
1348
|
+
get entries() {
|
|
1349
|
+
return Object.entries(this.elements);
|
|
1350
|
+
}
|
|
1348
1351
|
/**
|
|
1349
1352
|
* Finds the global element definition and merges each known element with the
|
|
1350
1353
|
* global, e.g. to assign global attributes.
|
|
@@ -1356,7 +1359,7 @@ class MetaTable {
|
|
|
1356
1359
|
delete this.elements["*"];
|
|
1357
1360
|
delete global.tagName;
|
|
1358
1361
|
delete global.void;
|
|
1359
|
-
for (const [tagName, entry] of
|
|
1362
|
+
for (const [tagName, entry] of this.entries) {
|
|
1360
1363
|
this.elements[tagName] = this.mergeElement(global, entry);
|
|
1361
1364
|
}
|
|
1362
1365
|
}
|
|
@@ -1677,6 +1680,7 @@ class DOMNode {
|
|
|
1677
1680
|
/**
|
|
1678
1681
|
* Create a new DOMNode.
|
|
1679
1682
|
*
|
|
1683
|
+
* @internal
|
|
1680
1684
|
* @param nodeType - What node type to create.
|
|
1681
1685
|
* @param nodeName - What node name to use. For `HtmlElement` this corresponds
|
|
1682
1686
|
* to the tagName but other node types have specific predefined values.
|
|
@@ -2144,7 +2148,7 @@ class AttrMatcher extends Matcher {
|
|
|
2144
2148
|
this.value = value;
|
|
2145
2149
|
}
|
|
2146
2150
|
match(node) {
|
|
2147
|
-
const attr = node.getAttribute(this.key, true)
|
|
2151
|
+
const attr = node.getAttribute(this.key, true);
|
|
2148
2152
|
return attr.some((cur) => {
|
|
2149
2153
|
switch (this.op) {
|
|
2150
2154
|
case void 0:
|
|
@@ -2345,8 +2349,15 @@ function createAdapter(node) {
|
|
|
2345
2349
|
};
|
|
2346
2350
|
}
|
|
2347
2351
|
class HtmlElement extends DOMNode {
|
|
2348
|
-
constructor(
|
|
2349
|
-
const
|
|
2352
|
+
constructor(details) {
|
|
2353
|
+
const {
|
|
2354
|
+
nodeType,
|
|
2355
|
+
tagName,
|
|
2356
|
+
parent = null,
|
|
2357
|
+
closed = 1 /* EndTag */,
|
|
2358
|
+
meta = null,
|
|
2359
|
+
location
|
|
2360
|
+
} = details;
|
|
2350
2361
|
super(nodeType, tagName, location);
|
|
2351
2362
|
if (isInvalidTagName(tagName)) {
|
|
2352
2363
|
throw new Error(`The tag name provided ("${tagName}") is not a valid name`);
|
|
@@ -2369,11 +2380,39 @@ class HtmlElement extends DOMNode {
|
|
|
2369
2380
|
}
|
|
2370
2381
|
}
|
|
2371
2382
|
}
|
|
2383
|
+
/**
|
|
2384
|
+
* Manually create a new element. This is primary useful for test-cases. While
|
|
2385
|
+
* the API is public it is not meant for general consumption and is not
|
|
2386
|
+
* guaranteed to be stable across versions.
|
|
2387
|
+
*
|
|
2388
|
+
* Use at your own risk. Prefer to use [[Parser]] to parse a string of markup
|
|
2389
|
+
* instead.
|
|
2390
|
+
*
|
|
2391
|
+
* @public
|
|
2392
|
+
* @since 8.22.0
|
|
2393
|
+
* @param tagName - Element tagname.
|
|
2394
|
+
* @param location - Element location.
|
|
2395
|
+
* @param details - Additional element details.
|
|
2396
|
+
*/
|
|
2397
|
+
static createElement(tagName, location, details = {}) {
|
|
2398
|
+
const { closed = 1 /* EndTag */, meta = null, parent = null } = details;
|
|
2399
|
+
return new HtmlElement({
|
|
2400
|
+
nodeType: NodeType.ELEMENT_NODE,
|
|
2401
|
+
tagName,
|
|
2402
|
+
parent,
|
|
2403
|
+
closed,
|
|
2404
|
+
meta,
|
|
2405
|
+
location
|
|
2406
|
+
});
|
|
2407
|
+
}
|
|
2372
2408
|
/**
|
|
2373
2409
|
* @internal
|
|
2374
2410
|
*/
|
|
2375
2411
|
static rootNode(location) {
|
|
2376
|
-
const root = new HtmlElement(
|
|
2412
|
+
const root = new HtmlElement({
|
|
2413
|
+
nodeType: NodeType.DOCUMENT_NODE,
|
|
2414
|
+
location
|
|
2415
|
+
});
|
|
2377
2416
|
root.setAnnotation("#document");
|
|
2378
2417
|
return root;
|
|
2379
2418
|
}
|
|
@@ -2392,7 +2431,14 @@ class HtmlElement extends DOMNode {
|
|
|
2392
2431
|
const open = startToken.data[1] !== "/";
|
|
2393
2432
|
const closed = isClosed(endToken, meta);
|
|
2394
2433
|
const location = sliceLocation(startToken.location, 1);
|
|
2395
|
-
return new HtmlElement(
|
|
2434
|
+
return new HtmlElement({
|
|
2435
|
+
nodeType: NodeType.ELEMENT_NODE,
|
|
2436
|
+
tagName,
|
|
2437
|
+
parent: open ? parent : null,
|
|
2438
|
+
closed,
|
|
2439
|
+
meta,
|
|
2440
|
+
location
|
|
2441
|
+
});
|
|
2396
2442
|
}
|
|
2397
2443
|
/**
|
|
2398
2444
|
* Returns annotated name if set or defaults to `<tagName>`.
|
|
@@ -2590,10 +2636,13 @@ class HtmlElement extends DOMNode {
|
|
|
2590
2636
|
*/
|
|
2591
2637
|
setAttribute(key, value, keyLocation, valueLocation, originalAttribute) {
|
|
2592
2638
|
key = key.toLowerCase();
|
|
2593
|
-
|
|
2594
|
-
|
|
2639
|
+
const attr = new Attribute(key, value, keyLocation, valueLocation, originalAttribute);
|
|
2640
|
+
const list = this.attr[key];
|
|
2641
|
+
if (list) {
|
|
2642
|
+
list.push(attr);
|
|
2643
|
+
} else {
|
|
2644
|
+
this.attr[key] = [attr];
|
|
2595
2645
|
}
|
|
2596
|
-
this.attr[key].push(new Attribute(key, value, keyLocation, valueLocation, originalAttribute));
|
|
2597
2646
|
}
|
|
2598
2647
|
/**
|
|
2599
2648
|
* Get parsed tabindex for this element.
|
|
@@ -2650,7 +2699,7 @@ class HtmlElement extends DOMNode {
|
|
|
2650
2699
|
const matches = this.attr[key];
|
|
2651
2700
|
return all ? matches : matches[0];
|
|
2652
2701
|
} else {
|
|
2653
|
-
return null;
|
|
2702
|
+
return all ? [] : null;
|
|
2654
2703
|
}
|
|
2655
2704
|
}
|
|
2656
2705
|
/**
|
|
@@ -2756,20 +2805,6 @@ class HtmlElement extends DOMNode {
|
|
|
2756
2805
|
yield* pattern.match(this);
|
|
2757
2806
|
}
|
|
2758
2807
|
}
|
|
2759
|
-
/**
|
|
2760
|
-
* Visit all nodes from this node and down. Depth first.
|
|
2761
|
-
*
|
|
2762
|
-
* @internal
|
|
2763
|
-
*/
|
|
2764
|
-
visitDepthFirst(callback) {
|
|
2765
|
-
function visit(node) {
|
|
2766
|
-
node.childElements.forEach(visit);
|
|
2767
|
-
if (!node.isRootElement()) {
|
|
2768
|
-
callback(node);
|
|
2769
|
-
}
|
|
2770
|
-
}
|
|
2771
|
-
visit(this);
|
|
2772
|
-
}
|
|
2773
2808
|
/**
|
|
2774
2809
|
* Evaluates callbackk on all descendants, returning true if any are true.
|
|
2775
2810
|
*
|
|
@@ -2833,738 +2868,309 @@ function isClosed(endToken, meta) {
|
|
|
2833
2868
|
return closed;
|
|
2834
2869
|
}
|
|
2835
2870
|
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
pushActive(node) {
|
|
2843
|
-
this.active = node;
|
|
2844
|
-
}
|
|
2845
|
-
popActive() {
|
|
2846
|
-
if (this.active.isRootElement()) {
|
|
2847
|
-
return;
|
|
2871
|
+
function dumpTree(root) {
|
|
2872
|
+
const lines = [];
|
|
2873
|
+
function decoration(node) {
|
|
2874
|
+
let output = "";
|
|
2875
|
+
if (node.id) {
|
|
2876
|
+
output += `#${node.id}`;
|
|
2848
2877
|
}
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
return
|
|
2878
|
+
if (node.hasAttribute("class")) {
|
|
2879
|
+
output += `.${node.classList.join(".")}`;
|
|
2880
|
+
}
|
|
2881
|
+
return output;
|
|
2853
2882
|
}
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2883
|
+
function writeNode(node, level, sibling) {
|
|
2884
|
+
if (node.parent) {
|
|
2885
|
+
const indent = " ".repeat(level - 1);
|
|
2886
|
+
const l = node.childElements.length > 0 ? "\u252C" : "\u2500";
|
|
2887
|
+
const b = sibling < node.parent.childElements.length - 1 ? "\u251C" : "\u2514";
|
|
2888
|
+
lines.push(`${indent}${b}\u2500${l} ${node.tagName}${decoration(node)}`);
|
|
2889
|
+
} else {
|
|
2890
|
+
lines.push("(root)");
|
|
2891
|
+
}
|
|
2892
|
+
node.childElements.forEach((child, index) => {
|
|
2893
|
+
writeNode(child, level + 1, index);
|
|
2860
2894
|
});
|
|
2861
2895
|
}
|
|
2862
|
-
|
|
2863
|
-
|
|
2896
|
+
writeNode(root, 0, 0);
|
|
2897
|
+
return lines;
|
|
2898
|
+
}
|
|
2899
|
+
|
|
2900
|
+
function escape(value) {
|
|
2901
|
+
return JSON.stringify(value);
|
|
2902
|
+
}
|
|
2903
|
+
function format(value, quote = false) {
|
|
2904
|
+
if (value === null) {
|
|
2905
|
+
return "null";
|
|
2864
2906
|
}
|
|
2865
|
-
|
|
2866
|
-
|
|
2907
|
+
if (typeof value === "number") {
|
|
2908
|
+
return value.toString();
|
|
2867
2909
|
}
|
|
2868
|
-
|
|
2869
|
-
return
|
|
2910
|
+
if (typeof value === "string") {
|
|
2911
|
+
return quote ? escape(value) : value;
|
|
2870
2912
|
}
|
|
2871
|
-
|
|
2872
|
-
|
|
2913
|
+
if (Array.isArray(value)) {
|
|
2914
|
+
const content = value.map((it) => format(it, true)).join(", ");
|
|
2915
|
+
return `[ ${content} ]`;
|
|
2873
2916
|
}
|
|
2874
|
-
|
|
2875
|
-
|
|
2917
|
+
if (typeof value === "object") {
|
|
2918
|
+
const content = Object.entries(value).map(([key, nested]) => `${key}: ${format(nested, true)}`).join(", ");
|
|
2919
|
+
return `{ ${content} }`;
|
|
2876
2920
|
}
|
|
2921
|
+
return String(value);
|
|
2922
|
+
}
|
|
2923
|
+
function interpolate(text, data) {
|
|
2924
|
+
return text.replace(/{{\s*([^\s{}]+)\s*}}/g, (match, key) => {
|
|
2925
|
+
return typeof data[key] !== "undefined" ? format(data[key]) : match;
|
|
2926
|
+
});
|
|
2877
2927
|
}
|
|
2878
2928
|
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
return
|
|
2894
|
-
|
|
2929
|
+
function isThenable(value) {
|
|
2930
|
+
return value && typeof value === "object" && "then" in value && typeof value.then === "function";
|
|
2931
|
+
}
|
|
2932
|
+
|
|
2933
|
+
var Severity = /* @__PURE__ */ ((Severity2) => {
|
|
2934
|
+
Severity2[Severity2["DISABLED"] = 0] = "DISABLED";
|
|
2935
|
+
Severity2[Severity2["WARN"] = 1] = "WARN";
|
|
2936
|
+
Severity2[Severity2["ERROR"] = 2] = "ERROR";
|
|
2937
|
+
return Severity2;
|
|
2938
|
+
})(Severity || {});
|
|
2939
|
+
function parseSeverity(value) {
|
|
2940
|
+
switch (value) {
|
|
2941
|
+
case 0:
|
|
2942
|
+
case "off":
|
|
2943
|
+
return 0 /* DISABLED */;
|
|
2944
|
+
case 1:
|
|
2945
|
+
case "warn":
|
|
2946
|
+
return 1 /* WARN */;
|
|
2947
|
+
case 2:
|
|
2948
|
+
case "error":
|
|
2949
|
+
return 2 /* ERROR */;
|
|
2950
|
+
default:
|
|
2951
|
+
throw new Error(`Invalid severity "${String(value)}"`);
|
|
2895
2952
|
}
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
for (const child of siblings.slice(limit)) {
|
|
2925
|
-
cb(child, category);
|
|
2926
|
-
}
|
|
2927
|
-
valid = false;
|
|
2928
|
-
}
|
|
2929
|
-
}
|
|
2930
|
-
}
|
|
2931
|
-
return valid;
|
|
2953
|
+
}
|
|
2954
|
+
|
|
2955
|
+
const cacheKey = Symbol("aria-naming");
|
|
2956
|
+
const defaultValue = "allowed";
|
|
2957
|
+
const prohibitedRoles = [
|
|
2958
|
+
"caption",
|
|
2959
|
+
"code",
|
|
2960
|
+
"deletion",
|
|
2961
|
+
"emphasis",
|
|
2962
|
+
"generic",
|
|
2963
|
+
"insertion",
|
|
2964
|
+
"paragraph",
|
|
2965
|
+
"presentation",
|
|
2966
|
+
"strong",
|
|
2967
|
+
"subscript",
|
|
2968
|
+
"superscript"
|
|
2969
|
+
];
|
|
2970
|
+
function byRole(role) {
|
|
2971
|
+
return prohibitedRoles.includes(role) ? "prohibited" : "allowed";
|
|
2972
|
+
}
|
|
2973
|
+
function byMeta(element, meta) {
|
|
2974
|
+
return meta.aria.naming(element._adapter);
|
|
2975
|
+
}
|
|
2976
|
+
function ariaNaming(element) {
|
|
2977
|
+
var _a;
|
|
2978
|
+
const cached = element.cacheGet(cacheKey);
|
|
2979
|
+
if (cached) {
|
|
2980
|
+
return cached;
|
|
2932
2981
|
}
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
*
|
|
2940
|
-
* For instance, for a `<table>` element the `<caption>` element must come
|
|
2941
|
-
* before a `<thead>` which must come before `<tbody>`.
|
|
2942
|
-
*
|
|
2943
|
-
* @param children - Array of children to validate.
|
|
2944
|
-
*/
|
|
2945
|
-
static validateOrder(children, rules, cb) {
|
|
2946
|
-
if (!rules) {
|
|
2947
|
-
return true;
|
|
2948
|
-
}
|
|
2949
|
-
let i = 0;
|
|
2950
|
-
let prev = null;
|
|
2951
|
-
for (const node of children) {
|
|
2952
|
-
const old = i;
|
|
2953
|
-
while (rules[i] && !Validator.validatePermittedCategory(node, rules[i], true)) {
|
|
2954
|
-
i++;
|
|
2955
|
-
}
|
|
2956
|
-
if (i >= rules.length) {
|
|
2957
|
-
const orderSpecified = rules.find(
|
|
2958
|
-
(cur) => Validator.validatePermittedCategory(node, cur, true)
|
|
2959
|
-
);
|
|
2960
|
-
if (orderSpecified) {
|
|
2961
|
-
cb(node, prev);
|
|
2962
|
-
return false;
|
|
2963
|
-
}
|
|
2964
|
-
i = old;
|
|
2965
|
-
}
|
|
2966
|
-
prev = node;
|
|
2982
|
+
const role = (_a = element.getAttribute("role")) == null ? void 0 : _a.value;
|
|
2983
|
+
if (role) {
|
|
2984
|
+
if (role instanceof DynamicValue) {
|
|
2985
|
+
return element.cacheSet(cacheKey, defaultValue);
|
|
2986
|
+
} else {
|
|
2987
|
+
return element.cacheSet(cacheKey, byRole(role));
|
|
2967
2988
|
}
|
|
2968
|
-
return true;
|
|
2969
2989
|
}
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
* Check if an element has the required set of elements. At least one of the
|
|
2974
|
-
* selectors must match.
|
|
2975
|
-
*/
|
|
2976
|
-
static validateAncestors(node, rules) {
|
|
2977
|
-
if (!rules || rules.length === 0) {
|
|
2978
|
-
return true;
|
|
2979
|
-
}
|
|
2980
|
-
return rules.some((rule) => node.closest(rule));
|
|
2990
|
+
const meta = element.meta;
|
|
2991
|
+
if (!meta) {
|
|
2992
|
+
return element.cacheSet(cacheKey, defaultValue);
|
|
2981
2993
|
}
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
(child) => Validator.validatePermittedCategory(child, tagName, false)
|
|
2998
|
-
);
|
|
2999
|
-
return !haveMatchingChild;
|
|
3000
|
-
});
|
|
2994
|
+
return element.cacheSet(cacheKey, byMeta(element, meta));
|
|
2995
|
+
}
|
|
2996
|
+
|
|
2997
|
+
const patternCache = /* @__PURE__ */ new Map();
|
|
2998
|
+
function compileStringPattern(pattern) {
|
|
2999
|
+
const regexp = pattern.replace(/[*]+/g, ".+");
|
|
3000
|
+
return new RegExp(`^${regexp}$`);
|
|
3001
|
+
}
|
|
3002
|
+
function compileRegExpPattern(pattern) {
|
|
3003
|
+
return new RegExp(`^${pattern}$`);
|
|
3004
|
+
}
|
|
3005
|
+
function compilePattern(pattern) {
|
|
3006
|
+
const cached = patternCache.get(pattern);
|
|
3007
|
+
if (cached) {
|
|
3008
|
+
return cached;
|
|
3001
3009
|
}
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
if (!rule) {
|
|
3012
|
-
return true;
|
|
3013
|
-
}
|
|
3014
|
-
const value = attr.value;
|
|
3015
|
-
if (value instanceof DynamicValue) {
|
|
3016
|
-
return true;
|
|
3017
|
-
}
|
|
3018
|
-
const empty = value === null || value === "";
|
|
3019
|
-
if (rule.boolean) {
|
|
3020
|
-
return empty || value === attr.key;
|
|
3021
|
-
}
|
|
3022
|
-
if (rule.omit && empty) {
|
|
3010
|
+
const match = pattern.match(/^\/(.*)\/$/);
|
|
3011
|
+
const regexp = match ? compileRegExpPattern(match[1]) : compileStringPattern(pattern);
|
|
3012
|
+
patternCache.set(pattern, regexp);
|
|
3013
|
+
return regexp;
|
|
3014
|
+
}
|
|
3015
|
+
function keywordPatternMatcher(list, keyword) {
|
|
3016
|
+
for (const pattern of list) {
|
|
3017
|
+
const regexp = compilePattern(pattern);
|
|
3018
|
+
if (regexp.test(keyword)) {
|
|
3023
3019
|
return true;
|
|
3024
3020
|
}
|
|
3025
|
-
if (rule.list) {
|
|
3026
|
-
const tokens = new DOMTokenList(value, attr.valueLocation);
|
|
3027
|
-
return tokens.every((token) => {
|
|
3028
|
-
return this.validateAttributeValue(token, rule);
|
|
3029
|
-
});
|
|
3030
|
-
}
|
|
3031
|
-
return this.validateAttributeValue(value, rule);
|
|
3032
3021
|
}
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
}
|
|
3040
|
-
const caseInsensitiveValue = value.toLowerCase();
|
|
3041
|
-
return rule.enum.some((entry) => {
|
|
3042
|
-
if (entry instanceof RegExp) {
|
|
3043
|
-
return !!value.match(entry);
|
|
3044
|
-
} else {
|
|
3045
|
-
return caseInsensitiveValue === entry;
|
|
3046
|
-
}
|
|
3047
|
-
});
|
|
3022
|
+
return false;
|
|
3023
|
+
}
|
|
3024
|
+
function isKeywordIgnored(options, keyword, matcher = (list, it) => list.includes(it)) {
|
|
3025
|
+
const { include, exclude } = options;
|
|
3026
|
+
if (include && !matcher(include, keyword)) {
|
|
3027
|
+
return true;
|
|
3048
3028
|
}
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
return Validator.validatePermittedCategory(node, rule, !isExclude);
|
|
3052
|
-
} else if (Array.isArray(rule)) {
|
|
3053
|
-
return rule.every((inner) => {
|
|
3054
|
-
return Validator.validatePermittedRule(node, inner, isExclude);
|
|
3055
|
-
});
|
|
3056
|
-
} else {
|
|
3057
|
-
validateKeys(rule);
|
|
3058
|
-
if (rule.exclude) {
|
|
3059
|
-
if (Array.isArray(rule.exclude)) {
|
|
3060
|
-
return !rule.exclude.some((inner) => {
|
|
3061
|
-
return Validator.validatePermittedRule(node, inner, true);
|
|
3062
|
-
});
|
|
3063
|
-
} else {
|
|
3064
|
-
return !Validator.validatePermittedRule(node, rule.exclude, true);
|
|
3065
|
-
}
|
|
3066
|
-
} else {
|
|
3067
|
-
return true;
|
|
3068
|
-
}
|
|
3069
|
-
}
|
|
3029
|
+
if (exclude && matcher(exclude, keyword)) {
|
|
3030
|
+
return true;
|
|
3070
3031
|
}
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
*/
|
|
3083
|
-
/* eslint-disable-next-line complexity -- rule does not like switch */
|
|
3084
|
-
static validatePermittedCategory(node, category, defaultMatch) {
|
|
3085
|
-
const [, rawCategory] = category.match(/^(@?.*?)([?*]?)$/);
|
|
3086
|
-
if (!rawCategory.startsWith("@")) {
|
|
3087
|
-
return node.tagName === rawCategory;
|
|
3088
|
-
}
|
|
3089
|
-
if (!node.meta) {
|
|
3090
|
-
return defaultMatch;
|
|
3091
|
-
}
|
|
3092
|
-
switch (rawCategory) {
|
|
3093
|
-
case "@meta":
|
|
3094
|
-
return node.meta.metadata;
|
|
3095
|
-
case "@flow":
|
|
3096
|
-
return node.meta.flow;
|
|
3097
|
-
case "@sectioning":
|
|
3098
|
-
return node.meta.sectioning;
|
|
3099
|
-
case "@heading":
|
|
3100
|
-
return node.meta.heading;
|
|
3101
|
-
case "@phrasing":
|
|
3102
|
-
return node.meta.phrasing;
|
|
3103
|
-
case "@embedded":
|
|
3104
|
-
return node.meta.embedded;
|
|
3105
|
-
case "@interactive":
|
|
3106
|
-
return node.meta.interactive;
|
|
3107
|
-
case "@script":
|
|
3108
|
-
return Boolean(node.meta.scriptSupporting);
|
|
3109
|
-
case "@form":
|
|
3110
|
-
return Boolean(node.meta.form);
|
|
3111
|
-
default:
|
|
3112
|
-
throw new Error(`Invalid content category "${category}"`);
|
|
3113
|
-
}
|
|
3032
|
+
return false;
|
|
3033
|
+
}
|
|
3034
|
+
|
|
3035
|
+
const ARIA_HIDDEN_CACHE = Symbol(isAriaHidden.name);
|
|
3036
|
+
const HTML_HIDDEN_CACHE = Symbol(isHTMLHidden.name);
|
|
3037
|
+
const INERT_CACHE = Symbol(isInert.name);
|
|
3038
|
+
const ROLE_PRESENTATION_CACHE = Symbol(isPresentation.name);
|
|
3039
|
+
const STYLE_HIDDEN_CACHE = Symbol(isStyleHidden.name);
|
|
3040
|
+
function inAccessibilityTree(node) {
|
|
3041
|
+
if (isAriaHidden(node)) {
|
|
3042
|
+
return false;
|
|
3114
3043
|
}
|
|
3044
|
+
if (isPresentation(node)) {
|
|
3045
|
+
return false;
|
|
3046
|
+
}
|
|
3047
|
+
if (isHTMLHidden(node)) {
|
|
3048
|
+
return false;
|
|
3049
|
+
}
|
|
3050
|
+
if (isInert(node)) {
|
|
3051
|
+
return false;
|
|
3052
|
+
}
|
|
3053
|
+
if (isStyleHidden(node)) {
|
|
3054
|
+
return false;
|
|
3055
|
+
}
|
|
3056
|
+
return true;
|
|
3115
3057
|
}
|
|
3116
|
-
function
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3058
|
+
function isAriaHiddenImpl(node) {
|
|
3059
|
+
const getAriaHiddenAttr = (node2) => {
|
|
3060
|
+
const ariaHidden = node2.getAttribute("aria-hidden");
|
|
3061
|
+
return Boolean(ariaHidden && ariaHidden.value === "true");
|
|
3062
|
+
};
|
|
3063
|
+
return {
|
|
3064
|
+
byParent: node.parent ? isAriaHidden(node.parent) : false,
|
|
3065
|
+
bySelf: getAriaHiddenAttr(node)
|
|
3066
|
+
};
|
|
3067
|
+
}
|
|
3068
|
+
function isAriaHidden(node, details) {
|
|
3069
|
+
const cached = node.cacheGet(ARIA_HIDDEN_CACHE);
|
|
3070
|
+
if (cached) {
|
|
3071
|
+
return details ? cached : cached.byParent || cached.bySelf;
|
|
3122
3072
|
}
|
|
3073
|
+
const result = node.cacheSet(ARIA_HIDDEN_CACHE, isAriaHiddenImpl(node));
|
|
3074
|
+
return details ? result : result.byParent || result.bySelf;
|
|
3123
3075
|
}
|
|
3124
|
-
function
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3076
|
+
function isHTMLHiddenImpl(node) {
|
|
3077
|
+
const getHiddenAttr = (node2) => {
|
|
3078
|
+
const hidden = node2.getAttribute("hidden");
|
|
3079
|
+
return Boolean(hidden == null ? void 0 : hidden.isStatic);
|
|
3080
|
+
};
|
|
3081
|
+
return {
|
|
3082
|
+
byParent: node.parent ? isHTMLHidden(node.parent) : false,
|
|
3083
|
+
bySelf: getHiddenAttr(node)
|
|
3084
|
+
};
|
|
3085
|
+
}
|
|
3086
|
+
function isHTMLHidden(node, details) {
|
|
3087
|
+
const cached = node.cacheGet(HTML_HIDDEN_CACHE);
|
|
3088
|
+
if (cached) {
|
|
3089
|
+
return details ? cached : cached.byParent || cached.bySelf;
|
|
3132
3090
|
}
|
|
3091
|
+
const result = node.cacheSet(HTML_HIDDEN_CACHE, isHTMLHiddenImpl(node));
|
|
3092
|
+
return details ? result : result.byParent || result.bySelf;
|
|
3133
3093
|
}
|
|
3134
|
-
|
|
3135
|
-
const
|
|
3136
|
-
const
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
},
|
|
3149
|
-
"extends": {
|
|
3150
|
-
type: "array",
|
|
3151
|
-
items: {
|
|
3152
|
-
type: "string"
|
|
3153
|
-
},
|
|
3154
|
-
title: "Configurations to extend",
|
|
3155
|
-
description: "Array of shareable or builtin configurations to extend."
|
|
3156
|
-
},
|
|
3157
|
-
elements: {
|
|
3158
|
-
type: "array",
|
|
3159
|
-
items: {
|
|
3160
|
-
anyOf: [
|
|
3161
|
-
{
|
|
3162
|
-
type: "string"
|
|
3163
|
-
},
|
|
3164
|
-
{
|
|
3165
|
-
type: "object"
|
|
3166
|
-
}
|
|
3167
|
-
]
|
|
3168
|
-
},
|
|
3169
|
-
title: "Element metadata to load",
|
|
3170
|
-
description: "Array of modules, plugins or files to load element metadata from. Use <rootDir> to refer to the folder with the package.json file.",
|
|
3171
|
-
examples: [
|
|
3172
|
-
[
|
|
3173
|
-
"html-validate:recommended",
|
|
3174
|
-
"plugin:recommended",
|
|
3175
|
-
"module",
|
|
3176
|
-
"./local-file.json"
|
|
3177
|
-
]
|
|
3178
|
-
]
|
|
3179
|
-
},
|
|
3180
|
-
plugins: {
|
|
3181
|
-
type: "array",
|
|
3182
|
-
items: {
|
|
3183
|
-
anyOf: [
|
|
3184
|
-
{
|
|
3185
|
-
type: "string"
|
|
3186
|
-
},
|
|
3187
|
-
{
|
|
3188
|
-
type: "object"
|
|
3189
|
-
}
|
|
3190
|
-
]
|
|
3191
|
-
},
|
|
3192
|
-
title: "Plugins to load",
|
|
3193
|
-
description: "Array of plugins load. Use <rootDir> to refer to the folder with the package.json file.",
|
|
3194
|
-
examples: [
|
|
3195
|
-
[
|
|
3196
|
-
"my-plugin",
|
|
3197
|
-
"./local-plugin"
|
|
3198
|
-
]
|
|
3199
|
-
]
|
|
3200
|
-
},
|
|
3201
|
-
transform: {
|
|
3202
|
-
type: "object",
|
|
3203
|
-
additionalProperties: {
|
|
3204
|
-
type: "string"
|
|
3205
|
-
},
|
|
3206
|
-
title: "File transformations to use.",
|
|
3207
|
-
description: "Object where key is regular expression to match filename and value is name of transformer.",
|
|
3208
|
-
examples: [
|
|
3209
|
-
{
|
|
3210
|
-
"^.*\\.foo$": "my-transformer",
|
|
3211
|
-
"^.*\\.bar$": "my-plugin",
|
|
3212
|
-
"^.*\\.baz$": "my-plugin:named"
|
|
3213
|
-
}
|
|
3214
|
-
]
|
|
3215
|
-
},
|
|
3216
|
-
rules: {
|
|
3217
|
-
type: "object",
|
|
3218
|
-
patternProperties: {
|
|
3219
|
-
".*": {
|
|
3220
|
-
anyOf: [
|
|
3221
|
-
{
|
|
3222
|
-
"enum": [
|
|
3223
|
-
0,
|
|
3224
|
-
1,
|
|
3225
|
-
2,
|
|
3226
|
-
"off",
|
|
3227
|
-
"warn",
|
|
3228
|
-
"error"
|
|
3229
|
-
]
|
|
3230
|
-
},
|
|
3231
|
-
{
|
|
3232
|
-
type: "array",
|
|
3233
|
-
minItems: 1,
|
|
3234
|
-
maxItems: 1,
|
|
3235
|
-
items: [
|
|
3236
|
-
{
|
|
3237
|
-
"enum": [
|
|
3238
|
-
0,
|
|
3239
|
-
1,
|
|
3240
|
-
2,
|
|
3241
|
-
"off",
|
|
3242
|
-
"warn",
|
|
3243
|
-
"error"
|
|
3244
|
-
]
|
|
3245
|
-
}
|
|
3246
|
-
]
|
|
3247
|
-
},
|
|
3248
|
-
{
|
|
3249
|
-
type: "array",
|
|
3250
|
-
minItems: 2,
|
|
3251
|
-
maxItems: 2,
|
|
3252
|
-
items: [
|
|
3253
|
-
{
|
|
3254
|
-
"enum": [
|
|
3255
|
-
0,
|
|
3256
|
-
1,
|
|
3257
|
-
2,
|
|
3258
|
-
"off",
|
|
3259
|
-
"warn",
|
|
3260
|
-
"error"
|
|
3261
|
-
]
|
|
3262
|
-
},
|
|
3263
|
-
{
|
|
3264
|
-
}
|
|
3265
|
-
]
|
|
3266
|
-
}
|
|
3267
|
-
]
|
|
3268
|
-
}
|
|
3269
|
-
},
|
|
3270
|
-
title: "Rule configuration.",
|
|
3271
|
-
description: "Enable/disable rules, set severity. Some rules have additional configuration like style or patterns to use.",
|
|
3272
|
-
examples: [
|
|
3273
|
-
{
|
|
3274
|
-
foo: "error",
|
|
3275
|
-
bar: "off",
|
|
3276
|
-
baz: [
|
|
3277
|
-
"error",
|
|
3278
|
-
{
|
|
3279
|
-
style: "camelcase"
|
|
3280
|
-
}
|
|
3281
|
-
]
|
|
3282
|
-
}
|
|
3283
|
-
]
|
|
3284
|
-
}
|
|
3285
|
-
};
|
|
3286
|
-
var configurationSchema = {
|
|
3287
|
-
$schema: $schema,
|
|
3288
|
-
$id: $id,
|
|
3289
|
-
type: type,
|
|
3290
|
-
additionalProperties: additionalProperties,
|
|
3291
|
-
properties: properties
|
|
3292
|
-
};
|
|
3293
|
-
|
|
3294
|
-
const TRANSFORMER_API = {
|
|
3295
|
-
VERSION: 1
|
|
3296
|
-
};
|
|
3297
|
-
|
|
3298
|
-
var Severity = /* @__PURE__ */ ((Severity2) => {
|
|
3299
|
-
Severity2[Severity2["DISABLED"] = 0] = "DISABLED";
|
|
3300
|
-
Severity2[Severity2["WARN"] = 1] = "WARN";
|
|
3301
|
-
Severity2[Severity2["ERROR"] = 2] = "ERROR";
|
|
3302
|
-
return Severity2;
|
|
3303
|
-
})(Severity || {});
|
|
3304
|
-
function parseSeverity(value) {
|
|
3305
|
-
switch (value) {
|
|
3306
|
-
case 0:
|
|
3307
|
-
case "off":
|
|
3308
|
-
return 0 /* DISABLED */;
|
|
3309
|
-
case 1:
|
|
3310
|
-
case "warn":
|
|
3311
|
-
return 1 /* WARN */;
|
|
3312
|
-
case 2:
|
|
3313
|
-
case "error":
|
|
3314
|
-
return 2 /* ERROR */;
|
|
3315
|
-
default:
|
|
3316
|
-
throw new Error(`Invalid severity "${String(value)}"`);
|
|
3094
|
+
function isInertImpl(node) {
|
|
3095
|
+
const getInertAttr = (node2) => {
|
|
3096
|
+
const inert = node2.getAttribute("inert");
|
|
3097
|
+
return Boolean(inert == null ? void 0 : inert.isStatic);
|
|
3098
|
+
};
|
|
3099
|
+
return {
|
|
3100
|
+
byParent: node.parent ? isInert(node.parent) : false,
|
|
3101
|
+
bySelf: getInertAttr(node)
|
|
3102
|
+
};
|
|
3103
|
+
}
|
|
3104
|
+
function isInert(node, details) {
|
|
3105
|
+
const cached = node.cacheGet(INERT_CACHE);
|
|
3106
|
+
if (cached) {
|
|
3107
|
+
return details ? cached : cached.byParent || cached.bySelf;
|
|
3317
3108
|
}
|
|
3109
|
+
const result = node.cacheSet(INERT_CACHE, isInertImpl(node));
|
|
3110
|
+
return details ? result : result.byParent || result.bySelf;
|
|
3318
3111
|
}
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3112
|
+
function isStyleHiddenImpl(node) {
|
|
3113
|
+
const getStyleAttr = (node2) => {
|
|
3114
|
+
const style = node2.getAttribute("style");
|
|
3115
|
+
const { display, visibility } = parseCssDeclaration(style == null ? void 0 : style.value);
|
|
3116
|
+
return display === "none" || visibility === "hidden";
|
|
3117
|
+
};
|
|
3118
|
+
const byParent = node.parent ? isStyleHidden(node.parent) : false;
|
|
3119
|
+
const bySelf = getStyleAttr(node);
|
|
3120
|
+
return byParent || bySelf;
|
|
3322
3121
|
}
|
|
3323
|
-
function
|
|
3324
|
-
|
|
3325
|
-
|
|
3122
|
+
function isStyleHidden(node) {
|
|
3123
|
+
const cached = node.cacheGet(STYLE_HIDDEN_CACHE);
|
|
3124
|
+
if (cached) {
|
|
3125
|
+
return cached;
|
|
3326
3126
|
}
|
|
3327
|
-
|
|
3328
|
-
|
|
3127
|
+
return node.cacheSet(STYLE_HIDDEN_CACHE, isStyleHiddenImpl(node));
|
|
3128
|
+
}
|
|
3129
|
+
function isPresentation(node) {
|
|
3130
|
+
if (node.cacheExists(ROLE_PRESENTATION_CACHE)) {
|
|
3131
|
+
return Boolean(node.cacheGet(ROLE_PRESENTATION_CACHE));
|
|
3329
3132
|
}
|
|
3330
|
-
|
|
3331
|
-
|
|
3133
|
+
const meta = node.meta;
|
|
3134
|
+
if (meta == null ? void 0 : meta.interactive) {
|
|
3135
|
+
return node.cacheSet(ROLE_PRESENTATION_CACHE, false);
|
|
3332
3136
|
}
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
return
|
|
3137
|
+
const tabindex = node.getAttribute("tabindex");
|
|
3138
|
+
if (tabindex) {
|
|
3139
|
+
return node.cacheSet(ROLE_PRESENTATION_CACHE, false);
|
|
3336
3140
|
}
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
return
|
|
3141
|
+
const role = node.getAttribute("role");
|
|
3142
|
+
if (role && (role.value === "presentation" || role.value === "none")) {
|
|
3143
|
+
return node.cacheSet(ROLE_PRESENTATION_CACHE, true);
|
|
3144
|
+
} else {
|
|
3145
|
+
return node.cacheSet(ROLE_PRESENTATION_CACHE, false);
|
|
3340
3146
|
}
|
|
3341
|
-
return String(value);
|
|
3342
|
-
}
|
|
3343
|
-
function interpolate(text, data) {
|
|
3344
|
-
return text.replace(/{{\s*([^\s{}]+)\s*}}/g, (match, key) => {
|
|
3345
|
-
return typeof data[key] !== "undefined" ? format(data[key]) : match;
|
|
3346
|
-
});
|
|
3347
3147
|
}
|
|
3348
3148
|
|
|
3349
|
-
const
|
|
3350
|
-
const
|
|
3351
|
-
const
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
"
|
|
3356
|
-
"
|
|
3357
|
-
"
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3149
|
+
const cachePrefix = classifyNodeText.name;
|
|
3150
|
+
const HTML_CACHE_KEY = Symbol(`${cachePrefix}|html`);
|
|
3151
|
+
const A11Y_CACHE_KEY = Symbol(`${cachePrefix}|a11y`);
|
|
3152
|
+
const IGNORE_HIDDEN_ROOT_HTML_CACHE_KEY = Symbol(`${cachePrefix}|html|ignore-hidden-root`);
|
|
3153
|
+
const IGNORE_HIDDEN_ROOT_A11Y_CACHE_KEY = Symbol(`${cachePrefix}|a11y|ignore-hidden-root`);
|
|
3154
|
+
var TextClassification = /* @__PURE__ */ ((TextClassification2) => {
|
|
3155
|
+
TextClassification2[TextClassification2["EMPTY_TEXT"] = 0] = "EMPTY_TEXT";
|
|
3156
|
+
TextClassification2[TextClassification2["DYNAMIC_TEXT"] = 1] = "DYNAMIC_TEXT";
|
|
3157
|
+
TextClassification2[TextClassification2["STATIC_TEXT"] = 2] = "STATIC_TEXT";
|
|
3158
|
+
return TextClassification2;
|
|
3159
|
+
})(TextClassification || {});
|
|
3160
|
+
function getCachekey(options) {
|
|
3161
|
+
const { accessible = false, ignoreHiddenRoot = false } = options;
|
|
3162
|
+
if (accessible && ignoreHiddenRoot) {
|
|
3163
|
+
return IGNORE_HIDDEN_ROOT_A11Y_CACHE_KEY;
|
|
3164
|
+
} else if (ignoreHiddenRoot) {
|
|
3165
|
+
return IGNORE_HIDDEN_ROOT_HTML_CACHE_KEY;
|
|
3166
|
+
} else if (accessible) {
|
|
3167
|
+
return A11Y_CACHE_KEY;
|
|
3168
|
+
} else {
|
|
3169
|
+
return HTML_CACHE_KEY;
|
|
3170
|
+
}
|
|
3366
3171
|
}
|
|
3367
|
-
function
|
|
3368
|
-
return
|
|
3369
|
-
}
|
|
3370
|
-
function ariaNaming(element) {
|
|
3371
|
-
var _a;
|
|
3372
|
-
const cached = element.cacheGet(cacheKey);
|
|
3373
|
-
if (cached) {
|
|
3374
|
-
return cached;
|
|
3375
|
-
}
|
|
3376
|
-
const role = (_a = element.getAttribute("role")) == null ? void 0 : _a.value;
|
|
3377
|
-
if (role) {
|
|
3378
|
-
if (role instanceof DynamicValue) {
|
|
3379
|
-
return element.cacheSet(cacheKey, defaultValue);
|
|
3380
|
-
} else {
|
|
3381
|
-
return element.cacheSet(cacheKey, byRole(role));
|
|
3382
|
-
}
|
|
3383
|
-
}
|
|
3384
|
-
const meta = element.meta;
|
|
3385
|
-
if (!meta) {
|
|
3386
|
-
return element.cacheSet(cacheKey, defaultValue);
|
|
3387
|
-
}
|
|
3388
|
-
return element.cacheSet(cacheKey, byMeta(element, meta));
|
|
3389
|
-
}
|
|
3390
|
-
|
|
3391
|
-
const patternCache = /* @__PURE__ */ new Map();
|
|
3392
|
-
function compileStringPattern(pattern) {
|
|
3393
|
-
const regexp = pattern.replace(/[*]+/g, ".+");
|
|
3394
|
-
return new RegExp(`^${regexp}$`);
|
|
3395
|
-
}
|
|
3396
|
-
function compileRegExpPattern(pattern) {
|
|
3397
|
-
return new RegExp(`^${pattern}$`);
|
|
3398
|
-
}
|
|
3399
|
-
function compilePattern(pattern) {
|
|
3400
|
-
const cached = patternCache.get(pattern);
|
|
3401
|
-
if (cached) {
|
|
3402
|
-
return cached;
|
|
3403
|
-
}
|
|
3404
|
-
const match = pattern.match(/^\/(.*)\/$/);
|
|
3405
|
-
const regexp = match ? compileRegExpPattern(match[1]) : compileStringPattern(pattern);
|
|
3406
|
-
patternCache.set(pattern, regexp);
|
|
3407
|
-
return regexp;
|
|
3408
|
-
}
|
|
3409
|
-
function keywordPatternMatcher(list, keyword) {
|
|
3410
|
-
for (const pattern of list) {
|
|
3411
|
-
const regexp = compilePattern(pattern);
|
|
3412
|
-
if (regexp.test(keyword)) {
|
|
3413
|
-
return true;
|
|
3414
|
-
}
|
|
3415
|
-
}
|
|
3416
|
-
return false;
|
|
3417
|
-
}
|
|
3418
|
-
function isKeywordIgnored(options, keyword, matcher = (list, it) => list.includes(it)) {
|
|
3419
|
-
const { include, exclude } = options;
|
|
3420
|
-
if (include && !matcher(include, keyword)) {
|
|
3421
|
-
return true;
|
|
3422
|
-
}
|
|
3423
|
-
if (exclude && matcher(exclude, keyword)) {
|
|
3424
|
-
return true;
|
|
3425
|
-
}
|
|
3426
|
-
return false;
|
|
3427
|
-
}
|
|
3428
|
-
|
|
3429
|
-
const ARIA_HIDDEN_CACHE = Symbol(isAriaHidden.name);
|
|
3430
|
-
const HTML_HIDDEN_CACHE = Symbol(isHTMLHidden.name);
|
|
3431
|
-
const INERT_CACHE = Symbol(isInert.name);
|
|
3432
|
-
const ROLE_PRESENTATION_CACHE = Symbol(isPresentation.name);
|
|
3433
|
-
const STYLE_HIDDEN_CACHE = Symbol(isStyleHidden.name);
|
|
3434
|
-
function inAccessibilityTree(node) {
|
|
3435
|
-
if (isAriaHidden(node)) {
|
|
3436
|
-
return false;
|
|
3437
|
-
}
|
|
3438
|
-
if (isPresentation(node)) {
|
|
3439
|
-
return false;
|
|
3440
|
-
}
|
|
3441
|
-
if (isHTMLHidden(node)) {
|
|
3442
|
-
return false;
|
|
3443
|
-
}
|
|
3444
|
-
if (isInert(node)) {
|
|
3445
|
-
return false;
|
|
3446
|
-
}
|
|
3447
|
-
if (isStyleHidden(node)) {
|
|
3448
|
-
return false;
|
|
3449
|
-
}
|
|
3450
|
-
return true;
|
|
3451
|
-
}
|
|
3452
|
-
function isAriaHiddenImpl(node) {
|
|
3453
|
-
const getAriaHiddenAttr = (node2) => {
|
|
3454
|
-
const ariaHidden = node2.getAttribute("aria-hidden");
|
|
3455
|
-
return Boolean(ariaHidden && ariaHidden.value === "true");
|
|
3456
|
-
};
|
|
3457
|
-
return {
|
|
3458
|
-
byParent: node.parent ? isAriaHidden(node.parent) : false,
|
|
3459
|
-
bySelf: getAriaHiddenAttr(node)
|
|
3460
|
-
};
|
|
3461
|
-
}
|
|
3462
|
-
function isAriaHidden(node, details) {
|
|
3463
|
-
const cached = node.cacheGet(ARIA_HIDDEN_CACHE);
|
|
3464
|
-
if (cached) {
|
|
3465
|
-
return details ? cached : cached.byParent || cached.bySelf;
|
|
3466
|
-
}
|
|
3467
|
-
const result = node.cacheSet(ARIA_HIDDEN_CACHE, isAriaHiddenImpl(node));
|
|
3468
|
-
return details ? result : result.byParent || result.bySelf;
|
|
3469
|
-
}
|
|
3470
|
-
function isHTMLHiddenImpl(node) {
|
|
3471
|
-
const getHiddenAttr = (node2) => {
|
|
3472
|
-
const hidden = node2.getAttribute("hidden");
|
|
3473
|
-
return Boolean(hidden == null ? void 0 : hidden.isStatic);
|
|
3474
|
-
};
|
|
3475
|
-
return {
|
|
3476
|
-
byParent: node.parent ? isHTMLHidden(node.parent) : false,
|
|
3477
|
-
bySelf: getHiddenAttr(node)
|
|
3478
|
-
};
|
|
3479
|
-
}
|
|
3480
|
-
function isHTMLHidden(node, details) {
|
|
3481
|
-
const cached = node.cacheGet(HTML_HIDDEN_CACHE);
|
|
3482
|
-
if (cached) {
|
|
3483
|
-
return details ? cached : cached.byParent || cached.bySelf;
|
|
3484
|
-
}
|
|
3485
|
-
const result = node.cacheSet(HTML_HIDDEN_CACHE, isHTMLHiddenImpl(node));
|
|
3486
|
-
return details ? result : result.byParent || result.bySelf;
|
|
3487
|
-
}
|
|
3488
|
-
function isInertImpl(node) {
|
|
3489
|
-
const getInertAttr = (node2) => {
|
|
3490
|
-
const inert = node2.getAttribute("inert");
|
|
3491
|
-
return Boolean(inert == null ? void 0 : inert.isStatic);
|
|
3492
|
-
};
|
|
3493
|
-
return {
|
|
3494
|
-
byParent: node.parent ? isInert(node.parent) : false,
|
|
3495
|
-
bySelf: getInertAttr(node)
|
|
3496
|
-
};
|
|
3497
|
-
}
|
|
3498
|
-
function isInert(node, details) {
|
|
3499
|
-
const cached = node.cacheGet(INERT_CACHE);
|
|
3500
|
-
if (cached) {
|
|
3501
|
-
return details ? cached : cached.byParent || cached.bySelf;
|
|
3502
|
-
}
|
|
3503
|
-
const result = node.cacheSet(INERT_CACHE, isInertImpl(node));
|
|
3504
|
-
return details ? result : result.byParent || result.bySelf;
|
|
3505
|
-
}
|
|
3506
|
-
function isStyleHiddenImpl(node) {
|
|
3507
|
-
const getStyleAttr = (node2) => {
|
|
3508
|
-
const style = node2.getAttribute("style");
|
|
3509
|
-
const { display, visibility } = parseCssDeclaration(style == null ? void 0 : style.value);
|
|
3510
|
-
return display === "none" || visibility === "hidden";
|
|
3511
|
-
};
|
|
3512
|
-
const byParent = node.parent ? isStyleHidden(node.parent) : false;
|
|
3513
|
-
const bySelf = getStyleAttr(node);
|
|
3514
|
-
return byParent || bySelf;
|
|
3515
|
-
}
|
|
3516
|
-
function isStyleHidden(node) {
|
|
3517
|
-
const cached = node.cacheGet(STYLE_HIDDEN_CACHE);
|
|
3518
|
-
if (cached) {
|
|
3519
|
-
return cached;
|
|
3520
|
-
}
|
|
3521
|
-
return node.cacheSet(STYLE_HIDDEN_CACHE, isStyleHiddenImpl(node));
|
|
3522
|
-
}
|
|
3523
|
-
function isPresentation(node) {
|
|
3524
|
-
if (node.cacheExists(ROLE_PRESENTATION_CACHE)) {
|
|
3525
|
-
return Boolean(node.cacheGet(ROLE_PRESENTATION_CACHE));
|
|
3526
|
-
}
|
|
3527
|
-
const meta = node.meta;
|
|
3528
|
-
if (meta == null ? void 0 : meta.interactive) {
|
|
3529
|
-
return node.cacheSet(ROLE_PRESENTATION_CACHE, false);
|
|
3530
|
-
}
|
|
3531
|
-
const tabindex = node.getAttribute("tabindex");
|
|
3532
|
-
if (tabindex) {
|
|
3533
|
-
return node.cacheSet(ROLE_PRESENTATION_CACHE, false);
|
|
3534
|
-
}
|
|
3535
|
-
const role = node.getAttribute("role");
|
|
3536
|
-
if (role && (role.value === "presentation" || role.value === "none")) {
|
|
3537
|
-
return node.cacheSet(ROLE_PRESENTATION_CACHE, true);
|
|
3538
|
-
} else {
|
|
3539
|
-
return node.cacheSet(ROLE_PRESENTATION_CACHE, false);
|
|
3540
|
-
}
|
|
3541
|
-
}
|
|
3542
|
-
|
|
3543
|
-
const cachePrefix = classifyNodeText.name;
|
|
3544
|
-
const HTML_CACHE_KEY = Symbol(`${cachePrefix}|html`);
|
|
3545
|
-
const A11Y_CACHE_KEY = Symbol(`${cachePrefix}|a11y`);
|
|
3546
|
-
const IGNORE_HIDDEN_ROOT_HTML_CACHE_KEY = Symbol(`${cachePrefix}|html|ignore-hidden-root`);
|
|
3547
|
-
const IGNORE_HIDDEN_ROOT_A11Y_CACHE_KEY = Symbol(`${cachePrefix}|a11y|ignore-hidden-root`);
|
|
3548
|
-
var TextClassification = /* @__PURE__ */ ((TextClassification2) => {
|
|
3549
|
-
TextClassification2[TextClassification2["EMPTY_TEXT"] = 0] = "EMPTY_TEXT";
|
|
3550
|
-
TextClassification2[TextClassification2["DYNAMIC_TEXT"] = 1] = "DYNAMIC_TEXT";
|
|
3551
|
-
TextClassification2[TextClassification2["STATIC_TEXT"] = 2] = "STATIC_TEXT";
|
|
3552
|
-
return TextClassification2;
|
|
3553
|
-
})(TextClassification || {});
|
|
3554
|
-
function getCachekey(options) {
|
|
3555
|
-
const { accessible = false, ignoreHiddenRoot = false } = options;
|
|
3556
|
-
if (accessible && ignoreHiddenRoot) {
|
|
3557
|
-
return IGNORE_HIDDEN_ROOT_A11Y_CACHE_KEY;
|
|
3558
|
-
} else if (ignoreHiddenRoot) {
|
|
3559
|
-
return IGNORE_HIDDEN_ROOT_HTML_CACHE_KEY;
|
|
3560
|
-
} else if (accessible) {
|
|
3561
|
-
return A11Y_CACHE_KEY;
|
|
3562
|
-
} else {
|
|
3563
|
-
return HTML_CACHE_KEY;
|
|
3564
|
-
}
|
|
3565
|
-
}
|
|
3566
|
-
function isSpecialEmpty(node) {
|
|
3567
|
-
return node.is("select") || node.is("textarea");
|
|
3172
|
+
function isSpecialEmpty(node) {
|
|
3173
|
+
return node.is("select") || node.is("textarea");
|
|
3568
3174
|
}
|
|
3569
3175
|
function classifyNodeText(node, options = {}) {
|
|
3570
3176
|
const { accessible = false, ignoreHiddenRoot = false } = options;
|
|
@@ -5022,7 +4628,7 @@ class AttributeAllowedValues extends Rule {
|
|
|
5022
4628
|
setup() {
|
|
5023
4629
|
this.on("dom:ready", (event) => {
|
|
5024
4630
|
const doc = event.document;
|
|
5025
|
-
|
|
4631
|
+
walk.depthFirst(doc, (node) => {
|
|
5026
4632
|
const meta = node.meta;
|
|
5027
4633
|
if (!(meta == null ? void 0 : meta.attributes))
|
|
5028
4634
|
return;
|
|
@@ -5082,7 +4688,7 @@ class AttributeBooleanStyle extends Rule {
|
|
|
5082
4688
|
setup() {
|
|
5083
4689
|
this.on("dom:ready", (event) => {
|
|
5084
4690
|
const doc = event.document;
|
|
5085
|
-
|
|
4691
|
+
walk.depthFirst(doc, (node) => {
|
|
5086
4692
|
const meta = node.meta;
|
|
5087
4693
|
if (!(meta == null ? void 0 : meta.attributes))
|
|
5088
4694
|
return;
|
|
@@ -5154,7 +4760,7 @@ class AttributeEmptyStyle extends Rule {
|
|
|
5154
4760
|
setup() {
|
|
5155
4761
|
this.on("dom:ready", (event) => {
|
|
5156
4762
|
const doc = event.document;
|
|
5157
|
-
|
|
4763
|
+
walk.depthFirst(doc, (node) => {
|
|
5158
4764
|
const meta = node.meta;
|
|
5159
4765
|
if (!(meta == null ? void 0 : meta.attributes))
|
|
5160
4766
|
return;
|
|
@@ -5806,7 +5412,7 @@ class ElementPermittedContent extends Rule {
|
|
|
5806
5412
|
setup() {
|
|
5807
5413
|
this.on("dom:ready", (event) => {
|
|
5808
5414
|
const doc = event.document;
|
|
5809
|
-
|
|
5415
|
+
walk.depthFirst(doc, (node) => {
|
|
5810
5416
|
const parent = node.parent;
|
|
5811
5417
|
if (!parent) {
|
|
5812
5418
|
return;
|
|
@@ -5885,7 +5491,7 @@ class ElementPermittedOccurrences extends Rule {
|
|
|
5885
5491
|
setup() {
|
|
5886
5492
|
this.on("dom:ready", (event) => {
|
|
5887
5493
|
const doc = event.document;
|
|
5888
|
-
|
|
5494
|
+
walk.depthFirst(doc, (node) => {
|
|
5889
5495
|
if (!node.meta) {
|
|
5890
5496
|
return;
|
|
5891
5497
|
}
|
|
@@ -5918,7 +5524,7 @@ class ElementPermittedOrder extends Rule {
|
|
|
5918
5524
|
setup() {
|
|
5919
5525
|
this.on("dom:ready", (event) => {
|
|
5920
5526
|
const doc = event.document;
|
|
5921
|
-
|
|
5527
|
+
walk.depthFirst(doc, (node) => {
|
|
5922
5528
|
if (!node.meta) {
|
|
5923
5529
|
return;
|
|
5924
5530
|
}
|
|
@@ -5988,7 +5594,7 @@ class ElementPermittedParent extends Rule {
|
|
|
5988
5594
|
setup() {
|
|
5989
5595
|
this.on("dom:ready", (event) => {
|
|
5990
5596
|
const doc = event.document;
|
|
5991
|
-
|
|
5597
|
+
walk.depthFirst(doc, (node) => {
|
|
5992
5598
|
var _a;
|
|
5993
5599
|
const parent = node.parent;
|
|
5994
5600
|
if (!parent) {
|
|
@@ -6036,7 +5642,7 @@ class ElementRequiredAncestor extends Rule {
|
|
|
6036
5642
|
setup() {
|
|
6037
5643
|
this.on("dom:ready", (event) => {
|
|
6038
5644
|
const doc = event.document;
|
|
6039
|
-
|
|
5645
|
+
walk.depthFirst(doc, (node) => {
|
|
6040
5646
|
const parent = node.parent;
|
|
6041
5647
|
if (!parent) {
|
|
6042
5648
|
return;
|
|
@@ -6120,7 +5726,7 @@ class ElementRequiredContent extends Rule {
|
|
|
6120
5726
|
setup() {
|
|
6121
5727
|
this.on("dom:ready", (event) => {
|
|
6122
5728
|
const doc = event.document;
|
|
6123
|
-
|
|
5729
|
+
walk.depthFirst(doc, (node) => {
|
|
6124
5730
|
if (!node.meta) {
|
|
6125
5731
|
return;
|
|
6126
5732
|
}
|
|
@@ -6640,7 +6246,7 @@ function isFocusableImpl(element) {
|
|
|
6640
6246
|
if (isDisabled(element, meta)) {
|
|
6641
6247
|
return false;
|
|
6642
6248
|
}
|
|
6643
|
-
return Boolean(meta
|
|
6249
|
+
return Boolean(meta.focusable);
|
|
6644
6250
|
}
|
|
6645
6251
|
function isFocusable(element) {
|
|
6646
6252
|
const cached = element.cacheGet(FOCUSABLE_CACHE);
|
|
@@ -9382,946 +8988,1459 @@ function matchFieldNames2(token) {
|
|
|
9382
8988
|
function matchWebauthn(token) {
|
|
9383
8989
|
return token === "webauthn";
|
|
9384
8990
|
}
|
|
9385
|
-
function matchToken(token) {
|
|
9386
|
-
if (matchSection(token)) {
|
|
9387
|
-
return "section";
|
|
8991
|
+
function matchToken(token) {
|
|
8992
|
+
if (matchSection(token)) {
|
|
8993
|
+
return "section";
|
|
8994
|
+
}
|
|
8995
|
+
if (matchHint(token)) {
|
|
8996
|
+
return "hint";
|
|
8997
|
+
}
|
|
8998
|
+
if (matchFieldNames1(token)) {
|
|
8999
|
+
return "field1";
|
|
9000
|
+
}
|
|
9001
|
+
if (matchFieldNames2(token)) {
|
|
9002
|
+
return "field2";
|
|
9003
|
+
}
|
|
9004
|
+
if (matchContact(token)) {
|
|
9005
|
+
return "contact";
|
|
9006
|
+
}
|
|
9007
|
+
if (matchWebauthn(token)) {
|
|
9008
|
+
return "webauthn";
|
|
9009
|
+
}
|
|
9010
|
+
return null;
|
|
9011
|
+
}
|
|
9012
|
+
function getControlGroups(type) {
|
|
9013
|
+
const allGroups = [
|
|
9014
|
+
"text",
|
|
9015
|
+
"multiline",
|
|
9016
|
+
"password",
|
|
9017
|
+
"url",
|
|
9018
|
+
"username",
|
|
9019
|
+
"tel",
|
|
9020
|
+
"numeric",
|
|
9021
|
+
"month",
|
|
9022
|
+
"date"
|
|
9023
|
+
];
|
|
9024
|
+
const mapping = {
|
|
9025
|
+
hidden: allGroups,
|
|
9026
|
+
text: allGroups.filter((it) => it !== "multiline"),
|
|
9027
|
+
search: allGroups.filter((it) => it !== "multiline"),
|
|
9028
|
+
password: ["password"],
|
|
9029
|
+
url: ["url"],
|
|
9030
|
+
email: ["username"],
|
|
9031
|
+
tel: ["tel"],
|
|
9032
|
+
number: ["numeric"],
|
|
9033
|
+
month: ["month"],
|
|
9034
|
+
date: ["date"]
|
|
9035
|
+
};
|
|
9036
|
+
return mapping[type] ?? [];
|
|
9037
|
+
}
|
|
9038
|
+
function isDisallowedType(node, type) {
|
|
9039
|
+
if (!node.is("input")) {
|
|
9040
|
+
return false;
|
|
9041
|
+
}
|
|
9042
|
+
return disallowedInputTypes.includes(type);
|
|
9043
|
+
}
|
|
9044
|
+
function getTerminalMessage(context) {
|
|
9045
|
+
switch (context.msg) {
|
|
9046
|
+
case 0 /* InvalidAttribute */:
|
|
9047
|
+
return "autocomplete attribute cannot be used on {{ what }}";
|
|
9048
|
+
case 1 /* InvalidValue */:
|
|
9049
|
+
return '"{{ value }}" cannot be used on {{ what }}';
|
|
9050
|
+
case 2 /* InvalidOrder */:
|
|
9051
|
+
return '"{{ second }}" must appear before "{{ first }}"';
|
|
9052
|
+
case 3 /* InvalidToken */:
|
|
9053
|
+
return '"{{ token }}" is not a valid autocomplete token or field name';
|
|
9054
|
+
case 4 /* InvalidCombination */:
|
|
9055
|
+
return '"{{ second }}" cannot be combined with "{{ first }}"';
|
|
9056
|
+
case 5 /* MissingField */:
|
|
9057
|
+
return "autocomplete attribute is missing field name";
|
|
9058
|
+
}
|
|
9059
|
+
}
|
|
9060
|
+
function getMarkdownMessage(context) {
|
|
9061
|
+
switch (context.msg) {
|
|
9062
|
+
case 0 /* InvalidAttribute */:
|
|
9063
|
+
return [
|
|
9064
|
+
`\`autocomplete\` attribute cannot be used on \`${context.what}\``,
|
|
9065
|
+
"",
|
|
9066
|
+
"The following input types cannot use the `autocomplete` attribute:",
|
|
9067
|
+
"",
|
|
9068
|
+
...disallowedInputTypes.map((it) => `- \`${it}\``)
|
|
9069
|
+
].join("\n");
|
|
9070
|
+
case 1 /* InvalidValue */: {
|
|
9071
|
+
const message = `\`"${context.value}"\` cannot be used on \`${context.what}\``;
|
|
9072
|
+
if (context.type === "form") {
|
|
9073
|
+
return [
|
|
9074
|
+
message,
|
|
9075
|
+
"",
|
|
9076
|
+
'The `<form>` element can only use the values `"on"` and `"off"`.'
|
|
9077
|
+
].join("\n");
|
|
9078
|
+
}
|
|
9079
|
+
if (context.type === "hidden") {
|
|
9080
|
+
return [
|
|
9081
|
+
message,
|
|
9082
|
+
"",
|
|
9083
|
+
'`<input type="hidden">` cannot use the values `"on"` and `"off"`.'
|
|
9084
|
+
].join("\n");
|
|
9085
|
+
}
|
|
9086
|
+
const controlGroups = getControlGroups(context.type);
|
|
9087
|
+
const currentGroup = fieldNameGroup[context.value];
|
|
9088
|
+
return [
|
|
9089
|
+
message,
|
|
9090
|
+
"",
|
|
9091
|
+
`\`${context.what}\` allows autocomplete fields from the following group${controlGroups.length > 1 ? "s" : ""}:`,
|
|
9092
|
+
"",
|
|
9093
|
+
...controlGroups.map((it) => `- ${it}`),
|
|
9094
|
+
"",
|
|
9095
|
+
`The field \`"${context.value}"\` belongs to the group /${currentGroup}/ which cannot be used with this input type.`
|
|
9096
|
+
].join("\n");
|
|
9097
|
+
}
|
|
9098
|
+
case 2 /* InvalidOrder */:
|
|
9099
|
+
return [
|
|
9100
|
+
`\`"${context.second}"\` must appear before \`"${context.first}"\``,
|
|
9101
|
+
"",
|
|
9102
|
+
"The autocomplete tokens must appear in the following order:",
|
|
9103
|
+
"",
|
|
9104
|
+
"- Optional section name (`section-` prefix).",
|
|
9105
|
+
"- Optional `shipping` or `billing` token.",
|
|
9106
|
+
"- Optional `home`, `work`, `mobile`, `fax` or `pager` token (for fields supporting it).",
|
|
9107
|
+
"- Field name",
|
|
9108
|
+
"- Optional `webauthn` token."
|
|
9109
|
+
].join("\n");
|
|
9110
|
+
case 3 /* InvalidToken */:
|
|
9111
|
+
return `\`"${context.token}"\` is not a valid autocomplete token or field name`;
|
|
9112
|
+
case 4 /* InvalidCombination */:
|
|
9113
|
+
return `\`"${context.second}"\` cannot be combined with \`"${context.first}"\``;
|
|
9114
|
+
case 5 /* MissingField */:
|
|
9115
|
+
return "Autocomplete attribute is missing field name";
|
|
9116
|
+
}
|
|
9117
|
+
}
|
|
9118
|
+
class ValidAutocomplete extends Rule {
|
|
9119
|
+
documentation(context) {
|
|
9120
|
+
return {
|
|
9121
|
+
description: getMarkdownMessage(context),
|
|
9122
|
+
url: "https://html-validate.org/rules/valid-autocomplete.html"
|
|
9123
|
+
};
|
|
9124
|
+
}
|
|
9125
|
+
setup() {
|
|
9126
|
+
this.on("dom:ready", (event) => {
|
|
9127
|
+
const { document } = event;
|
|
9128
|
+
const elements = document.querySelectorAll("[autocomplete]");
|
|
9129
|
+
for (const element of elements) {
|
|
9130
|
+
const autocomplete = element.getAttribute("autocomplete");
|
|
9131
|
+
if (autocomplete.value === null || autocomplete.value instanceof DynamicValue) {
|
|
9132
|
+
continue;
|
|
9133
|
+
}
|
|
9134
|
+
const location = autocomplete.valueLocation;
|
|
9135
|
+
const value = autocomplete.value.toLowerCase();
|
|
9136
|
+
const tokens = new DOMTokenList(value, location);
|
|
9137
|
+
if (tokens.length === 0) {
|
|
9138
|
+
continue;
|
|
9139
|
+
}
|
|
9140
|
+
this.validate(element, value, tokens, autocomplete.keyLocation, location);
|
|
9141
|
+
}
|
|
9142
|
+
});
|
|
9143
|
+
}
|
|
9144
|
+
validate(node, value, tokens, keyLocation, valueLocation) {
|
|
9145
|
+
switch (node.tagName) {
|
|
9146
|
+
case "form":
|
|
9147
|
+
this.validateFormAutocomplete(node, value, valueLocation);
|
|
9148
|
+
break;
|
|
9149
|
+
case "input":
|
|
9150
|
+
case "textarea":
|
|
9151
|
+
case "select":
|
|
9152
|
+
this.validateControlAutocomplete(node, tokens, keyLocation);
|
|
9153
|
+
break;
|
|
9154
|
+
}
|
|
9155
|
+
}
|
|
9156
|
+
validateControlAutocomplete(node, tokens, keyLocation) {
|
|
9157
|
+
const type = node.getAttributeValue("type") ?? "text";
|
|
9158
|
+
const mantle = type !== "hidden" ? "expectation" : "anchor";
|
|
9159
|
+
if (isDisallowedType(node, type)) {
|
|
9160
|
+
const context = {
|
|
9161
|
+
msg: 0 /* InvalidAttribute */,
|
|
9162
|
+
what: `<input type="${type}">`
|
|
9163
|
+
};
|
|
9164
|
+
this.report({
|
|
9165
|
+
node,
|
|
9166
|
+
message: getTerminalMessage(context),
|
|
9167
|
+
location: keyLocation,
|
|
9168
|
+
context
|
|
9169
|
+
});
|
|
9170
|
+
return;
|
|
9171
|
+
}
|
|
9172
|
+
if (tokens.includes("on") || tokens.includes("off")) {
|
|
9173
|
+
this.validateOnOff(node, mantle, tokens);
|
|
9174
|
+
return;
|
|
9175
|
+
}
|
|
9176
|
+
this.validateTokens(node, tokens, keyLocation);
|
|
9177
|
+
}
|
|
9178
|
+
validateFormAutocomplete(node, value, location) {
|
|
9179
|
+
const trimmed = value.trim();
|
|
9180
|
+
if (["on", "off"].includes(trimmed)) {
|
|
9181
|
+
return;
|
|
9182
|
+
}
|
|
9183
|
+
const context = {
|
|
9184
|
+
msg: 1 /* InvalidValue */,
|
|
9185
|
+
type: "form",
|
|
9186
|
+
value: trimmed,
|
|
9187
|
+
what: "<form>"
|
|
9188
|
+
};
|
|
9189
|
+
this.report({
|
|
9190
|
+
node,
|
|
9191
|
+
message: getTerminalMessage(context),
|
|
9192
|
+
location,
|
|
9193
|
+
context
|
|
9194
|
+
});
|
|
9195
|
+
}
|
|
9196
|
+
validateOnOff(node, mantle, tokens) {
|
|
9197
|
+
const index = tokens.findIndex((it) => it === "on" || it === "off");
|
|
9198
|
+
const value = tokens.item(index);
|
|
9199
|
+
const location = tokens.location(index);
|
|
9200
|
+
if (tokens.length > 1) {
|
|
9201
|
+
const context = {
|
|
9202
|
+
msg: 4 /* InvalidCombination */,
|
|
9203
|
+
/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
|
|
9204
|
+
first: tokens.item(index > 0 ? 0 : 1),
|
|
9205
|
+
second: value
|
|
9206
|
+
};
|
|
9207
|
+
this.report({
|
|
9208
|
+
node,
|
|
9209
|
+
message: getTerminalMessage(context),
|
|
9210
|
+
location,
|
|
9211
|
+
context
|
|
9212
|
+
});
|
|
9213
|
+
}
|
|
9214
|
+
switch (mantle) {
|
|
9215
|
+
case "expectation":
|
|
9216
|
+
return;
|
|
9217
|
+
case "anchor": {
|
|
9218
|
+
const context = {
|
|
9219
|
+
msg: 1 /* InvalidValue */,
|
|
9220
|
+
type: "hidden",
|
|
9221
|
+
value,
|
|
9222
|
+
what: `<input type="hidden">`
|
|
9223
|
+
};
|
|
9224
|
+
this.report({
|
|
9225
|
+
node,
|
|
9226
|
+
message: getTerminalMessage(context),
|
|
9227
|
+
location: tokens.location(0),
|
|
9228
|
+
context
|
|
9229
|
+
});
|
|
9230
|
+
}
|
|
9231
|
+
}
|
|
9232
|
+
}
|
|
9233
|
+
validateTokens(node, tokens, keyLocation) {
|
|
9234
|
+
const order = [];
|
|
9235
|
+
for (const { item, location } of tokens.iterator()) {
|
|
9236
|
+
const tokenType = matchToken(item);
|
|
9237
|
+
if (tokenType) {
|
|
9238
|
+
order.push(tokenType);
|
|
9239
|
+
} else {
|
|
9240
|
+
const context = {
|
|
9241
|
+
msg: 3 /* InvalidToken */,
|
|
9242
|
+
token: item
|
|
9243
|
+
};
|
|
9244
|
+
this.report({
|
|
9245
|
+
node,
|
|
9246
|
+
message: getTerminalMessage(context),
|
|
9247
|
+
location,
|
|
9248
|
+
context
|
|
9249
|
+
});
|
|
9250
|
+
return;
|
|
9251
|
+
}
|
|
9252
|
+
}
|
|
9253
|
+
const fieldTokens = order.map((it) => it === "field1" || it === "field2");
|
|
9254
|
+
this.validateFieldPresence(node, tokens, fieldTokens, keyLocation);
|
|
9255
|
+
this.validateContact(node, tokens, order);
|
|
9256
|
+
this.validateOrder(node, tokens, order);
|
|
9257
|
+
this.validateControlGroup(node, tokens, fieldTokens);
|
|
9258
|
+
}
|
|
9259
|
+
/**
|
|
9260
|
+
* Ensure that exactly one field name is present from the two field lists.
|
|
9261
|
+
*/
|
|
9262
|
+
validateFieldPresence(node, tokens, fieldTokens, keyLocation) {
|
|
9263
|
+
const numFields = fieldTokens.filter(Boolean).length;
|
|
9264
|
+
if (numFields === 0) {
|
|
9265
|
+
const context = {
|
|
9266
|
+
msg: 5 /* MissingField */
|
|
9267
|
+
};
|
|
9268
|
+
this.report({
|
|
9269
|
+
node,
|
|
9270
|
+
message: getTerminalMessage(context),
|
|
9271
|
+
location: keyLocation,
|
|
9272
|
+
context
|
|
9273
|
+
});
|
|
9274
|
+
} else if (numFields > 1) {
|
|
9275
|
+
const a = fieldTokens.indexOf(true);
|
|
9276
|
+
const b = fieldTokens.lastIndexOf(true);
|
|
9277
|
+
const context = {
|
|
9278
|
+
msg: 4 /* InvalidCombination */,
|
|
9279
|
+
/* eslint-disable @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
|
|
9280
|
+
first: tokens.item(a),
|
|
9281
|
+
second: tokens.item(b)
|
|
9282
|
+
/* eslint-enable @typescript-eslint/no-non-null-assertion */
|
|
9283
|
+
};
|
|
9284
|
+
this.report({
|
|
9285
|
+
node,
|
|
9286
|
+
message: getTerminalMessage(context),
|
|
9287
|
+
location: tokens.location(b),
|
|
9288
|
+
context
|
|
9289
|
+
});
|
|
9290
|
+
}
|
|
9291
|
+
}
|
|
9292
|
+
/**
|
|
9293
|
+
* Ensure contact token is only used with field names from the second list.
|
|
9294
|
+
*/
|
|
9295
|
+
validateContact(node, tokens, order) {
|
|
9296
|
+
if (order.includes("contact") && order.includes("field1")) {
|
|
9297
|
+
const a = order.indexOf("field1");
|
|
9298
|
+
const b = order.indexOf("contact");
|
|
9299
|
+
const context = {
|
|
9300
|
+
msg: 4 /* InvalidCombination */,
|
|
9301
|
+
/* eslint-disable @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
|
|
9302
|
+
first: tokens.item(a),
|
|
9303
|
+
second: tokens.item(b)
|
|
9304
|
+
/* eslint-enable @typescript-eslint/no-non-null-assertion */
|
|
9305
|
+
};
|
|
9306
|
+
this.report({
|
|
9307
|
+
node,
|
|
9308
|
+
message: getTerminalMessage(context),
|
|
9309
|
+
location: tokens.location(b),
|
|
9310
|
+
context
|
|
9311
|
+
});
|
|
9312
|
+
}
|
|
9313
|
+
}
|
|
9314
|
+
validateOrder(node, tokens, order) {
|
|
9315
|
+
const indicies = order.map((it) => expectedOrder.indexOf(it));
|
|
9316
|
+
for (let i = 0; i < indicies.length - 1; i++) {
|
|
9317
|
+
if (indicies[0] > indicies[i + 1]) {
|
|
9318
|
+
const context = {
|
|
9319
|
+
msg: 2 /* InvalidOrder */,
|
|
9320
|
+
/* eslint-disable @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
|
|
9321
|
+
first: tokens.item(i),
|
|
9322
|
+
second: tokens.item(i + 1)
|
|
9323
|
+
/* eslint-enable @typescript-eslint/no-non-null-assertion */
|
|
9324
|
+
};
|
|
9325
|
+
this.report({
|
|
9326
|
+
node,
|
|
9327
|
+
message: getTerminalMessage(context),
|
|
9328
|
+
location: tokens.location(i + 1),
|
|
9329
|
+
context
|
|
9330
|
+
});
|
|
9331
|
+
}
|
|
9332
|
+
}
|
|
9333
|
+
}
|
|
9334
|
+
validateControlGroup(node, tokens, fieldTokens) {
|
|
9335
|
+
const numFields = fieldTokens.filter(Boolean).length;
|
|
9336
|
+
if (numFields === 0) {
|
|
9337
|
+
return;
|
|
9338
|
+
}
|
|
9339
|
+
if (!node.is("input")) {
|
|
9340
|
+
return;
|
|
9341
|
+
}
|
|
9342
|
+
const attr = node.getAttribute("type");
|
|
9343
|
+
const type = (attr == null ? void 0 : attr.value) ?? "text";
|
|
9344
|
+
if (type instanceof DynamicValue) {
|
|
9345
|
+
return;
|
|
9346
|
+
}
|
|
9347
|
+
const controlGroups = getControlGroups(type);
|
|
9348
|
+
const fieldIndex = fieldTokens.indexOf(true);
|
|
9349
|
+
const fieldToken = tokens.item(fieldIndex);
|
|
9350
|
+
const fieldGroup = fieldNameGroup[fieldToken];
|
|
9351
|
+
if (!controlGroups.includes(fieldGroup)) {
|
|
9352
|
+
const context = {
|
|
9353
|
+
msg: 1 /* InvalidValue */,
|
|
9354
|
+
type,
|
|
9355
|
+
value: fieldToken,
|
|
9356
|
+
what: `<input type="${type}">`
|
|
9357
|
+
};
|
|
9358
|
+
this.report({
|
|
9359
|
+
node,
|
|
9360
|
+
message: getTerminalMessage(context),
|
|
9361
|
+
location: tokens.location(fieldIndex),
|
|
9362
|
+
context
|
|
9363
|
+
});
|
|
9364
|
+
}
|
|
9365
|
+
}
|
|
9366
|
+
}
|
|
9367
|
+
|
|
9368
|
+
const defaults$3 = {
|
|
9369
|
+
relaxed: false
|
|
9370
|
+
};
|
|
9371
|
+
class ValidID extends Rule {
|
|
9372
|
+
constructor(options) {
|
|
9373
|
+
super({ ...defaults$3, ...options });
|
|
9388
9374
|
}
|
|
9389
|
-
|
|
9390
|
-
return
|
|
9375
|
+
static schema() {
|
|
9376
|
+
return {
|
|
9377
|
+
relaxed: {
|
|
9378
|
+
type: "boolean"
|
|
9379
|
+
}
|
|
9380
|
+
};
|
|
9391
9381
|
}
|
|
9392
|
-
|
|
9393
|
-
|
|
9382
|
+
documentation(context) {
|
|
9383
|
+
const { relaxed } = this.options;
|
|
9384
|
+
const message = this.messages[context].replace("id", "ID").replace(/^(.)/, (m) => m.toUpperCase());
|
|
9385
|
+
const relaxedDescription = relaxed ? [] : [
|
|
9386
|
+
" - ID must begin with a letter",
|
|
9387
|
+
" - ID must only contain letters, digits, `-` and `_`"
|
|
9388
|
+
];
|
|
9389
|
+
return {
|
|
9390
|
+
description: [
|
|
9391
|
+
`${message}.`,
|
|
9392
|
+
"",
|
|
9393
|
+
"Under the current configuration the following rules are applied:",
|
|
9394
|
+
"",
|
|
9395
|
+
" - ID must not be empty",
|
|
9396
|
+
" - ID must not contain any whitespace characters",
|
|
9397
|
+
...relaxedDescription
|
|
9398
|
+
].join("\n"),
|
|
9399
|
+
url: "https://html-validate.org/rules/valid-id.html"
|
|
9400
|
+
};
|
|
9394
9401
|
}
|
|
9395
|
-
|
|
9396
|
-
|
|
9402
|
+
setup() {
|
|
9403
|
+
this.on("attr", this.isRelevant, (event) => {
|
|
9404
|
+
const { value } = event;
|
|
9405
|
+
if (value === null || value instanceof DynamicValue) {
|
|
9406
|
+
return;
|
|
9407
|
+
}
|
|
9408
|
+
if (value === "") {
|
|
9409
|
+
const context = 1 /* EMPTY */;
|
|
9410
|
+
this.report(event.target, this.messages[context], event.location, context);
|
|
9411
|
+
return;
|
|
9412
|
+
}
|
|
9413
|
+
if (value.match(/\s/)) {
|
|
9414
|
+
const context = 2 /* WHITESPACE */;
|
|
9415
|
+
this.report(event.target, this.messages[context], event.valueLocation, context);
|
|
9416
|
+
return;
|
|
9417
|
+
}
|
|
9418
|
+
const { relaxed } = this.options;
|
|
9419
|
+
if (relaxed) {
|
|
9420
|
+
return;
|
|
9421
|
+
}
|
|
9422
|
+
if (value.match(/^[^\p{L}]/u)) {
|
|
9423
|
+
const context = 3 /* LEADING_CHARACTER */;
|
|
9424
|
+
this.report(event.target, this.messages[context], event.valueLocation, context);
|
|
9425
|
+
return;
|
|
9426
|
+
}
|
|
9427
|
+
if (value.match(/[^\p{L}\p{N}_-]/u)) {
|
|
9428
|
+
const context = 4 /* DISALLOWED_CHARACTER */;
|
|
9429
|
+
this.report(event.target, this.messages[context], event.valueLocation, context);
|
|
9430
|
+
}
|
|
9431
|
+
});
|
|
9397
9432
|
}
|
|
9398
|
-
|
|
9399
|
-
return
|
|
9433
|
+
get messages() {
|
|
9434
|
+
return {
|
|
9435
|
+
[1 /* EMPTY */]: "element id must not be empty",
|
|
9436
|
+
[2 /* WHITESPACE */]: "element id must not contain whitespace",
|
|
9437
|
+
[3 /* LEADING_CHARACTER */]: "element id must begin with a letter",
|
|
9438
|
+
[4 /* DISALLOWED_CHARACTER */]: "element id must only contain letters, digits, dash and underscore characters"
|
|
9439
|
+
};
|
|
9400
9440
|
}
|
|
9401
|
-
|
|
9402
|
-
return "
|
|
9441
|
+
isRelevant(event) {
|
|
9442
|
+
return event.key === "id";
|
|
9403
9443
|
}
|
|
9404
|
-
return null;
|
|
9405
9444
|
}
|
|
9406
|
-
|
|
9407
|
-
|
|
9408
|
-
|
|
9409
|
-
|
|
9410
|
-
|
|
9411
|
-
|
|
9412
|
-
|
|
9413
|
-
|
|
9414
|
-
|
|
9415
|
-
|
|
9416
|
-
|
|
9417
|
-
];
|
|
9418
|
-
const mapping = {
|
|
9419
|
-
hidden: allGroups,
|
|
9420
|
-
text: allGroups.filter((it) => it !== "multiline"),
|
|
9421
|
-
search: allGroups.filter((it) => it !== "multiline"),
|
|
9422
|
-
password: ["password"],
|
|
9423
|
-
url: ["url"],
|
|
9424
|
-
email: ["username"],
|
|
9425
|
-
tel: ["tel"],
|
|
9426
|
-
number: ["numeric"],
|
|
9427
|
-
month: ["month"],
|
|
9428
|
-
date: ["date"]
|
|
9429
|
-
};
|
|
9430
|
-
const groups = mapping[type];
|
|
9431
|
-
if (groups) {
|
|
9432
|
-
return groups;
|
|
9445
|
+
|
|
9446
|
+
class VoidContent extends Rule {
|
|
9447
|
+
documentation(tagName) {
|
|
9448
|
+
const doc = {
|
|
9449
|
+
description: "HTML void elements cannot have any content and must not have content or end tag.",
|
|
9450
|
+
url: "https://html-validate.org/rules/void-content.html"
|
|
9451
|
+
};
|
|
9452
|
+
if (tagName) {
|
|
9453
|
+
doc.description = `<${tagName}> is a void element and must not have content or end tag.`;
|
|
9454
|
+
}
|
|
9455
|
+
return doc;
|
|
9433
9456
|
}
|
|
9434
|
-
|
|
9435
|
-
|
|
9436
|
-
|
|
9437
|
-
|
|
9438
|
-
|
|
9457
|
+
setup() {
|
|
9458
|
+
this.on("tag:end", (event) => {
|
|
9459
|
+
const node = event.target;
|
|
9460
|
+
if (!node) {
|
|
9461
|
+
return;
|
|
9462
|
+
}
|
|
9463
|
+
if (!node.voidElement) {
|
|
9464
|
+
return;
|
|
9465
|
+
}
|
|
9466
|
+
if (node.closed === NodeClosed.EndTag) {
|
|
9467
|
+
this.report(
|
|
9468
|
+
null,
|
|
9469
|
+
`End tag for <${node.tagName}> must be omitted`,
|
|
9470
|
+
node.location,
|
|
9471
|
+
node.tagName
|
|
9472
|
+
);
|
|
9473
|
+
}
|
|
9474
|
+
});
|
|
9439
9475
|
}
|
|
9440
|
-
return disallowedInputTypes.includes(type);
|
|
9441
9476
|
}
|
|
9442
|
-
|
|
9443
|
-
|
|
9444
|
-
|
|
9445
|
-
|
|
9446
|
-
|
|
9447
|
-
|
|
9448
|
-
|
|
9449
|
-
|
|
9450
|
-
case 3 /* InvalidToken */:
|
|
9451
|
-
return '"{{ token }}" is not a valid autocomplete token or field name';
|
|
9452
|
-
case 4 /* InvalidCombination */:
|
|
9453
|
-
return '"{{ second }}" cannot be combined with "{{ first }}"';
|
|
9454
|
-
case 5 /* MissingField */:
|
|
9455
|
-
return "autocomplete attribute is missing field name";
|
|
9477
|
+
|
|
9478
|
+
const defaults$2 = {
|
|
9479
|
+
style: "omit"
|
|
9480
|
+
};
|
|
9481
|
+
class VoidStyle extends Rule {
|
|
9482
|
+
constructor(options) {
|
|
9483
|
+
super({ ...defaults$2, ...options });
|
|
9484
|
+
this.style = parseStyle(this.options.style);
|
|
9456
9485
|
}
|
|
9457
|
-
|
|
9458
|
-
|
|
9459
|
-
|
|
9460
|
-
|
|
9461
|
-
|
|
9462
|
-
|
|
9463
|
-
|
|
9464
|
-
"The following input types cannot use the `autocomplete` attribute:",
|
|
9465
|
-
"",
|
|
9466
|
-
...disallowedInputTypes.map((it) => `- \`${it}\``)
|
|
9467
|
-
].join("\n");
|
|
9468
|
-
case 1 /* InvalidValue */: {
|
|
9469
|
-
const message = `\`"${context.value}"\` cannot be used on \`${context.what}\``;
|
|
9470
|
-
if (context.type === "form") {
|
|
9471
|
-
return [
|
|
9472
|
-
message,
|
|
9473
|
-
"",
|
|
9474
|
-
'The `<form>` element can only use the values `"on"` and `"off"`.'
|
|
9475
|
-
].join("\n");
|
|
9476
|
-
}
|
|
9477
|
-
if (context.type === "hidden") {
|
|
9478
|
-
return [
|
|
9479
|
-
message,
|
|
9480
|
-
"",
|
|
9481
|
-
'`<input type="hidden">` cannot use the values `"on"` and `"off"`.'
|
|
9482
|
-
].join("\n");
|
|
9483
|
-
}
|
|
9484
|
-
const controlGroups = getControlGroups(context.type);
|
|
9485
|
-
const currentGroup = fieldNameGroup[context.value];
|
|
9486
|
-
return [
|
|
9487
|
-
message,
|
|
9488
|
-
"",
|
|
9489
|
-
`\`${context.what}\` allows autocomplete fields from the following group${controlGroups.length > 1 ? "s" : ""}:`,
|
|
9490
|
-
"",
|
|
9491
|
-
...controlGroups.map((it) => `- ${it}`),
|
|
9492
|
-
"",
|
|
9493
|
-
`The field \`"${context.value}"\` belongs to the group /${currentGroup}/ which cannot be used with this input type.`
|
|
9494
|
-
].join("\n");
|
|
9495
|
-
}
|
|
9496
|
-
case 2 /* InvalidOrder */:
|
|
9497
|
-
return [
|
|
9498
|
-
`\`"${context.second}"\` must appear before \`"${context.first}"\``,
|
|
9499
|
-
"",
|
|
9500
|
-
"The autocomplete tokens must appear in the following order:",
|
|
9501
|
-
"",
|
|
9502
|
-
"- Optional section name (`section-` prefix).",
|
|
9503
|
-
"- Optional `shipping` or `billing` token.",
|
|
9504
|
-
"- Optional `home`, `work`, `mobile`, `fax` or `pager` token (for fields supporting it).",
|
|
9505
|
-
"- Field name",
|
|
9506
|
-
"- Optional `webauthn` token."
|
|
9507
|
-
].join("\n");
|
|
9508
|
-
case 3 /* InvalidToken */:
|
|
9509
|
-
return `\`"${context.token}"\` is not a valid autocomplete token or field name`;
|
|
9510
|
-
case 4 /* InvalidCombination */:
|
|
9511
|
-
return `\`"${context.second}"\` cannot be combined with \`"${context.first}"\``;
|
|
9512
|
-
case 5 /* MissingField */:
|
|
9513
|
-
return "Autocomplete attribute is missing field name";
|
|
9486
|
+
static schema() {
|
|
9487
|
+
return {
|
|
9488
|
+
style: {
|
|
9489
|
+
enum: ["omit", "selfclose", "selfclosing"],
|
|
9490
|
+
type: "string"
|
|
9491
|
+
}
|
|
9492
|
+
};
|
|
9514
9493
|
}
|
|
9515
|
-
}
|
|
9516
|
-
class ValidAutocomplete extends Rule {
|
|
9517
9494
|
documentation(context) {
|
|
9495
|
+
const [desc, end] = styleDescription(context.style);
|
|
9518
9496
|
return {
|
|
9519
|
-
description:
|
|
9520
|
-
url: "https://html-validate.org/rules/
|
|
9497
|
+
description: `The current configuration requires void elements to ${desc}, use <${context.tagName}${end}> instead.`,
|
|
9498
|
+
url: "https://html-validate.org/rules/void-style.html"
|
|
9521
9499
|
};
|
|
9522
9500
|
}
|
|
9523
9501
|
setup() {
|
|
9524
|
-
this.on("
|
|
9525
|
-
const
|
|
9526
|
-
|
|
9527
|
-
|
|
9528
|
-
const autocomplete = element.getAttribute("autocomplete");
|
|
9529
|
-
if (autocomplete.value === null || autocomplete.value instanceof DynamicValue) {
|
|
9530
|
-
continue;
|
|
9531
|
-
}
|
|
9532
|
-
const location = autocomplete.valueLocation;
|
|
9533
|
-
const value = autocomplete.value.toLowerCase();
|
|
9534
|
-
const tokens = new DOMTokenList(value, location);
|
|
9535
|
-
if (tokens.length === 0) {
|
|
9536
|
-
continue;
|
|
9537
|
-
}
|
|
9538
|
-
this.validate(element, value, tokens, autocomplete.keyLocation, location);
|
|
9502
|
+
this.on("tag:end", (event) => {
|
|
9503
|
+
const active = event.previous;
|
|
9504
|
+
if (active.meta) {
|
|
9505
|
+
this.validateActive(active);
|
|
9539
9506
|
}
|
|
9540
9507
|
});
|
|
9541
9508
|
}
|
|
9542
|
-
|
|
9543
|
-
|
|
9544
|
-
|
|
9545
|
-
this.validateFormAutocomplete(node, value, valueLocation);
|
|
9546
|
-
break;
|
|
9547
|
-
case "input":
|
|
9548
|
-
case "textarea":
|
|
9549
|
-
case "select":
|
|
9550
|
-
this.validateControlAutocomplete(node, tokens, keyLocation);
|
|
9551
|
-
break;
|
|
9509
|
+
validateActive(node) {
|
|
9510
|
+
if (!node.voidElement) {
|
|
9511
|
+
return;
|
|
9552
9512
|
}
|
|
9553
|
-
|
|
9554
|
-
|
|
9555
|
-
const type = node.getAttributeValue("type") ?? "text";
|
|
9556
|
-
const mantle = type !== "hidden" ? "expectation" : "anchor";
|
|
9557
|
-
if (isDisallowedType(node, type)) {
|
|
9558
|
-
const context = {
|
|
9559
|
-
msg: 0 /* InvalidAttribute */,
|
|
9560
|
-
what: `<input type="${type}">`
|
|
9561
|
-
};
|
|
9562
|
-
this.report({
|
|
9513
|
+
if (this.shouldBeOmitted(node)) {
|
|
9514
|
+
this.reportError(
|
|
9563
9515
|
node,
|
|
9564
|
-
|
|
9565
|
-
|
|
9566
|
-
context
|
|
9567
|
-
});
|
|
9568
|
-
return;
|
|
9516
|
+
`Expected omitted end tag <${node.tagName}> instead of self-closing element <${node.tagName}/>`
|
|
9517
|
+
);
|
|
9569
9518
|
}
|
|
9570
|
-
if (
|
|
9571
|
-
this.
|
|
9572
|
-
|
|
9519
|
+
if (this.shouldBeSelfClosed(node)) {
|
|
9520
|
+
this.reportError(
|
|
9521
|
+
node,
|
|
9522
|
+
`Expected self-closing element <${node.tagName}/> instead of omitted end-tag <${node.tagName}>`
|
|
9523
|
+
);
|
|
9573
9524
|
}
|
|
9574
|
-
this.validateTokens(node, tokens, keyLocation);
|
|
9575
9525
|
}
|
|
9576
|
-
|
|
9577
|
-
const trimmed = value.trim();
|
|
9578
|
-
if (["on", "off"].includes(trimmed)) {
|
|
9579
|
-
return;
|
|
9580
|
-
}
|
|
9526
|
+
reportError(node, message) {
|
|
9581
9527
|
const context = {
|
|
9582
|
-
|
|
9583
|
-
|
|
9584
|
-
value: trimmed,
|
|
9585
|
-
what: "<form>"
|
|
9528
|
+
style: this.style,
|
|
9529
|
+
tagName: node.tagName
|
|
9586
9530
|
};
|
|
9587
|
-
|
|
9588
|
-
node,
|
|
9589
|
-
message: getTerminalMessage(context),
|
|
9590
|
-
location,
|
|
9591
|
-
context
|
|
9592
|
-
});
|
|
9531
|
+
super.report(node, message, null, context);
|
|
9593
9532
|
}
|
|
9594
|
-
|
|
9595
|
-
|
|
9596
|
-
const value = tokens.item(index);
|
|
9597
|
-
const location = tokens.location(index);
|
|
9598
|
-
if (tokens.length > 1) {
|
|
9599
|
-
const context = {
|
|
9600
|
-
msg: 4 /* InvalidCombination */,
|
|
9601
|
-
/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
|
|
9602
|
-
first: tokens.item(index > 0 ? 0 : 1),
|
|
9603
|
-
second: value
|
|
9604
|
-
};
|
|
9605
|
-
this.report({
|
|
9606
|
-
node,
|
|
9607
|
-
message: getTerminalMessage(context),
|
|
9608
|
-
location,
|
|
9609
|
-
context
|
|
9610
|
-
});
|
|
9611
|
-
}
|
|
9612
|
-
switch (mantle) {
|
|
9613
|
-
case "expectation":
|
|
9614
|
-
return;
|
|
9615
|
-
case "anchor": {
|
|
9616
|
-
const context = {
|
|
9617
|
-
msg: 1 /* InvalidValue */,
|
|
9618
|
-
type: "hidden",
|
|
9619
|
-
value,
|
|
9620
|
-
what: `<input type="hidden">`
|
|
9621
|
-
};
|
|
9622
|
-
this.report({
|
|
9623
|
-
node,
|
|
9624
|
-
message: getTerminalMessage(context),
|
|
9625
|
-
location: tokens.location(0),
|
|
9626
|
-
context
|
|
9627
|
-
});
|
|
9628
|
-
}
|
|
9629
|
-
}
|
|
9533
|
+
shouldBeOmitted(node) {
|
|
9534
|
+
return this.style === 1 /* AlwaysOmit */ && node.closed === NodeClosed.VoidSelfClosed;
|
|
9630
9535
|
}
|
|
9631
|
-
|
|
9632
|
-
|
|
9633
|
-
|
|
9634
|
-
|
|
9635
|
-
|
|
9636
|
-
|
|
9637
|
-
|
|
9638
|
-
|
|
9639
|
-
|
|
9640
|
-
|
|
9641
|
-
|
|
9642
|
-
|
|
9643
|
-
|
|
9644
|
-
|
|
9645
|
-
|
|
9646
|
-
|
|
9647
|
-
|
|
9648
|
-
|
|
9536
|
+
shouldBeSelfClosed(node) {
|
|
9537
|
+
return this.style === 2 /* AlwaysSelfclose */ && node.closed === NodeClosed.VoidOmitted;
|
|
9538
|
+
}
|
|
9539
|
+
}
|
|
9540
|
+
function parseStyle(name) {
|
|
9541
|
+
switch (name) {
|
|
9542
|
+
case "omit":
|
|
9543
|
+
return 1 /* AlwaysOmit */;
|
|
9544
|
+
case "selfclose":
|
|
9545
|
+
case "selfclosing":
|
|
9546
|
+
return 2 /* AlwaysSelfclose */;
|
|
9547
|
+
default:
|
|
9548
|
+
throw new Error(`Invalid style "${name}" for "void-style" rule`);
|
|
9549
|
+
}
|
|
9550
|
+
}
|
|
9551
|
+
function styleDescription(style) {
|
|
9552
|
+
switch (style) {
|
|
9553
|
+
case 1 /* AlwaysOmit */:
|
|
9554
|
+
return ["omit end tag", ""];
|
|
9555
|
+
case 2 /* AlwaysSelfclose */:
|
|
9556
|
+
return ["be self-closed", "/"];
|
|
9557
|
+
default:
|
|
9558
|
+
throw new Error(`Unknown style`);
|
|
9559
|
+
}
|
|
9560
|
+
}
|
|
9561
|
+
|
|
9562
|
+
class H30 extends Rule {
|
|
9563
|
+
documentation() {
|
|
9564
|
+
return {
|
|
9565
|
+
description: "WCAG 2.1 requires each `<a href>` anchor link to have a text describing the purpose of the link using either plain text or an `<img>` with the `alt` attribute set.",
|
|
9566
|
+
url: "https://html-validate.org/rules/wcag/h30.html"
|
|
9567
|
+
};
|
|
9568
|
+
}
|
|
9569
|
+
setup() {
|
|
9570
|
+
this.on("dom:ready", (event) => {
|
|
9571
|
+
const links = event.document.getElementsByTagName("a");
|
|
9572
|
+
for (const link of links) {
|
|
9573
|
+
if (!link.hasAttribute("href")) {
|
|
9574
|
+
continue;
|
|
9575
|
+
}
|
|
9576
|
+
if (!inAccessibilityTree(link)) {
|
|
9577
|
+
continue;
|
|
9578
|
+
}
|
|
9579
|
+
const textClassification = classifyNodeText(link, { ignoreHiddenRoot: true });
|
|
9580
|
+
if (textClassification !== TextClassification.EMPTY_TEXT) {
|
|
9581
|
+
continue;
|
|
9582
|
+
}
|
|
9583
|
+
const images = link.querySelectorAll("img");
|
|
9584
|
+
if (images.some((image) => hasAltText(image))) {
|
|
9585
|
+
continue;
|
|
9586
|
+
}
|
|
9587
|
+
const labels = link.querySelectorAll("[aria-label]");
|
|
9588
|
+
if (hasAriaLabel(link) || labels.some((cur) => hasAriaLabel(cur))) {
|
|
9589
|
+
continue;
|
|
9590
|
+
}
|
|
9591
|
+
this.report(link, "Anchor link must have a text describing its purpose");
|
|
9649
9592
|
}
|
|
9650
|
-
}
|
|
9651
|
-
const fieldTokens = order.map((it) => it === "field1" || it === "field2");
|
|
9652
|
-
this.validateFieldPresence(node, tokens, fieldTokens, keyLocation);
|
|
9653
|
-
this.validateContact(node, tokens, order);
|
|
9654
|
-
this.validateOrder(node, tokens, order);
|
|
9655
|
-
this.validateControlGroup(node, tokens, fieldTokens);
|
|
9593
|
+
});
|
|
9656
9594
|
}
|
|
9657
|
-
|
|
9658
|
-
|
|
9659
|
-
|
|
9660
|
-
|
|
9661
|
-
|
|
9662
|
-
|
|
9663
|
-
|
|
9664
|
-
|
|
9665
|
-
|
|
9666
|
-
|
|
9667
|
-
|
|
9668
|
-
|
|
9669
|
-
|
|
9670
|
-
|
|
9671
|
-
|
|
9672
|
-
|
|
9673
|
-
|
|
9674
|
-
|
|
9675
|
-
|
|
9676
|
-
|
|
9677
|
-
|
|
9678
|
-
|
|
9679
|
-
|
|
9680
|
-
|
|
9681
|
-
|
|
9682
|
-
this.report({
|
|
9683
|
-
node,
|
|
9684
|
-
message: getTerminalMessage(context),
|
|
9685
|
-
location: tokens.location(b),
|
|
9686
|
-
context
|
|
9687
|
-
});
|
|
9688
|
-
}
|
|
9595
|
+
}
|
|
9596
|
+
|
|
9597
|
+
class H32 extends Rule {
|
|
9598
|
+
documentation() {
|
|
9599
|
+
return {
|
|
9600
|
+
description: "WCAG 2.1 requires each `<form>` element to have at least one submit button.",
|
|
9601
|
+
url: "https://html-validate.org/rules/wcag/h32.html"
|
|
9602
|
+
};
|
|
9603
|
+
}
|
|
9604
|
+
setup() {
|
|
9605
|
+
const formTags = this.getTagsWithProperty("form");
|
|
9606
|
+
const formSelector = formTags.join(",");
|
|
9607
|
+
this.on("dom:ready", (event) => {
|
|
9608
|
+
const { document } = event;
|
|
9609
|
+
const forms = document.querySelectorAll(formSelector);
|
|
9610
|
+
for (const form of forms) {
|
|
9611
|
+
if (hasNestedSubmit(form)) {
|
|
9612
|
+
continue;
|
|
9613
|
+
}
|
|
9614
|
+
if (hasAssociatedSubmit(document, form)) {
|
|
9615
|
+
continue;
|
|
9616
|
+
}
|
|
9617
|
+
this.report(form, `<${form.tagName}> element must have a submit button`);
|
|
9618
|
+
}
|
|
9619
|
+
});
|
|
9689
9620
|
}
|
|
9690
|
-
|
|
9691
|
-
|
|
9692
|
-
|
|
9693
|
-
|
|
9694
|
-
|
|
9695
|
-
|
|
9696
|
-
|
|
9697
|
-
|
|
9698
|
-
|
|
9699
|
-
|
|
9700
|
-
|
|
9701
|
-
|
|
9702
|
-
|
|
9703
|
-
|
|
9704
|
-
|
|
9705
|
-
|
|
9706
|
-
|
|
9707
|
-
location: tokens.location(b),
|
|
9708
|
-
context
|
|
9709
|
-
});
|
|
9710
|
-
}
|
|
9621
|
+
}
|
|
9622
|
+
function isSubmit(node) {
|
|
9623
|
+
const type = node.getAttribute("type");
|
|
9624
|
+
return Boolean(!type || type.valueMatches(/submit|image/));
|
|
9625
|
+
}
|
|
9626
|
+
function isAssociated(id, node) {
|
|
9627
|
+
const form = node.getAttribute("form");
|
|
9628
|
+
return Boolean(form == null ? void 0 : form.valueMatches(id, true));
|
|
9629
|
+
}
|
|
9630
|
+
function hasNestedSubmit(form) {
|
|
9631
|
+
const matches = form.querySelectorAll("button,input").filter(isSubmit).filter((node) => !node.hasAttribute("form"));
|
|
9632
|
+
return matches.length > 0;
|
|
9633
|
+
}
|
|
9634
|
+
function hasAssociatedSubmit(document, form) {
|
|
9635
|
+
const { id } = form;
|
|
9636
|
+
if (!id) {
|
|
9637
|
+
return false;
|
|
9711
9638
|
}
|
|
9712
|
-
|
|
9713
|
-
|
|
9714
|
-
|
|
9715
|
-
|
|
9716
|
-
|
|
9717
|
-
|
|
9718
|
-
|
|
9719
|
-
|
|
9720
|
-
|
|
9721
|
-
|
|
9722
|
-
|
|
9639
|
+
const matches = document.querySelectorAll("button[form],input[form]").filter(isSubmit).filter((node) => isAssociated(id, node));
|
|
9640
|
+
return matches.length > 0;
|
|
9641
|
+
}
|
|
9642
|
+
|
|
9643
|
+
class H36 extends Rule {
|
|
9644
|
+
documentation() {
|
|
9645
|
+
return {
|
|
9646
|
+
description: [
|
|
9647
|
+
"WCAG 2.1 requires all images used as submit buttons to have a non-empty textual description using the `alt` attribute.",
|
|
9648
|
+
'The alt text cannot be empty (`alt=""`).'
|
|
9649
|
+
].join("\n"),
|
|
9650
|
+
url: "https://html-validate.org/rules/wcag/h36.html"
|
|
9651
|
+
};
|
|
9652
|
+
}
|
|
9653
|
+
setup() {
|
|
9654
|
+
this.on("tag:end", (event) => {
|
|
9655
|
+
const node = event.previous;
|
|
9656
|
+
if (node.tagName !== "input")
|
|
9657
|
+
return;
|
|
9658
|
+
if (node.getAttributeValue("type") !== "image") {
|
|
9659
|
+
return;
|
|
9660
|
+
}
|
|
9661
|
+
if (!inAccessibilityTree(node)) {
|
|
9662
|
+
return;
|
|
9663
|
+
}
|
|
9664
|
+
if (!hasAltText(node)) {
|
|
9665
|
+
const message = "image used as submit button must have non-empty alt text";
|
|
9666
|
+
const alt = node.getAttribute("alt");
|
|
9723
9667
|
this.report({
|
|
9724
9668
|
node,
|
|
9725
|
-
message
|
|
9726
|
-
location:
|
|
9727
|
-
context
|
|
9669
|
+
message,
|
|
9670
|
+
location: alt ? alt.keyLocation : node.location
|
|
9728
9671
|
});
|
|
9729
9672
|
}
|
|
9730
|
-
}
|
|
9731
|
-
}
|
|
9732
|
-
validateControlGroup(node, tokens, fieldTokens) {
|
|
9733
|
-
const numFields = fieldTokens.filter(Boolean).length;
|
|
9734
|
-
if (numFields === 0) {
|
|
9735
|
-
return;
|
|
9736
|
-
}
|
|
9737
|
-
if (!node.is("input")) {
|
|
9738
|
-
return;
|
|
9739
|
-
}
|
|
9740
|
-
const attr = node.getAttribute("type");
|
|
9741
|
-
const type = (attr == null ? void 0 : attr.value) ?? "text";
|
|
9742
|
-
if (type instanceof DynamicValue) {
|
|
9743
|
-
return;
|
|
9744
|
-
}
|
|
9745
|
-
const controlGroups = getControlGroups(type);
|
|
9746
|
-
const fieldIndex = fieldTokens.indexOf(true);
|
|
9747
|
-
const fieldToken = tokens.item(fieldIndex);
|
|
9748
|
-
const fieldGroup = fieldNameGroup[fieldToken];
|
|
9749
|
-
if (!controlGroups.includes(fieldGroup)) {
|
|
9750
|
-
const context = {
|
|
9751
|
-
msg: 1 /* InvalidValue */,
|
|
9752
|
-
type,
|
|
9753
|
-
value: fieldToken,
|
|
9754
|
-
what: `<input type="${type}">`
|
|
9755
|
-
};
|
|
9756
|
-
this.report({
|
|
9757
|
-
node,
|
|
9758
|
-
message: getTerminalMessage(context),
|
|
9759
|
-
location: tokens.location(fieldIndex),
|
|
9760
|
-
context
|
|
9761
|
-
});
|
|
9762
|
-
}
|
|
9673
|
+
});
|
|
9763
9674
|
}
|
|
9764
9675
|
}
|
|
9765
9676
|
|
|
9766
|
-
const defaults$
|
|
9767
|
-
|
|
9677
|
+
const defaults$1 = {
|
|
9678
|
+
allowEmpty: true,
|
|
9679
|
+
alias: []
|
|
9768
9680
|
};
|
|
9769
|
-
class
|
|
9681
|
+
class H37 extends Rule {
|
|
9770
9682
|
constructor(options) {
|
|
9771
|
-
super({ ...defaults$
|
|
9683
|
+
super({ ...defaults$1, ...options });
|
|
9684
|
+
if (!Array.isArray(this.options.alias)) {
|
|
9685
|
+
this.options.alias = [this.options.alias];
|
|
9686
|
+
}
|
|
9772
9687
|
}
|
|
9773
9688
|
static schema() {
|
|
9774
9689
|
return {
|
|
9775
|
-
|
|
9690
|
+
alias: {
|
|
9691
|
+
anyOf: [
|
|
9692
|
+
{
|
|
9693
|
+
items: {
|
|
9694
|
+
type: "string"
|
|
9695
|
+
},
|
|
9696
|
+
type: "array"
|
|
9697
|
+
},
|
|
9698
|
+
{
|
|
9699
|
+
type: "string"
|
|
9700
|
+
}
|
|
9701
|
+
]
|
|
9702
|
+
},
|
|
9703
|
+
allowEmpty: {
|
|
9776
9704
|
type: "boolean"
|
|
9777
9705
|
}
|
|
9778
9706
|
};
|
|
9779
9707
|
}
|
|
9780
|
-
documentation(
|
|
9781
|
-
const { relaxed } = this.options;
|
|
9782
|
-
const message = this.messages[context].replace("id", "ID").replace(/^(.)/, (m) => m.toUpperCase());
|
|
9783
|
-
const relaxedDescription = relaxed ? [] : [
|
|
9784
|
-
" - ID must begin with a letter",
|
|
9785
|
-
" - ID must only contain letters, digits, `-` and `_`"
|
|
9786
|
-
];
|
|
9708
|
+
documentation() {
|
|
9787
9709
|
return {
|
|
9788
|
-
description:
|
|
9789
|
-
|
|
9790
|
-
"",
|
|
9791
|
-
"Under the current configuration the following rules are applied:",
|
|
9792
|
-
"",
|
|
9793
|
-
" - ID must not be empty",
|
|
9794
|
-
" - ID must not contain any whitespace characters",
|
|
9795
|
-
...relaxedDescription
|
|
9796
|
-
].join("\n"),
|
|
9797
|
-
url: "https://html-validate.org/rules/valid-id.html"
|
|
9710
|
+
description: "Both HTML5 and WCAG 2.0 requires images to have a alternative text for each image.",
|
|
9711
|
+
url: "https://html-validate.org/rules/wcag/h37.html"
|
|
9798
9712
|
};
|
|
9799
9713
|
}
|
|
9800
9714
|
setup() {
|
|
9801
|
-
this.on("
|
|
9802
|
-
const {
|
|
9803
|
-
|
|
9804
|
-
|
|
9715
|
+
this.on("dom:ready", (event) => {
|
|
9716
|
+
const { document } = event;
|
|
9717
|
+
const nodes = document.querySelectorAll("img");
|
|
9718
|
+
for (const node of nodes) {
|
|
9719
|
+
this.validateNode(node);
|
|
9805
9720
|
}
|
|
9806
|
-
|
|
9807
|
-
|
|
9808
|
-
|
|
9721
|
+
});
|
|
9722
|
+
}
|
|
9723
|
+
validateNode(node) {
|
|
9724
|
+
if (!inAccessibilityTree(node)) {
|
|
9725
|
+
return;
|
|
9726
|
+
}
|
|
9727
|
+
if (Boolean(node.getAttributeValue("alt")) || Boolean(node.hasAttribute("alt") && this.options.allowEmpty)) {
|
|
9728
|
+
return;
|
|
9729
|
+
}
|
|
9730
|
+
for (const attr of this.options.alias) {
|
|
9731
|
+
if (node.getAttribute(attr)) {
|
|
9809
9732
|
return;
|
|
9810
9733
|
}
|
|
9811
|
-
|
|
9812
|
-
|
|
9813
|
-
|
|
9734
|
+
}
|
|
9735
|
+
const tag = node.annotatedName;
|
|
9736
|
+
if (node.hasAttribute("alt")) {
|
|
9737
|
+
const attr = node.getAttribute("alt");
|
|
9738
|
+
this.report(node, `${tag} cannot have empty "alt" attribute`, attr.keyLocation);
|
|
9739
|
+
} else {
|
|
9740
|
+
this.report(node, `${tag} is missing required "alt" attribute`, node.location);
|
|
9741
|
+
}
|
|
9742
|
+
}
|
|
9743
|
+
}
|
|
9744
|
+
|
|
9745
|
+
var _a;
|
|
9746
|
+
const { enum: validScopes } = (_a = elements.html5.th.attributes) == null ? void 0 : _a.scope;
|
|
9747
|
+
const joinedScopes = utils_naturalJoin.naturalJoin(validScopes);
|
|
9748
|
+
class H63 extends Rule {
|
|
9749
|
+
documentation() {
|
|
9750
|
+
return {
|
|
9751
|
+
description: "H63: Using the scope attribute to associate header cells and data cells in data tables",
|
|
9752
|
+
url: "https://html-validate.org/rules/wcag/h63.html"
|
|
9753
|
+
};
|
|
9754
|
+
}
|
|
9755
|
+
setup() {
|
|
9756
|
+
this.on("tag:ready", (event) => {
|
|
9757
|
+
const node = event.target;
|
|
9758
|
+
if (node.tagName !== "th") {
|
|
9814
9759
|
return;
|
|
9815
9760
|
}
|
|
9816
|
-
const
|
|
9817
|
-
|
|
9761
|
+
const scope = node.getAttribute("scope");
|
|
9762
|
+
const value = scope == null ? void 0 : scope.value;
|
|
9763
|
+
if (value instanceof DynamicValue) {
|
|
9818
9764
|
return;
|
|
9819
9765
|
}
|
|
9820
|
-
if (value.
|
|
9821
|
-
const context = 3 /* LEADING_CHARACTER */;
|
|
9822
|
-
this.report(event.target, this.messages[context], event.valueLocation, context);
|
|
9766
|
+
if (value && validScopes.includes(value)) {
|
|
9823
9767
|
return;
|
|
9824
9768
|
}
|
|
9825
|
-
|
|
9826
|
-
|
|
9827
|
-
|
|
9828
|
-
}
|
|
9769
|
+
const message = `<th> element must have a valid scope attribute: ${joinedScopes}`;
|
|
9770
|
+
const location = (scope == null ? void 0 : scope.valueLocation) ?? (scope == null ? void 0 : scope.keyLocation) ?? node.location;
|
|
9771
|
+
this.report(node, message, location);
|
|
9829
9772
|
});
|
|
9830
9773
|
}
|
|
9831
|
-
|
|
9832
|
-
|
|
9833
|
-
|
|
9834
|
-
|
|
9835
|
-
|
|
9836
|
-
|
|
9837
|
-
|
|
9838
|
-
}
|
|
9839
|
-
isRelevant(event) {
|
|
9840
|
-
return event.key === "id";
|
|
9841
|
-
}
|
|
9842
|
-
}
|
|
9843
|
-
|
|
9844
|
-
class VoidContent extends Rule {
|
|
9845
|
-
documentation(tagName) {
|
|
9846
|
-
const doc = {
|
|
9847
|
-
description: "HTML void elements cannot have any content and must not have content or end tag.",
|
|
9848
|
-
url: "https://html-validate.org/rules/void-content.html"
|
|
9774
|
+
}
|
|
9775
|
+
|
|
9776
|
+
class H67 extends Rule {
|
|
9777
|
+
documentation() {
|
|
9778
|
+
return {
|
|
9779
|
+
description: "A decorative image cannot have a title attribute. Either remove `title` or add a descriptive `alt` text.",
|
|
9780
|
+
url: "https://html-validate.org/rules/wcag/h67.html"
|
|
9849
9781
|
};
|
|
9850
|
-
if (tagName) {
|
|
9851
|
-
doc.description = `<${tagName}> is a void element and must not have content or end tag.`;
|
|
9852
|
-
}
|
|
9853
|
-
return doc;
|
|
9854
9782
|
}
|
|
9855
9783
|
setup() {
|
|
9856
9784
|
this.on("tag:end", (event) => {
|
|
9857
9785
|
const node = event.target;
|
|
9858
|
-
if (!node) {
|
|
9786
|
+
if (!node || node.tagName !== "img") {
|
|
9859
9787
|
return;
|
|
9860
9788
|
}
|
|
9861
|
-
|
|
9789
|
+
const title = node.getAttribute("title");
|
|
9790
|
+
if (!title || title.value === "") {
|
|
9862
9791
|
return;
|
|
9863
9792
|
}
|
|
9864
|
-
|
|
9865
|
-
|
|
9866
|
-
|
|
9867
|
-
`End tag for <${node.tagName}> must be omitted`,
|
|
9868
|
-
node.location,
|
|
9869
|
-
node.tagName
|
|
9870
|
-
);
|
|
9793
|
+
const alt = node.getAttributeValue("alt");
|
|
9794
|
+
if (alt && alt !== "") {
|
|
9795
|
+
return;
|
|
9871
9796
|
}
|
|
9797
|
+
this.report(node, "<img> with empty alt text cannot have title attribute", title.keyLocation);
|
|
9872
9798
|
});
|
|
9873
9799
|
}
|
|
9874
9800
|
}
|
|
9875
9801
|
|
|
9876
|
-
|
|
9877
|
-
|
|
9878
|
-
};
|
|
9879
|
-
class VoidStyle extends Rule {
|
|
9880
|
-
constructor(options) {
|
|
9881
|
-
super({ ...defaults$2, ...options });
|
|
9882
|
-
this.style = parseStyle(this.options.style);
|
|
9883
|
-
}
|
|
9884
|
-
static schema() {
|
|
9885
|
-
return {
|
|
9886
|
-
style: {
|
|
9887
|
-
enum: ["omit", "selfclose", "selfclosing"],
|
|
9888
|
-
type: "string"
|
|
9889
|
-
}
|
|
9890
|
-
};
|
|
9891
|
-
}
|
|
9892
|
-
documentation(context) {
|
|
9893
|
-
const [desc, end] = styleDescription(context.style);
|
|
9802
|
+
class H71 extends Rule {
|
|
9803
|
+
documentation() {
|
|
9894
9804
|
return {
|
|
9895
|
-
description:
|
|
9896
|
-
url: "https://html-validate.org/rules/
|
|
9805
|
+
description: "H71: Providing a description for groups of form controls using fieldset and legend elements",
|
|
9806
|
+
url: "https://html-validate.org/rules/wcag/h71.html"
|
|
9897
9807
|
};
|
|
9898
9808
|
}
|
|
9899
9809
|
setup() {
|
|
9900
|
-
this.on("
|
|
9901
|
-
const
|
|
9902
|
-
|
|
9903
|
-
|
|
9810
|
+
this.on("dom:ready", (event) => {
|
|
9811
|
+
const { document } = event;
|
|
9812
|
+
const fieldsets = document.querySelectorAll(this.selector);
|
|
9813
|
+
for (const fieldset of fieldsets) {
|
|
9814
|
+
this.validate(fieldset);
|
|
9904
9815
|
}
|
|
9905
9816
|
});
|
|
9906
9817
|
}
|
|
9907
|
-
|
|
9908
|
-
|
|
9909
|
-
|
|
9910
|
-
|
|
9911
|
-
if (this.shouldBeOmitted(node)) {
|
|
9912
|
-
this.reportError(
|
|
9913
|
-
node,
|
|
9914
|
-
`Expected omitted end tag <${node.tagName}> instead of self-closing element <${node.tagName}/>`
|
|
9915
|
-
);
|
|
9916
|
-
}
|
|
9917
|
-
if (this.shouldBeSelfClosed(node)) {
|
|
9918
|
-
this.reportError(
|
|
9919
|
-
node,
|
|
9920
|
-
`Expected self-closing element <${node.tagName}/> instead of omitted end-tag <${node.tagName}>`
|
|
9921
|
-
);
|
|
9818
|
+
validate(fieldset) {
|
|
9819
|
+
const legend = fieldset.querySelectorAll("> legend");
|
|
9820
|
+
if (legend.length === 0) {
|
|
9821
|
+
this.reportNode(fieldset);
|
|
9922
9822
|
}
|
|
9923
9823
|
}
|
|
9924
|
-
|
|
9925
|
-
|
|
9926
|
-
style: this.style,
|
|
9927
|
-
tagName: node.tagName
|
|
9928
|
-
};
|
|
9929
|
-
super.report(node, message, null, context);
|
|
9930
|
-
}
|
|
9931
|
-
shouldBeOmitted(node) {
|
|
9932
|
-
return this.style === 1 /* AlwaysOmit */ && node.closed === NodeClosed.VoidSelfClosed;
|
|
9824
|
+
reportNode(node) {
|
|
9825
|
+
super.report(node, `${node.annotatedName} must have a <legend> as the first child`);
|
|
9933
9826
|
}
|
|
9934
|
-
|
|
9935
|
-
return this.
|
|
9827
|
+
get selector() {
|
|
9828
|
+
return this.getTagsDerivedFrom("fieldset").join(",");
|
|
9936
9829
|
}
|
|
9937
9830
|
}
|
|
9938
|
-
|
|
9939
|
-
|
|
9940
|
-
|
|
9941
|
-
|
|
9942
|
-
|
|
9943
|
-
|
|
9944
|
-
|
|
9945
|
-
|
|
9946
|
-
|
|
9947
|
-
|
|
9831
|
+
|
|
9832
|
+
const bundledRules$1 = {
|
|
9833
|
+
"wcag/h30": H30,
|
|
9834
|
+
"wcag/h32": H32,
|
|
9835
|
+
"wcag/h36": H36,
|
|
9836
|
+
"wcag/h37": H37,
|
|
9837
|
+
"wcag/h63": H63,
|
|
9838
|
+
"wcag/h67": H67,
|
|
9839
|
+
"wcag/h71": H71
|
|
9840
|
+
};
|
|
9841
|
+
|
|
9842
|
+
const bundledRules = {
|
|
9843
|
+
"allowed-links": AllowedLinks,
|
|
9844
|
+
"area-alt": AreaAlt,
|
|
9845
|
+
"aria-hidden-body": AriaHiddenBody,
|
|
9846
|
+
"aria-label-misuse": AriaLabelMisuse,
|
|
9847
|
+
"attr-case": AttrCase,
|
|
9848
|
+
"attr-delimiter": AttrDelimiter,
|
|
9849
|
+
"attr-pattern": AttrPattern,
|
|
9850
|
+
"attr-quotes": AttrQuotes,
|
|
9851
|
+
"attr-spacing": AttrSpacing,
|
|
9852
|
+
"attribute-allowed-values": AttributeAllowedValues,
|
|
9853
|
+
"attribute-boolean-style": AttributeBooleanStyle,
|
|
9854
|
+
"attribute-empty-style": AttributeEmptyStyle,
|
|
9855
|
+
"attribute-misuse": AttributeMisuse,
|
|
9856
|
+
"class-pattern": ClassPattern,
|
|
9857
|
+
"close-attr": CloseAttr,
|
|
9858
|
+
"close-order": CloseOrder,
|
|
9859
|
+
deprecated: Deprecated,
|
|
9860
|
+
"deprecated-rule": DeprecatedRule,
|
|
9861
|
+
"doctype-html": NoStyleTag$1,
|
|
9862
|
+
"doctype-style": DoctypeStyle,
|
|
9863
|
+
"element-case": ElementCase,
|
|
9864
|
+
"element-name": ElementName,
|
|
9865
|
+
"element-permitted-content": ElementPermittedContent,
|
|
9866
|
+
"element-permitted-occurrences": ElementPermittedOccurrences,
|
|
9867
|
+
"element-permitted-order": ElementPermittedOrder,
|
|
9868
|
+
"element-permitted-parent": ElementPermittedParent,
|
|
9869
|
+
"element-required-ancestor": ElementRequiredAncestor,
|
|
9870
|
+
"element-required-attributes": ElementRequiredAttributes,
|
|
9871
|
+
"element-required-content": ElementRequiredContent,
|
|
9872
|
+
"empty-heading": EmptyHeading,
|
|
9873
|
+
"empty-title": EmptyTitle,
|
|
9874
|
+
"form-dup-name": FormDupName,
|
|
9875
|
+
"heading-level": HeadingLevel,
|
|
9876
|
+
"hidden-focusable": HiddenFocusable,
|
|
9877
|
+
"id-pattern": IdPattern,
|
|
9878
|
+
"input-attributes": InputAttributes,
|
|
9879
|
+
"input-missing-label": InputMissingLabel,
|
|
9880
|
+
"long-title": LongTitle,
|
|
9881
|
+
"map-dup-name": MapDupName,
|
|
9882
|
+
"map-id-name": MapIdName,
|
|
9883
|
+
"meta-refresh": MetaRefresh,
|
|
9884
|
+
"missing-doctype": MissingDoctype,
|
|
9885
|
+
"multiple-labeled-controls": MultipleLabeledControls,
|
|
9886
|
+
"name-pattern": NamePattern,
|
|
9887
|
+
"no-abstract-role": NoAbstractRole,
|
|
9888
|
+
"no-autoplay": NoAutoplay,
|
|
9889
|
+
"no-conditional-comment": NoConditionalComment,
|
|
9890
|
+
"no-deprecated-attr": NoDeprecatedAttr,
|
|
9891
|
+
"no-dup-attr": NoDupAttr,
|
|
9892
|
+
"no-dup-class": NoDupClass,
|
|
9893
|
+
"no-dup-id": NoDupID,
|
|
9894
|
+
"no-implicit-button-type": NoImplicitButtonType,
|
|
9895
|
+
"no-implicit-input-type": NoImplicitInputType,
|
|
9896
|
+
"no-implicit-close": NoImplicitClose,
|
|
9897
|
+
"no-inline-style": NoInlineStyle,
|
|
9898
|
+
"no-missing-references": NoMissingReferences,
|
|
9899
|
+
"no-multiple-main": NoMultipleMain,
|
|
9900
|
+
"no-raw-characters": NoRawCharacters,
|
|
9901
|
+
"no-redundant-aria-label": NoRedundantAriaLabel,
|
|
9902
|
+
"no-redundant-for": NoRedundantFor,
|
|
9903
|
+
"no-redundant-role": NoRedundantRole,
|
|
9904
|
+
"no-self-closing": NoSelfClosing,
|
|
9905
|
+
"no-style-tag": NoStyleTag,
|
|
9906
|
+
"no-trailing-whitespace": NoTrailingWhitespace,
|
|
9907
|
+
"no-unknown-elements": NoUnknownElements,
|
|
9908
|
+
"no-unused-disable": NoUnusedDisable,
|
|
9909
|
+
"no-utf8-bom": NoUtf8Bom,
|
|
9910
|
+
"prefer-button": PreferButton,
|
|
9911
|
+
"prefer-native-element": PreferNativeElement,
|
|
9912
|
+
"prefer-tbody": PreferTbody,
|
|
9913
|
+
"require-csp-nonce": RequireCSPNonce,
|
|
9914
|
+
"require-sri": RequireSri,
|
|
9915
|
+
"script-element": ScriptElement,
|
|
9916
|
+
"script-type": ScriptType,
|
|
9917
|
+
"svg-focusable": SvgFocusable,
|
|
9918
|
+
"tel-non-breaking": TelNonBreaking,
|
|
9919
|
+
"text-content": TextContent,
|
|
9920
|
+
"unique-landmark": UniqueLandmark,
|
|
9921
|
+
"unrecognized-char-ref": UnknownCharReference,
|
|
9922
|
+
"valid-autocomplete": ValidAutocomplete,
|
|
9923
|
+
"valid-id": ValidID,
|
|
9924
|
+
"void-content": VoidContent,
|
|
9925
|
+
"void-style": VoidStyle,
|
|
9926
|
+
...bundledRules$1
|
|
9927
|
+
};
|
|
9928
|
+
|
|
9929
|
+
const ruleIds = new Set(Object.keys(bundledRules));
|
|
9930
|
+
function ruleExists(ruleId) {
|
|
9931
|
+
return ruleIds.has(ruleId);
|
|
9948
9932
|
}
|
|
9949
|
-
|
|
9950
|
-
|
|
9951
|
-
|
|
9952
|
-
|
|
9953
|
-
|
|
9954
|
-
|
|
9955
|
-
|
|
9956
|
-
|
|
9933
|
+
|
|
9934
|
+
function depthFirst(root, callback) {
|
|
9935
|
+
if (root instanceof DOMTree) {
|
|
9936
|
+
if (root.readyState !== "complete") {
|
|
9937
|
+
throw new Error(`Cannot call walk.depthFirst(..) before document is ready`);
|
|
9938
|
+
}
|
|
9939
|
+
root = root.root;
|
|
9940
|
+
}
|
|
9941
|
+
function visit(node) {
|
|
9942
|
+
node.childElements.forEach(visit);
|
|
9943
|
+
if (!node.isRootElement()) {
|
|
9944
|
+
callback(node);
|
|
9945
|
+
}
|
|
9957
9946
|
}
|
|
9947
|
+
visit(root);
|
|
9958
9948
|
}
|
|
9949
|
+
const walk = {
|
|
9950
|
+
depthFirst
|
|
9951
|
+
};
|
|
9959
9952
|
|
|
9960
|
-
class
|
|
9961
|
-
|
|
9962
|
-
|
|
9963
|
-
|
|
9964
|
-
|
|
9965
|
-
|
|
9953
|
+
class DOMTree {
|
|
9954
|
+
/**
|
|
9955
|
+
* @internal
|
|
9956
|
+
*/
|
|
9957
|
+
constructor(location) {
|
|
9958
|
+
this.root = HtmlElement.rootNode(location);
|
|
9959
|
+
this.active = this.root;
|
|
9960
|
+
this.doctype = null;
|
|
9961
|
+
this._readyState = "loading";
|
|
9962
|
+
}
|
|
9963
|
+
/**
|
|
9964
|
+
* @internal
|
|
9965
|
+
*/
|
|
9966
|
+
pushActive(node) {
|
|
9967
|
+
this.active = node;
|
|
9968
|
+
}
|
|
9969
|
+
/**
|
|
9970
|
+
* @internal
|
|
9971
|
+
*/
|
|
9972
|
+
popActive() {
|
|
9973
|
+
if (this.active.isRootElement()) {
|
|
9974
|
+
return;
|
|
9975
|
+
}
|
|
9976
|
+
this.active = this.active.parent ?? this.root;
|
|
9977
|
+
}
|
|
9978
|
+
/**
|
|
9979
|
+
* @internal
|
|
9980
|
+
*/
|
|
9981
|
+
getActive() {
|
|
9982
|
+
return this.active;
|
|
9983
|
+
}
|
|
9984
|
+
/**
|
|
9985
|
+
* Describes the loading state of the document.
|
|
9986
|
+
*
|
|
9987
|
+
* When `"loading"` it is still not safe to use functions such as
|
|
9988
|
+
* `querySelector` or presence of attributes, child nodes, etc.
|
|
9989
|
+
*/
|
|
9990
|
+
get readyState() {
|
|
9991
|
+
return this._readyState;
|
|
9966
9992
|
}
|
|
9967
|
-
|
|
9968
|
-
|
|
9969
|
-
|
|
9970
|
-
|
|
9971
|
-
|
|
9972
|
-
|
|
9973
|
-
|
|
9974
|
-
|
|
9975
|
-
|
|
9976
|
-
}
|
|
9977
|
-
const textClassification = classifyNodeText(link, { ignoreHiddenRoot: true });
|
|
9978
|
-
if (textClassification !== TextClassification.EMPTY_TEXT) {
|
|
9979
|
-
continue;
|
|
9980
|
-
}
|
|
9981
|
-
const images = link.querySelectorAll("img");
|
|
9982
|
-
if (images.some((image) => hasAltText(image))) {
|
|
9983
|
-
continue;
|
|
9984
|
-
}
|
|
9985
|
-
const labels = link.querySelectorAll("[aria-label]");
|
|
9986
|
-
if (hasAriaLabel(link) || labels.some((cur) => hasAriaLabel(cur))) {
|
|
9987
|
-
continue;
|
|
9988
|
-
}
|
|
9989
|
-
this.report(link, "Anchor link must have a text describing its purpose");
|
|
9990
|
-
}
|
|
9993
|
+
/**
|
|
9994
|
+
* Resolve dynamic meta expressions.
|
|
9995
|
+
*
|
|
9996
|
+
* @internal
|
|
9997
|
+
*/
|
|
9998
|
+
resolveMeta(table) {
|
|
9999
|
+
this._readyState = "complete";
|
|
10000
|
+
walk.depthFirst(this, (node) => {
|
|
10001
|
+
table.resolve(node);
|
|
9991
10002
|
});
|
|
9992
10003
|
}
|
|
9993
|
-
|
|
9994
|
-
|
|
9995
|
-
class H32 extends Rule {
|
|
9996
|
-
documentation() {
|
|
9997
|
-
return {
|
|
9998
|
-
description: "WCAG 2.1 requires each `<form>` element to have at least one submit button.",
|
|
9999
|
-
url: "https://html-validate.org/rules/wcag/h32.html"
|
|
10000
|
-
};
|
|
10004
|
+
getElementsByTagName(tagName) {
|
|
10005
|
+
return this.root.getElementsByTagName(tagName);
|
|
10001
10006
|
}
|
|
10002
|
-
|
|
10003
|
-
|
|
10004
|
-
|
|
10005
|
-
|
|
10006
|
-
|
|
10007
|
-
const forms = document.querySelectorAll(formSelector);
|
|
10008
|
-
for (const form of forms) {
|
|
10009
|
-
if (hasNestedSubmit(form)) {
|
|
10010
|
-
continue;
|
|
10011
|
-
}
|
|
10012
|
-
if (hasAssociatedSubmit(document, form)) {
|
|
10013
|
-
continue;
|
|
10014
|
-
}
|
|
10015
|
-
this.report(form, `<${form.tagName}> element must have a submit button`);
|
|
10016
|
-
}
|
|
10017
|
-
});
|
|
10007
|
+
/**
|
|
10008
|
+
* @deprecated use utility function `walk.depthFirst(..)` instead (since 8.21.0).
|
|
10009
|
+
*/
|
|
10010
|
+
visitDepthFirst(callback) {
|
|
10011
|
+
walk.depthFirst(this, callback);
|
|
10018
10012
|
}
|
|
10019
|
-
|
|
10020
|
-
|
|
10021
|
-
|
|
10022
|
-
|
|
10023
|
-
|
|
10024
|
-
function isAssociated(id, node) {
|
|
10025
|
-
const form = node.getAttribute("form");
|
|
10026
|
-
return Boolean(form == null ? void 0 : form.valueMatches(id, true));
|
|
10027
|
-
}
|
|
10028
|
-
function hasNestedSubmit(form) {
|
|
10029
|
-
const matches = form.querySelectorAll("button,input").filter(isSubmit).filter((node) => !node.hasAttribute("form"));
|
|
10030
|
-
return matches.length > 0;
|
|
10031
|
-
}
|
|
10032
|
-
function hasAssociatedSubmit(document, form) {
|
|
10033
|
-
const { id } = form;
|
|
10034
|
-
if (!id) {
|
|
10035
|
-
return false;
|
|
10013
|
+
/**
|
|
10014
|
+
* @deprecated use `querySelector(..)` instead (since 8.21.0)
|
|
10015
|
+
*/
|
|
10016
|
+
find(callback) {
|
|
10017
|
+
return this.root.find(callback);
|
|
10036
10018
|
}
|
|
10037
|
-
|
|
10038
|
-
|
|
10039
|
-
}
|
|
10040
|
-
|
|
10041
|
-
class H36 extends Rule {
|
|
10042
|
-
documentation() {
|
|
10043
|
-
return {
|
|
10044
|
-
description: [
|
|
10045
|
-
"WCAG 2.1 requires all images used as submit buttons to have a non-empty textual description using the `alt` attribute.",
|
|
10046
|
-
'The alt text cannot be empty (`alt=""`).'
|
|
10047
|
-
].join("\n"),
|
|
10048
|
-
url: "https://html-validate.org/rules/wcag/h36.html"
|
|
10049
|
-
};
|
|
10019
|
+
querySelector(selector) {
|
|
10020
|
+
return this.root.querySelector(selector);
|
|
10050
10021
|
}
|
|
10051
|
-
|
|
10052
|
-
this.
|
|
10053
|
-
const node = event.previous;
|
|
10054
|
-
if (node.tagName !== "input")
|
|
10055
|
-
return;
|
|
10056
|
-
if (node.getAttributeValue("type") !== "image") {
|
|
10057
|
-
return;
|
|
10058
|
-
}
|
|
10059
|
-
if (!inAccessibilityTree(node)) {
|
|
10060
|
-
return;
|
|
10061
|
-
}
|
|
10062
|
-
if (!hasAltText(node)) {
|
|
10063
|
-
const message = "image used as submit button must have non-empty alt text";
|
|
10064
|
-
const alt = node.getAttribute("alt");
|
|
10065
|
-
this.report({
|
|
10066
|
-
node,
|
|
10067
|
-
message,
|
|
10068
|
-
location: alt ? alt.keyLocation : node.location
|
|
10069
|
-
});
|
|
10070
|
-
}
|
|
10071
|
-
});
|
|
10022
|
+
querySelectorAll(selector) {
|
|
10023
|
+
return this.root.querySelectorAll(selector);
|
|
10072
10024
|
}
|
|
10073
10025
|
}
|
|
10074
10026
|
|
|
10075
|
-
const
|
|
10076
|
-
|
|
10077
|
-
|
|
10078
|
-
|
|
10079
|
-
|
|
10080
|
-
|
|
10081
|
-
|
|
10082
|
-
|
|
10083
|
-
|
|
10027
|
+
const allowedKeys = ["exclude"];
|
|
10028
|
+
class Validator {
|
|
10029
|
+
/**
|
|
10030
|
+
* Test if element is used in a proper context.
|
|
10031
|
+
*
|
|
10032
|
+
* @param node - Element to test.
|
|
10033
|
+
* @param rules - List of rules.
|
|
10034
|
+
* @returns `true` if element passes all tests.
|
|
10035
|
+
*/
|
|
10036
|
+
static validatePermitted(node, rules) {
|
|
10037
|
+
if (!rules) {
|
|
10038
|
+
return true;
|
|
10084
10039
|
}
|
|
10040
|
+
return rules.some((rule) => {
|
|
10041
|
+
return Validator.validatePermittedRule(node, rule);
|
|
10042
|
+
});
|
|
10085
10043
|
}
|
|
10086
|
-
|
|
10087
|
-
|
|
10088
|
-
|
|
10089
|
-
|
|
10090
|
-
|
|
10091
|
-
|
|
10092
|
-
|
|
10093
|
-
|
|
10094
|
-
|
|
10095
|
-
|
|
10096
|
-
|
|
10097
|
-
|
|
10044
|
+
/**
|
|
10045
|
+
* Test if an element is used the correct amount of times.
|
|
10046
|
+
*
|
|
10047
|
+
* For instance, a `<table>` element can only contain a single `<tbody>`
|
|
10048
|
+
* child. If multiple `<tbody>` exists this test will fail both nodes.
|
|
10049
|
+
* Note that this is called on the parent but will fail the children violating
|
|
10050
|
+
* the rule.
|
|
10051
|
+
*
|
|
10052
|
+
* @param children - Array of children to validate.
|
|
10053
|
+
* @param rules - List of rules of the parent element.
|
|
10054
|
+
* @returns `true` if the parent element of the children passes the test.
|
|
10055
|
+
*/
|
|
10056
|
+
static validateOccurrences(children, rules, cb) {
|
|
10057
|
+
if (!rules) {
|
|
10058
|
+
return true;
|
|
10059
|
+
}
|
|
10060
|
+
let valid = true;
|
|
10061
|
+
for (const rule of rules) {
|
|
10062
|
+
if (typeof rule !== "string") {
|
|
10063
|
+
return false;
|
|
10064
|
+
}
|
|
10065
|
+
const [, category, quantifier] = rule.match(/^(@?.*?)([?*]?)$/);
|
|
10066
|
+
const limit = category && quantifier && parseQuantifier(quantifier);
|
|
10067
|
+
if (limit) {
|
|
10068
|
+
const siblings = children.filter(
|
|
10069
|
+
(cur) => Validator.validatePermittedCategory(cur, rule, true)
|
|
10070
|
+
);
|
|
10071
|
+
if (siblings.length > limit) {
|
|
10072
|
+
for (const child of siblings.slice(limit)) {
|
|
10073
|
+
cb(child, category);
|
|
10098
10074
|
}
|
|
10099
|
-
|
|
10100
|
-
|
|
10101
|
-
allowEmpty: {
|
|
10102
|
-
type: "boolean"
|
|
10075
|
+
valid = false;
|
|
10076
|
+
}
|
|
10103
10077
|
}
|
|
10104
|
-
}
|
|
10105
|
-
|
|
10106
|
-
documentation() {
|
|
10107
|
-
return {
|
|
10108
|
-
description: "Both HTML5 and WCAG 2.0 requires images to have a alternative text for each image.",
|
|
10109
|
-
url: "https://html-validate.org/rules/wcag/h37.html"
|
|
10110
|
-
};
|
|
10078
|
+
}
|
|
10079
|
+
return valid;
|
|
10111
10080
|
}
|
|
10112
|
-
|
|
10113
|
-
|
|
10114
|
-
|
|
10115
|
-
|
|
10116
|
-
|
|
10117
|
-
|
|
10081
|
+
/**
|
|
10082
|
+
* Validate elements order.
|
|
10083
|
+
*
|
|
10084
|
+
* Given a parent element with children and metadata containing permitted
|
|
10085
|
+
* order it will validate each children and ensure each one exists in the
|
|
10086
|
+
* specified order.
|
|
10087
|
+
*
|
|
10088
|
+
* For instance, for a `<table>` element the `<caption>` element must come
|
|
10089
|
+
* before a `<thead>` which must come before `<tbody>`.
|
|
10090
|
+
*
|
|
10091
|
+
* @param children - Array of children to validate.
|
|
10092
|
+
*/
|
|
10093
|
+
static validateOrder(children, rules, cb) {
|
|
10094
|
+
if (!rules) {
|
|
10095
|
+
return true;
|
|
10096
|
+
}
|
|
10097
|
+
let i = 0;
|
|
10098
|
+
let prev = null;
|
|
10099
|
+
for (const node of children) {
|
|
10100
|
+
const old = i;
|
|
10101
|
+
while (rules[i] && !Validator.validatePermittedCategory(node, rules[i], true)) {
|
|
10102
|
+
i++;
|
|
10103
|
+
}
|
|
10104
|
+
if (i >= rules.length) {
|
|
10105
|
+
const orderSpecified = rules.find(
|
|
10106
|
+
(cur) => Validator.validatePermittedCategory(node, cur, true)
|
|
10107
|
+
);
|
|
10108
|
+
if (orderSpecified) {
|
|
10109
|
+
cb(node, prev);
|
|
10110
|
+
return false;
|
|
10111
|
+
}
|
|
10112
|
+
i = old;
|
|
10118
10113
|
}
|
|
10114
|
+
prev = node;
|
|
10115
|
+
}
|
|
10116
|
+
return true;
|
|
10117
|
+
}
|
|
10118
|
+
/**
|
|
10119
|
+
* Validate element ancestors.
|
|
10120
|
+
*
|
|
10121
|
+
* Check if an element has the required set of elements. At least one of the
|
|
10122
|
+
* selectors must match.
|
|
10123
|
+
*/
|
|
10124
|
+
static validateAncestors(node, rules) {
|
|
10125
|
+
if (!rules || rules.length === 0) {
|
|
10126
|
+
return true;
|
|
10127
|
+
}
|
|
10128
|
+
return rules.some((rule) => node.closest(rule));
|
|
10129
|
+
}
|
|
10130
|
+
/**
|
|
10131
|
+
* Validate element required content.
|
|
10132
|
+
*
|
|
10133
|
+
* Check if an element has the required set of elements. At least one of the
|
|
10134
|
+
* selectors must match.
|
|
10135
|
+
*
|
|
10136
|
+
* Returns `[]` when valid or a list of required but missing tagnames or
|
|
10137
|
+
* categories.
|
|
10138
|
+
*/
|
|
10139
|
+
static validateRequiredContent(node, rules) {
|
|
10140
|
+
if (!rules || rules.length === 0) {
|
|
10141
|
+
return [];
|
|
10142
|
+
}
|
|
10143
|
+
return rules.filter((tagName) => {
|
|
10144
|
+
const haveMatchingChild = node.childElements.some(
|
|
10145
|
+
(child) => Validator.validatePermittedCategory(child, tagName, false)
|
|
10146
|
+
);
|
|
10147
|
+
return !haveMatchingChild;
|
|
10119
10148
|
});
|
|
10120
10149
|
}
|
|
10121
|
-
|
|
10122
|
-
|
|
10123
|
-
|
|
10150
|
+
/**
|
|
10151
|
+
* Test if an attribute has an allowed value and/or format.
|
|
10152
|
+
*
|
|
10153
|
+
* @param attr - Attribute to test.
|
|
10154
|
+
* @param rules - Element attribute metadta.
|
|
10155
|
+
* @returns `true` if attribute passes all tests.
|
|
10156
|
+
*/
|
|
10157
|
+
static validateAttribute(attr, rules) {
|
|
10158
|
+
const rule = rules[attr.key];
|
|
10159
|
+
if (!rule) {
|
|
10160
|
+
return true;
|
|
10124
10161
|
}
|
|
10125
|
-
|
|
10126
|
-
|
|
10162
|
+
const value = attr.value;
|
|
10163
|
+
if (value instanceof DynamicValue) {
|
|
10164
|
+
return true;
|
|
10127
10165
|
}
|
|
10128
|
-
|
|
10129
|
-
|
|
10130
|
-
|
|
10131
|
-
}
|
|
10166
|
+
const empty = value === null || value === "";
|
|
10167
|
+
if (rule.boolean) {
|
|
10168
|
+
return empty || value === attr.key;
|
|
10132
10169
|
}
|
|
10133
|
-
|
|
10134
|
-
|
|
10135
|
-
const attr = node.getAttribute("alt");
|
|
10136
|
-
this.report(node, `${tag} cannot have empty "alt" attribute`, attr.keyLocation);
|
|
10137
|
-
} else {
|
|
10138
|
-
this.report(node, `${tag} is missing required "alt" attribute`, node.location);
|
|
10170
|
+
if (rule.omit && empty) {
|
|
10171
|
+
return true;
|
|
10139
10172
|
}
|
|
10173
|
+
if (rule.list) {
|
|
10174
|
+
const tokens = new DOMTokenList(value, attr.valueLocation);
|
|
10175
|
+
return tokens.every((token) => {
|
|
10176
|
+
return this.validateAttributeValue(token, rule);
|
|
10177
|
+
});
|
|
10178
|
+
}
|
|
10179
|
+
return this.validateAttributeValue(value, rule);
|
|
10140
10180
|
}
|
|
10141
|
-
|
|
10142
|
-
|
|
10143
|
-
|
|
10144
|
-
|
|
10145
|
-
|
|
10146
|
-
|
|
10147
|
-
|
|
10148
|
-
|
|
10149
|
-
|
|
10150
|
-
|
|
10151
|
-
|
|
10152
|
-
|
|
10153
|
-
|
|
10154
|
-
this.on("tag:ready", (event) => {
|
|
10155
|
-
const node = event.target;
|
|
10156
|
-
if (node.tagName !== "th") {
|
|
10157
|
-
return;
|
|
10158
|
-
}
|
|
10159
|
-
const scope = node.getAttribute("scope");
|
|
10160
|
-
const value = scope == null ? void 0 : scope.value;
|
|
10161
|
-
if (value instanceof DynamicValue) {
|
|
10162
|
-
return;
|
|
10163
|
-
}
|
|
10164
|
-
if (value && validScopes.includes(value)) {
|
|
10165
|
-
return;
|
|
10166
|
-
}
|
|
10167
|
-
const message = `<th> element must have a valid scope attribute: ${joinedScopes}`;
|
|
10168
|
-
const location = (scope == null ? void 0 : scope.valueLocation) ?? (scope == null ? void 0 : scope.keyLocation) ?? node.location;
|
|
10169
|
-
this.report(node, message, location);
|
|
10170
|
-
});
|
|
10171
|
-
}
|
|
10172
|
-
}
|
|
10173
|
-
|
|
10174
|
-
class H67 extends Rule {
|
|
10175
|
-
documentation() {
|
|
10176
|
-
return {
|
|
10177
|
-
description: "A decorative image cannot have a title attribute. Either remove `title` or add a descriptive `alt` text.",
|
|
10178
|
-
url: "https://html-validate.org/rules/wcag/h67.html"
|
|
10179
|
-
};
|
|
10180
|
-
}
|
|
10181
|
-
setup() {
|
|
10182
|
-
this.on("tag:end", (event) => {
|
|
10183
|
-
const node = event.target;
|
|
10184
|
-
if (!node || node.tagName !== "img") {
|
|
10185
|
-
return;
|
|
10186
|
-
}
|
|
10187
|
-
const title = node.getAttribute("title");
|
|
10188
|
-
if (!title || title.value === "") {
|
|
10189
|
-
return;
|
|
10190
|
-
}
|
|
10191
|
-
const alt = node.getAttributeValue("alt");
|
|
10192
|
-
if (alt && alt !== "") {
|
|
10193
|
-
return;
|
|
10181
|
+
static validateAttributeValue(value, rule) {
|
|
10182
|
+
if (!rule.enum) {
|
|
10183
|
+
return true;
|
|
10184
|
+
}
|
|
10185
|
+
if (value === null) {
|
|
10186
|
+
return false;
|
|
10187
|
+
}
|
|
10188
|
+
const caseInsensitiveValue = value.toLowerCase();
|
|
10189
|
+
return rule.enum.some((entry) => {
|
|
10190
|
+
if (entry instanceof RegExp) {
|
|
10191
|
+
return !!value.match(entry);
|
|
10192
|
+
} else {
|
|
10193
|
+
return caseInsensitiveValue === entry;
|
|
10194
10194
|
}
|
|
10195
|
-
this.report(node, "<img> with empty alt text cannot have title attribute", title.keyLocation);
|
|
10196
10195
|
});
|
|
10197
10196
|
}
|
|
10198
|
-
|
|
10199
|
-
|
|
10200
|
-
|
|
10201
|
-
|
|
10202
|
-
|
|
10203
|
-
|
|
10204
|
-
|
|
10205
|
-
}
|
|
10206
|
-
|
|
10207
|
-
|
|
10208
|
-
|
|
10209
|
-
|
|
10210
|
-
|
|
10211
|
-
|
|
10212
|
-
|
|
10197
|
+
static validatePermittedRule(node, rule, isExclude = false) {
|
|
10198
|
+
if (typeof rule === "string") {
|
|
10199
|
+
return Validator.validatePermittedCategory(node, rule, !isExclude);
|
|
10200
|
+
} else if (Array.isArray(rule)) {
|
|
10201
|
+
return rule.every((inner) => {
|
|
10202
|
+
return Validator.validatePermittedRule(node, inner, isExclude);
|
|
10203
|
+
});
|
|
10204
|
+
} else {
|
|
10205
|
+
validateKeys(rule);
|
|
10206
|
+
if (rule.exclude) {
|
|
10207
|
+
if (Array.isArray(rule.exclude)) {
|
|
10208
|
+
return !rule.exclude.some((inner) => {
|
|
10209
|
+
return Validator.validatePermittedRule(node, inner, true);
|
|
10210
|
+
});
|
|
10211
|
+
} else {
|
|
10212
|
+
return !Validator.validatePermittedRule(node, rule.exclude, true);
|
|
10213
|
+
}
|
|
10214
|
+
} else {
|
|
10215
|
+
return true;
|
|
10213
10216
|
}
|
|
10214
|
-
}
|
|
10217
|
+
}
|
|
10215
10218
|
}
|
|
10216
|
-
|
|
10217
|
-
|
|
10218
|
-
|
|
10219
|
-
|
|
10219
|
+
/**
|
|
10220
|
+
* Validate node against a content category.
|
|
10221
|
+
*
|
|
10222
|
+
* When matching parent nodes against permitted parents use the superset
|
|
10223
|
+
* parameter to also match for `@flow`. E.g. if a node expects a `@phrasing`
|
|
10224
|
+
* parent it should also allow `@flow` parent since `@phrasing` is a subset of
|
|
10225
|
+
* `@flow`.
|
|
10226
|
+
*
|
|
10227
|
+
* @param node - The node to test against
|
|
10228
|
+
* @param category - Name of category with `@` prefix or tag name.
|
|
10229
|
+
* @param defaultMatch - The default return value when node categories is not known.
|
|
10230
|
+
*/
|
|
10231
|
+
/* eslint-disable-next-line complexity -- rule does not like switch */
|
|
10232
|
+
static validatePermittedCategory(node, category, defaultMatch) {
|
|
10233
|
+
const [, rawCategory] = category.match(/^(@?.*?)([?*]?)$/);
|
|
10234
|
+
if (!rawCategory.startsWith("@")) {
|
|
10235
|
+
return node.tagName === rawCategory;
|
|
10236
|
+
}
|
|
10237
|
+
if (!node.meta) {
|
|
10238
|
+
return defaultMatch;
|
|
10239
|
+
}
|
|
10240
|
+
switch (rawCategory) {
|
|
10241
|
+
case "@meta":
|
|
10242
|
+
return node.meta.metadata;
|
|
10243
|
+
case "@flow":
|
|
10244
|
+
return node.meta.flow;
|
|
10245
|
+
case "@sectioning":
|
|
10246
|
+
return node.meta.sectioning;
|
|
10247
|
+
case "@heading":
|
|
10248
|
+
return node.meta.heading;
|
|
10249
|
+
case "@phrasing":
|
|
10250
|
+
return node.meta.phrasing;
|
|
10251
|
+
case "@embedded":
|
|
10252
|
+
return node.meta.embedded;
|
|
10253
|
+
case "@interactive":
|
|
10254
|
+
return node.meta.interactive;
|
|
10255
|
+
case "@script":
|
|
10256
|
+
return Boolean(node.meta.scriptSupporting);
|
|
10257
|
+
case "@form":
|
|
10258
|
+
return Boolean(node.meta.form);
|
|
10259
|
+
default:
|
|
10260
|
+
throw new Error(`Invalid content category "${category}"`);
|
|
10220
10261
|
}
|
|
10221
10262
|
}
|
|
10222
|
-
|
|
10223
|
-
|
|
10263
|
+
}
|
|
10264
|
+
function validateKeys(rule) {
|
|
10265
|
+
for (const key of Object.keys(rule)) {
|
|
10266
|
+
if (!allowedKeys.includes(key)) {
|
|
10267
|
+
const str = JSON.stringify(rule);
|
|
10268
|
+
throw new Error(`Permitted rule "${str}" contains unknown property "${key}"`);
|
|
10269
|
+
}
|
|
10224
10270
|
}
|
|
10225
|
-
|
|
10226
|
-
|
|
10271
|
+
}
|
|
10272
|
+
function parseQuantifier(quantifier) {
|
|
10273
|
+
switch (quantifier) {
|
|
10274
|
+
case "?":
|
|
10275
|
+
return 1;
|
|
10276
|
+
case "*":
|
|
10277
|
+
return null;
|
|
10278
|
+
default:
|
|
10279
|
+
throw new Error(`Invalid quantifier "${quantifier}" used`);
|
|
10227
10280
|
}
|
|
10228
10281
|
}
|
|
10229
10282
|
|
|
10230
|
-
const
|
|
10231
|
-
|
|
10232
|
-
|
|
10233
|
-
|
|
10234
|
-
|
|
10235
|
-
|
|
10236
|
-
|
|
10237
|
-
|
|
10283
|
+
const $schema = "http://json-schema.org/draft-06/schema#";
|
|
10284
|
+
const $id = "https://html-validate.org/schemas/config.json";
|
|
10285
|
+
const type = "object";
|
|
10286
|
+
const additionalProperties = false;
|
|
10287
|
+
const properties = {
|
|
10288
|
+
$schema: {
|
|
10289
|
+
type: "string"
|
|
10290
|
+
},
|
|
10291
|
+
root: {
|
|
10292
|
+
type: "boolean",
|
|
10293
|
+
title: "Mark as root configuration",
|
|
10294
|
+
description: "If this is set to true no further configurations will be searched.",
|
|
10295
|
+
"default": false
|
|
10296
|
+
},
|
|
10297
|
+
"extends": {
|
|
10298
|
+
type: "array",
|
|
10299
|
+
items: {
|
|
10300
|
+
type: "string"
|
|
10301
|
+
},
|
|
10302
|
+
title: "Configurations to extend",
|
|
10303
|
+
description: "Array of shareable or builtin configurations to extend."
|
|
10304
|
+
},
|
|
10305
|
+
elements: {
|
|
10306
|
+
type: "array",
|
|
10307
|
+
items: {
|
|
10308
|
+
anyOf: [
|
|
10309
|
+
{
|
|
10310
|
+
type: "string"
|
|
10311
|
+
},
|
|
10312
|
+
{
|
|
10313
|
+
type: "object"
|
|
10314
|
+
}
|
|
10315
|
+
]
|
|
10316
|
+
},
|
|
10317
|
+
title: "Element metadata to load",
|
|
10318
|
+
description: "Array of modules, plugins or files to load element metadata from. Use <rootDir> to refer to the folder with the package.json file.",
|
|
10319
|
+
examples: [
|
|
10320
|
+
[
|
|
10321
|
+
"html-validate:recommended",
|
|
10322
|
+
"plugin:recommended",
|
|
10323
|
+
"module",
|
|
10324
|
+
"./local-file.json"
|
|
10325
|
+
]
|
|
10326
|
+
]
|
|
10327
|
+
},
|
|
10328
|
+
plugins: {
|
|
10329
|
+
type: "array",
|
|
10330
|
+
items: {
|
|
10331
|
+
anyOf: [
|
|
10332
|
+
{
|
|
10333
|
+
type: "string"
|
|
10334
|
+
},
|
|
10335
|
+
{
|
|
10336
|
+
type: "object"
|
|
10337
|
+
}
|
|
10338
|
+
]
|
|
10339
|
+
},
|
|
10340
|
+
title: "Plugins to load",
|
|
10341
|
+
description: "Array of plugins load. Use <rootDir> to refer to the folder with the package.json file.",
|
|
10342
|
+
examples: [
|
|
10343
|
+
[
|
|
10344
|
+
"my-plugin",
|
|
10345
|
+
"./local-plugin"
|
|
10346
|
+
]
|
|
10347
|
+
]
|
|
10348
|
+
},
|
|
10349
|
+
transform: {
|
|
10350
|
+
type: "object",
|
|
10351
|
+
additionalProperties: {
|
|
10352
|
+
type: "string"
|
|
10353
|
+
},
|
|
10354
|
+
title: "File transformations to use.",
|
|
10355
|
+
description: "Object where key is regular expression to match filename and value is name of transformer.",
|
|
10356
|
+
examples: [
|
|
10357
|
+
{
|
|
10358
|
+
"^.*\\.foo$": "my-transformer",
|
|
10359
|
+
"^.*\\.bar$": "my-plugin",
|
|
10360
|
+
"^.*\\.baz$": "my-plugin:named"
|
|
10361
|
+
}
|
|
10362
|
+
]
|
|
10363
|
+
},
|
|
10364
|
+
rules: {
|
|
10365
|
+
type: "object",
|
|
10366
|
+
patternProperties: {
|
|
10367
|
+
".*": {
|
|
10368
|
+
anyOf: [
|
|
10369
|
+
{
|
|
10370
|
+
"enum": [
|
|
10371
|
+
0,
|
|
10372
|
+
1,
|
|
10373
|
+
2,
|
|
10374
|
+
"off",
|
|
10375
|
+
"warn",
|
|
10376
|
+
"error"
|
|
10377
|
+
]
|
|
10378
|
+
},
|
|
10379
|
+
{
|
|
10380
|
+
type: "array",
|
|
10381
|
+
minItems: 1,
|
|
10382
|
+
maxItems: 1,
|
|
10383
|
+
items: [
|
|
10384
|
+
{
|
|
10385
|
+
"enum": [
|
|
10386
|
+
0,
|
|
10387
|
+
1,
|
|
10388
|
+
2,
|
|
10389
|
+
"off",
|
|
10390
|
+
"warn",
|
|
10391
|
+
"error"
|
|
10392
|
+
]
|
|
10393
|
+
}
|
|
10394
|
+
]
|
|
10395
|
+
},
|
|
10396
|
+
{
|
|
10397
|
+
type: "array",
|
|
10398
|
+
minItems: 2,
|
|
10399
|
+
maxItems: 2,
|
|
10400
|
+
items: [
|
|
10401
|
+
{
|
|
10402
|
+
"enum": [
|
|
10403
|
+
0,
|
|
10404
|
+
1,
|
|
10405
|
+
2,
|
|
10406
|
+
"off",
|
|
10407
|
+
"warn",
|
|
10408
|
+
"error"
|
|
10409
|
+
]
|
|
10410
|
+
},
|
|
10411
|
+
{
|
|
10412
|
+
}
|
|
10413
|
+
]
|
|
10414
|
+
}
|
|
10415
|
+
]
|
|
10416
|
+
}
|
|
10417
|
+
},
|
|
10418
|
+
title: "Rule configuration.",
|
|
10419
|
+
description: "Enable/disable rules, set severity. Some rules have additional configuration like style or patterns to use.",
|
|
10420
|
+
examples: [
|
|
10421
|
+
{
|
|
10422
|
+
foo: "error",
|
|
10423
|
+
bar: "off",
|
|
10424
|
+
baz: [
|
|
10425
|
+
"error",
|
|
10426
|
+
{
|
|
10427
|
+
style: "camelcase"
|
|
10428
|
+
}
|
|
10429
|
+
]
|
|
10430
|
+
}
|
|
10431
|
+
]
|
|
10432
|
+
}
|
|
10433
|
+
};
|
|
10434
|
+
var configurationSchema = {
|
|
10435
|
+
$schema: $schema,
|
|
10436
|
+
$id: $id,
|
|
10437
|
+
type: type,
|
|
10438
|
+
additionalProperties: additionalProperties,
|
|
10439
|
+
properties: properties
|
|
10238
10440
|
};
|
|
10239
10441
|
|
|
10240
|
-
const
|
|
10241
|
-
|
|
10242
|
-
"area-alt": AreaAlt,
|
|
10243
|
-
"aria-hidden-body": AriaHiddenBody,
|
|
10244
|
-
"aria-label-misuse": AriaLabelMisuse,
|
|
10245
|
-
"attr-case": AttrCase,
|
|
10246
|
-
"attr-delimiter": AttrDelimiter,
|
|
10247
|
-
"attr-pattern": AttrPattern,
|
|
10248
|
-
"attr-quotes": AttrQuotes,
|
|
10249
|
-
"attr-spacing": AttrSpacing,
|
|
10250
|
-
"attribute-allowed-values": AttributeAllowedValues,
|
|
10251
|
-
"attribute-boolean-style": AttributeBooleanStyle,
|
|
10252
|
-
"attribute-empty-style": AttributeEmptyStyle,
|
|
10253
|
-
"attribute-misuse": AttributeMisuse,
|
|
10254
|
-
"class-pattern": ClassPattern,
|
|
10255
|
-
"close-attr": CloseAttr,
|
|
10256
|
-
"close-order": CloseOrder,
|
|
10257
|
-
deprecated: Deprecated,
|
|
10258
|
-
"deprecated-rule": DeprecatedRule,
|
|
10259
|
-
"doctype-html": NoStyleTag$1,
|
|
10260
|
-
"doctype-style": DoctypeStyle,
|
|
10261
|
-
"element-case": ElementCase,
|
|
10262
|
-
"element-name": ElementName,
|
|
10263
|
-
"element-permitted-content": ElementPermittedContent,
|
|
10264
|
-
"element-permitted-occurrences": ElementPermittedOccurrences,
|
|
10265
|
-
"element-permitted-order": ElementPermittedOrder,
|
|
10266
|
-
"element-permitted-parent": ElementPermittedParent,
|
|
10267
|
-
"element-required-ancestor": ElementRequiredAncestor,
|
|
10268
|
-
"element-required-attributes": ElementRequiredAttributes,
|
|
10269
|
-
"element-required-content": ElementRequiredContent,
|
|
10270
|
-
"empty-heading": EmptyHeading,
|
|
10271
|
-
"empty-title": EmptyTitle,
|
|
10272
|
-
"form-dup-name": FormDupName,
|
|
10273
|
-
"heading-level": HeadingLevel,
|
|
10274
|
-
"hidden-focusable": HiddenFocusable,
|
|
10275
|
-
"id-pattern": IdPattern,
|
|
10276
|
-
"input-attributes": InputAttributes,
|
|
10277
|
-
"input-missing-label": InputMissingLabel,
|
|
10278
|
-
"long-title": LongTitle,
|
|
10279
|
-
"map-dup-name": MapDupName,
|
|
10280
|
-
"map-id-name": MapIdName,
|
|
10281
|
-
"meta-refresh": MetaRefresh,
|
|
10282
|
-
"missing-doctype": MissingDoctype,
|
|
10283
|
-
"multiple-labeled-controls": MultipleLabeledControls,
|
|
10284
|
-
"name-pattern": NamePattern,
|
|
10285
|
-
"no-abstract-role": NoAbstractRole,
|
|
10286
|
-
"no-autoplay": NoAutoplay,
|
|
10287
|
-
"no-conditional-comment": NoConditionalComment,
|
|
10288
|
-
"no-deprecated-attr": NoDeprecatedAttr,
|
|
10289
|
-
"no-dup-attr": NoDupAttr,
|
|
10290
|
-
"no-dup-class": NoDupClass,
|
|
10291
|
-
"no-dup-id": NoDupID,
|
|
10292
|
-
"no-implicit-button-type": NoImplicitButtonType,
|
|
10293
|
-
"no-implicit-input-type": NoImplicitInputType,
|
|
10294
|
-
"no-implicit-close": NoImplicitClose,
|
|
10295
|
-
"no-inline-style": NoInlineStyle,
|
|
10296
|
-
"no-missing-references": NoMissingReferences,
|
|
10297
|
-
"no-multiple-main": NoMultipleMain,
|
|
10298
|
-
"no-raw-characters": NoRawCharacters,
|
|
10299
|
-
"no-redundant-aria-label": NoRedundantAriaLabel,
|
|
10300
|
-
"no-redundant-for": NoRedundantFor,
|
|
10301
|
-
"no-redundant-role": NoRedundantRole,
|
|
10302
|
-
"no-self-closing": NoSelfClosing,
|
|
10303
|
-
"no-style-tag": NoStyleTag,
|
|
10304
|
-
"no-trailing-whitespace": NoTrailingWhitespace,
|
|
10305
|
-
"no-unknown-elements": NoUnknownElements,
|
|
10306
|
-
"no-unused-disable": NoUnusedDisable,
|
|
10307
|
-
"no-utf8-bom": NoUtf8Bom,
|
|
10308
|
-
"prefer-button": PreferButton,
|
|
10309
|
-
"prefer-native-element": PreferNativeElement,
|
|
10310
|
-
"prefer-tbody": PreferTbody,
|
|
10311
|
-
"require-csp-nonce": RequireCSPNonce,
|
|
10312
|
-
"require-sri": RequireSri,
|
|
10313
|
-
"script-element": ScriptElement,
|
|
10314
|
-
"script-type": ScriptType,
|
|
10315
|
-
"svg-focusable": SvgFocusable,
|
|
10316
|
-
"tel-non-breaking": TelNonBreaking,
|
|
10317
|
-
"text-content": TextContent,
|
|
10318
|
-
"unique-landmark": UniqueLandmark,
|
|
10319
|
-
"unrecognized-char-ref": UnknownCharReference,
|
|
10320
|
-
"valid-autocomplete": ValidAutocomplete,
|
|
10321
|
-
"valid-id": ValidID,
|
|
10322
|
-
"void-content": VoidContent,
|
|
10323
|
-
"void-style": VoidStyle,
|
|
10324
|
-
...bundledRules$1
|
|
10442
|
+
const TRANSFORMER_API = {
|
|
10443
|
+
VERSION: 1
|
|
10325
10444
|
};
|
|
10326
10445
|
|
|
10327
10446
|
var defaultConfig = {};
|
|
@@ -11449,7 +11568,7 @@ class Parser {
|
|
|
11449
11568
|
location
|
|
11450
11569
|
};
|
|
11451
11570
|
this.trigger("tag:end", event);
|
|
11452
|
-
if (
|
|
11571
|
+
if (!active.isRootElement()) {
|
|
11453
11572
|
this.trigger("element:ready", {
|
|
11454
11573
|
target: active,
|
|
11455
11574
|
location: active.location
|
|
@@ -11801,15 +11920,6 @@ class Parser {
|
|
|
11801
11920
|
}
|
|
11802
11921
|
}
|
|
11803
11922
|
|
|
11804
|
-
function isThenable(value) {
|
|
11805
|
-
return value && typeof value === "object" && "then" in value && typeof value.then === "function";
|
|
11806
|
-
}
|
|
11807
|
-
|
|
11808
|
-
const ruleIds = new Set(Object.keys(bundledRules));
|
|
11809
|
-
function ruleExists(ruleId) {
|
|
11810
|
-
return ruleIds.has(ruleId);
|
|
11811
|
-
}
|
|
11812
|
-
|
|
11813
11923
|
function freeze(src) {
|
|
11814
11924
|
return {
|
|
11815
11925
|
...src,
|
|
@@ -11980,7 +12090,7 @@ class Engine {
|
|
|
11980
12090
|
const directiveContext = {
|
|
11981
12091
|
rules,
|
|
11982
12092
|
reportUnused(rules2, unused, options, location2) {
|
|
11983
|
-
if (
|
|
12093
|
+
if (!rules2.has(noUnusedDisable.name)) {
|
|
11984
12094
|
noUnusedDisable.reportUnused(unused, options, location2);
|
|
11985
12095
|
}
|
|
11986
12096
|
}
|
|
@@ -12057,33 +12167,8 @@ class Engine {
|
|
|
12057
12167
|
}
|
|
12058
12168
|
dumpTree(source) {
|
|
12059
12169
|
const parser = this.instantiateParser();
|
|
12060
|
-
const
|
|
12061
|
-
|
|
12062
|
-
function decoration(node) {
|
|
12063
|
-
let output = "";
|
|
12064
|
-
if (node.id) {
|
|
12065
|
-
output += `#${node.id}`;
|
|
12066
|
-
}
|
|
12067
|
-
if (node.hasAttribute("class")) {
|
|
12068
|
-
output += `.${node.classList.join(".")}`;
|
|
12069
|
-
}
|
|
12070
|
-
return output;
|
|
12071
|
-
}
|
|
12072
|
-
function writeNode(node, level, sibling) {
|
|
12073
|
-
if (node.parent) {
|
|
12074
|
-
const indent = " ".repeat(level - 1);
|
|
12075
|
-
const l = node.childElements.length > 0 ? "\u252C" : "\u2500";
|
|
12076
|
-
const b = sibling < node.parent.childElements.length - 1 ? "\u251C" : "\u2514";
|
|
12077
|
-
lines.push(`${indent}${b}\u2500${l} ${node.tagName}${decoration(node)}`);
|
|
12078
|
-
} else {
|
|
12079
|
-
lines.push("(root)");
|
|
12080
|
-
}
|
|
12081
|
-
node.childElements.forEach((child, index) => {
|
|
12082
|
-
writeNode(child, level + 1, index);
|
|
12083
|
-
});
|
|
12084
|
-
}
|
|
12085
|
-
writeNode(document, 0, 0);
|
|
12086
|
-
return lines;
|
|
12170
|
+
const root = parser.parseHtml(source[0]);
|
|
12171
|
+
return dumpTree(root);
|
|
12087
12172
|
}
|
|
12088
12173
|
/**
|
|
12089
12174
|
* Get rule documentation.
|
|
@@ -12111,7 +12196,9 @@ class Engine {
|
|
|
12111
12196
|
return new this.ParserClass(this.config);
|
|
12112
12197
|
}
|
|
12113
12198
|
processDirective(event, parser, context) {
|
|
12114
|
-
const rules = event.data.split(",").map((name) => name.trim()).map((name) => context.rules[name]).filter((rule) =>
|
|
12199
|
+
const rules = event.data.split(",").map((name) => name.trim()).map((name) => context.rules[name]).filter((rule) => {
|
|
12200
|
+
return Boolean(rule);
|
|
12201
|
+
});
|
|
12115
12202
|
const location = event.optionsLocation ?? event.location;
|
|
12116
12203
|
switch (event.action) {
|
|
12117
12204
|
case "enable":
|
|
@@ -12135,7 +12222,7 @@ class Engine {
|
|
|
12135
12222
|
rule.setServerity(Severity.ERROR);
|
|
12136
12223
|
}
|
|
12137
12224
|
}
|
|
12138
|
-
parser.on("tag:start", (
|
|
12225
|
+
parser.on("tag:start", (_event, data) => {
|
|
12139
12226
|
data.target.enableRules(rules.map((rule) => rule.name));
|
|
12140
12227
|
});
|
|
12141
12228
|
}
|
|
@@ -12143,7 +12230,7 @@ class Engine {
|
|
|
12143
12230
|
for (const rule of rules) {
|
|
12144
12231
|
rule.setEnabled(false);
|
|
12145
12232
|
}
|
|
12146
|
-
parser.on("tag:start", (
|
|
12233
|
+
parser.on("tag:start", (_event, data) => {
|
|
12147
12234
|
data.target.disableRules(rules.map((rule) => rule.name));
|
|
12148
12235
|
});
|
|
12149
12236
|
}
|
|
@@ -12155,14 +12242,14 @@ class Engine {
|
|
|
12155
12242
|
for (const rule of rules) {
|
|
12156
12243
|
rule.block(blocker);
|
|
12157
12244
|
}
|
|
12158
|
-
const unregisterOpen = parser.on("tag:start", (
|
|
12245
|
+
const unregisterOpen = parser.on("tag:start", (_event, data) => {
|
|
12159
12246
|
var _a;
|
|
12160
12247
|
if (directiveBlock === null) {
|
|
12161
12248
|
directiveBlock = ((_a = data.target.parent) == null ? void 0 : _a.unique) ?? null;
|
|
12162
12249
|
}
|
|
12163
12250
|
data.target.blockRules(ruleIds, blocker);
|
|
12164
12251
|
});
|
|
12165
|
-
const unregisterClose = parser.on("tag:end", (
|
|
12252
|
+
const unregisterClose = parser.on("tag:end", (_event, data) => {
|
|
12166
12253
|
const lastNode = directiveBlock === null;
|
|
12167
12254
|
const parentClosed = directiveBlock === data.previous.unique;
|
|
12168
12255
|
if (lastNode || parentClosed) {
|
|
@@ -12173,7 +12260,7 @@ class Engine {
|
|
|
12173
12260
|
}
|
|
12174
12261
|
}
|
|
12175
12262
|
});
|
|
12176
|
-
parser.on("rule:error", (
|
|
12263
|
+
parser.on("rule:error", (_event, data) => {
|
|
12177
12264
|
if (data.blockers.includes(blocker)) {
|
|
12178
12265
|
unused.delete(data.ruleId);
|
|
12179
12266
|
}
|
|
@@ -12189,10 +12276,10 @@ class Engine {
|
|
|
12189
12276
|
for (const rule of rules) {
|
|
12190
12277
|
rule.block(blocker);
|
|
12191
12278
|
}
|
|
12192
|
-
const unregister = parser.on("tag:start", (
|
|
12279
|
+
const unregister = parser.on("tag:start", (_event, data) => {
|
|
12193
12280
|
data.target.blockRules(ruleIds, blocker);
|
|
12194
12281
|
});
|
|
12195
|
-
parser.on("rule:error", (
|
|
12282
|
+
parser.on("rule:error", (_event, data) => {
|
|
12196
12283
|
if (data.blockers.includes(blocker)) {
|
|
12197
12284
|
unused.delete(data.ruleId);
|
|
12198
12285
|
}
|
|
@@ -12756,7 +12843,7 @@ class HtmlValidate {
|
|
|
12756
12843
|
}
|
|
12757
12844
|
|
|
12758
12845
|
const name = "html-validate";
|
|
12759
|
-
const version = "8.
|
|
12846
|
+
const version = "8.22.0";
|
|
12760
12847
|
const bugs = "https://gitlab.com/html-validate/html-validate/issues/new";
|
|
12761
12848
|
|
|
12762
12849
|
function definePlugin(plugin) {
|
|
@@ -12844,17 +12931,26 @@ const REPLACERS = [
|
|
|
12844
12931
|
[
|
|
12845
12932
|
// (a\ ) -> (a )
|
|
12846
12933
|
// (a ) -> (a)
|
|
12934
|
+
// (a ) -> (a)
|
|
12847
12935
|
// (a \ ) -> (a )
|
|
12848
|
-
|
|
12849
|
-
|
|
12850
|
-
|
|
12851
|
-
|
|
12936
|
+
/((?:\\\\)*?)(\\?\s+)$/,
|
|
12937
|
+
(_, m1, m2) => m1 + (
|
|
12938
|
+
m2.indexOf('\\') === 0
|
|
12939
|
+
? SPACE
|
|
12940
|
+
: EMPTY
|
|
12941
|
+
)
|
|
12852
12942
|
],
|
|
12853
12943
|
|
|
12854
12944
|
// replace (\ ) with ' '
|
|
12945
|
+
// (\ ) -> ' '
|
|
12946
|
+
// (\\ ) -> '\\ '
|
|
12947
|
+
// (\\\ ) -> '\\ '
|
|
12855
12948
|
[
|
|
12856
|
-
|
|
12857
|
-
() =>
|
|
12949
|
+
/(\\+?)\s/g,
|
|
12950
|
+
(_, m1) => {
|
|
12951
|
+
const {length} = m1;
|
|
12952
|
+
return m1.slice(0, length - length % 2) + SPACE
|
|
12953
|
+
}
|
|
12858
12954
|
],
|
|
12859
12955
|
|
|
12860
12956
|
// Escape metacharacters
|
|
@@ -13082,7 +13178,8 @@ const makeRegex = (pattern, ignoreCase) => {
|
|
|
13082
13178
|
|
|
13083
13179
|
if (!source) {
|
|
13084
13180
|
source = REPLACERS.reduce(
|
|
13085
|
-
(prev,
|
|
13181
|
+
(prev, [matcher, replacer]) =>
|
|
13182
|
+
prev.replace(matcher, replacer.bind(pattern)),
|
|
13086
13183
|
pattern
|
|
13087
13184
|
);
|
|
13088
13185
|
regexCache[pattern] = source;
|
|
@@ -13448,6 +13545,81 @@ const defaults = {
|
|
|
13448
13545
|
showSummary: true,
|
|
13449
13546
|
showSelector: false
|
|
13450
13547
|
};
|
|
13548
|
+
const NEWLINE = /\r\n|[\n\r\u2028\u2029]/;
|
|
13549
|
+
function getMarkerLines(loc, source) {
|
|
13550
|
+
const startLoc = {
|
|
13551
|
+
...loc.start
|
|
13552
|
+
};
|
|
13553
|
+
const endLoc = {
|
|
13554
|
+
...startLoc,
|
|
13555
|
+
...loc.end
|
|
13556
|
+
};
|
|
13557
|
+
const linesAbove = 2;
|
|
13558
|
+
const linesBelow = 3;
|
|
13559
|
+
const startLine = startLoc.line;
|
|
13560
|
+
const startColumn = startLoc.column;
|
|
13561
|
+
const endLine = endLoc.line;
|
|
13562
|
+
const endColumn = endLoc.column;
|
|
13563
|
+
const start = Math.max(startLine - (linesAbove + 1), 0);
|
|
13564
|
+
const end = Math.min(source.length, endLine + linesBelow);
|
|
13565
|
+
const lineDiff = endLine - startLine;
|
|
13566
|
+
const markerLines = {};
|
|
13567
|
+
if (lineDiff) {
|
|
13568
|
+
for (let i = 0; i <= lineDiff; i++) {
|
|
13569
|
+
const lineNumber = i + startLine;
|
|
13570
|
+
if (!startColumn) {
|
|
13571
|
+
markerLines[lineNumber] = true;
|
|
13572
|
+
} else if (i === 0) {
|
|
13573
|
+
const sourceLength = source[lineNumber - 1].length;
|
|
13574
|
+
markerLines[lineNumber] = [startColumn, sourceLength - startColumn + 1];
|
|
13575
|
+
} else if (i === lineDiff) {
|
|
13576
|
+
markerLines[lineNumber] = [0, endColumn];
|
|
13577
|
+
} else {
|
|
13578
|
+
const sourceLength = source[lineNumber - i].length;
|
|
13579
|
+
markerLines[lineNumber] = [0, sourceLength];
|
|
13580
|
+
}
|
|
13581
|
+
}
|
|
13582
|
+
} else {
|
|
13583
|
+
if (startColumn === endColumn) {
|
|
13584
|
+
if (startColumn) {
|
|
13585
|
+
markerLines[startLine] = [startColumn, 0];
|
|
13586
|
+
} else {
|
|
13587
|
+
markerLines[startLine] = true;
|
|
13588
|
+
}
|
|
13589
|
+
} else {
|
|
13590
|
+
markerLines[startLine] = [startColumn, endColumn - startColumn];
|
|
13591
|
+
}
|
|
13592
|
+
}
|
|
13593
|
+
return { start, end, markerLines };
|
|
13594
|
+
}
|
|
13595
|
+
function codeFrameColumns(rawLines, loc) {
|
|
13596
|
+
const lines = rawLines.split(NEWLINE);
|
|
13597
|
+
const { start, end, markerLines } = getMarkerLines(loc, lines);
|
|
13598
|
+
const numberMaxWidth = String(end).length;
|
|
13599
|
+
return rawLines.split(NEWLINE, end).slice(start, end).map((line, index) => {
|
|
13600
|
+
const number = start + 1 + index;
|
|
13601
|
+
const paddedNumber = ` ${String(number)}`.slice(-numberMaxWidth);
|
|
13602
|
+
const gutter = ` ${paddedNumber} |`;
|
|
13603
|
+
const hasMarker = markerLines[number];
|
|
13604
|
+
if (hasMarker) {
|
|
13605
|
+
let markerLine = "";
|
|
13606
|
+
if (Array.isArray(hasMarker)) {
|
|
13607
|
+
const markerSpacing = line.slice(0, Math.max(hasMarker[0] - 1, 0)).replace(/[^\t]/g, " ");
|
|
13608
|
+
const numberOfMarkers = hasMarker[1] || 1;
|
|
13609
|
+
markerLine = [
|
|
13610
|
+
"\n ",
|
|
13611
|
+
gutter.replace(/\d/g, " "),
|
|
13612
|
+
" ",
|
|
13613
|
+
markerSpacing,
|
|
13614
|
+
"^".repeat(numberOfMarkers)
|
|
13615
|
+
].join("");
|
|
13616
|
+
}
|
|
13617
|
+
return [">", gutter, line.length > 0 ? ` ${line}` : "", markerLine].join("");
|
|
13618
|
+
} else {
|
|
13619
|
+
return [" ", gutter, line.length > 0 ? ` ${line}` : ""].join("");
|
|
13620
|
+
}
|
|
13621
|
+
}).join("\n");
|
|
13622
|
+
}
|
|
13451
13623
|
function pluralize(word, count) {
|
|
13452
13624
|
return count === 1 ? word : `${word}s`;
|
|
13453
13625
|
}
|
|
@@ -13490,16 +13662,11 @@ function formatMessage(message, parentResult, options) {
|
|
|
13490
13662
|
].filter(String).join(" ");
|
|
13491
13663
|
const result = [firstLine];
|
|
13492
13664
|
if (sourceCode) {
|
|
13493
|
-
|
|
13494
|
-
|
|
13495
|
-
|
|
13496
|
-
|
|
13497
|
-
|
|
13498
|
-
end: getEndLocation(message, sourceCode)
|
|
13499
|
-
},
|
|
13500
|
-
{ highlightCode: false }
|
|
13501
|
-
)
|
|
13502
|
-
);
|
|
13665
|
+
const output = codeFrameColumns(sourceCode, {
|
|
13666
|
+
start: getStartLocation(message),
|
|
13667
|
+
end: getEndLocation(message, sourceCode)
|
|
13668
|
+
});
|
|
13669
|
+
result.push(output);
|
|
13503
13670
|
}
|
|
13504
13671
|
if (options.showSelector) {
|
|
13505
13672
|
result.push(`${kleur__default.default.bold("Selector:")} ${message.selector ?? "-"}`);
|
|
@@ -13678,4 +13845,5 @@ exports.ruleExists = ruleExists;
|
|
|
13678
13845
|
exports.sliceLocation = sliceLocation;
|
|
13679
13846
|
exports.staticResolver = staticResolver;
|
|
13680
13847
|
exports.version = version;
|
|
13848
|
+
exports.walk = walk;
|
|
13681
13849
|
//# sourceMappingURL=core.js.map
|