html-validate 8.20.0 → 8.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/browser.js +1 -0
- package/dist/cjs/browser.js.map +1 -1
- package/dist/cjs/cli.js.map +1 -1
- package/dist/cjs/core-nodejs.js.map +1 -1
- package/dist/cjs/core.js +2186 -2148
- 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/index.js +1 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/jest-diff.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/es/browser.js +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 +2186 -2149
- 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 -1
- package/dist/es/index.js +1 -1
- package/dist/es/jest-diff.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/tsdoc-metadata.json +1 -1
- package/dist/types/browser.d.ts +44 -8
- package/dist/types/index.d.ts +44 -8
- package/package.json +21 -21
package/dist/cjs/core.js
CHANGED
|
@@ -1279,14 +1279,12 @@ class MetaTable {
|
|
|
1279
1279
|
* @returns A shallow copy of metadata.
|
|
1280
1280
|
*/
|
|
1281
1281
|
getMetaFor(tagName) {
|
|
1282
|
-
|
|
1283
|
-
if (
|
|
1284
|
-
return { ...
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
return { ...this.elements["*"] };
|
|
1282
|
+
const meta = this.elements[tagName.toLowerCase()] ?? this.elements["*"];
|
|
1283
|
+
if (meta) {
|
|
1284
|
+
return { ...meta };
|
|
1285
|
+
} else {
|
|
1286
|
+
return null;
|
|
1288
1287
|
}
|
|
1289
|
-
return null;
|
|
1290
1288
|
}
|
|
1291
1289
|
/**
|
|
1292
1290
|
* Find all tags which has enabled given property.
|
|
@@ -1294,7 +1292,7 @@ class MetaTable {
|
|
|
1294
1292
|
* @public
|
|
1295
1293
|
*/
|
|
1296
1294
|
getTagsWithProperty(propName) {
|
|
1297
|
-
return
|
|
1295
|
+
return this.entries.filter(([, entry]) => entry[propName]).map(([tagName]) => tagName);
|
|
1298
1296
|
}
|
|
1299
1297
|
/**
|
|
1300
1298
|
* Find tag matching tagName or inheriting from it.
|
|
@@ -1302,10 +1300,10 @@ class MetaTable {
|
|
|
1302
1300
|
* @public
|
|
1303
1301
|
*/
|
|
1304
1302
|
getTagsDerivedFrom(tagName) {
|
|
1305
|
-
return
|
|
1303
|
+
return this.entries.filter(([key, entry]) => key === tagName || entry.inherit === tagName).map(([tagName2]) => tagName2);
|
|
1306
1304
|
}
|
|
1307
1305
|
addEntry(tagName, entry) {
|
|
1308
|
-
let parent = this.elements[tagName]
|
|
1306
|
+
let parent = this.elements[tagName];
|
|
1309
1307
|
if (entry.inherit) {
|
|
1310
1308
|
const name = entry.inherit;
|
|
1311
1309
|
parent = this.elements[name];
|
|
@@ -1316,7 +1314,7 @@ class MetaTable {
|
|
|
1316
1314
|
});
|
|
1317
1315
|
}
|
|
1318
1316
|
}
|
|
1319
|
-
const expanded = this.mergeElement(parent, { ...entry, tagName });
|
|
1317
|
+
const expanded = this.mergeElement(parent ?? {}, { ...entry, tagName });
|
|
1320
1318
|
expandRegex(expanded);
|
|
1321
1319
|
this.elements[tagName] = expanded;
|
|
1322
1320
|
}
|
|
@@ -1345,6 +1343,12 @@ class MetaTable {
|
|
|
1345
1343
|
getJSONSchema() {
|
|
1346
1344
|
return this.schema;
|
|
1347
1345
|
}
|
|
1346
|
+
/**
|
|
1347
|
+
* @internal
|
|
1348
|
+
*/
|
|
1349
|
+
get entries() {
|
|
1350
|
+
return Object.entries(this.elements);
|
|
1351
|
+
}
|
|
1348
1352
|
/**
|
|
1349
1353
|
* Finds the global element definition and merges each known element with the
|
|
1350
1354
|
* global, e.g. to assign global attributes.
|
|
@@ -1356,7 +1360,7 @@ class MetaTable {
|
|
|
1356
1360
|
delete this.elements["*"];
|
|
1357
1361
|
delete global.tagName;
|
|
1358
1362
|
delete global.void;
|
|
1359
|
-
for (const [tagName, entry] of
|
|
1363
|
+
for (const [tagName, entry] of this.entries) {
|
|
1360
1364
|
this.elements[tagName] = this.mergeElement(global, entry);
|
|
1361
1365
|
}
|
|
1362
1366
|
}
|
|
@@ -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:
|
|
@@ -2590,10 +2594,13 @@ class HtmlElement extends DOMNode {
|
|
|
2590
2594
|
*/
|
|
2591
2595
|
setAttribute(key, value, keyLocation, valueLocation, originalAttribute) {
|
|
2592
2596
|
key = key.toLowerCase();
|
|
2593
|
-
|
|
2594
|
-
|
|
2597
|
+
const attr = new Attribute(key, value, keyLocation, valueLocation, originalAttribute);
|
|
2598
|
+
const list = this.attr[key];
|
|
2599
|
+
if (list) {
|
|
2600
|
+
list.push(attr);
|
|
2601
|
+
} else {
|
|
2602
|
+
this.attr[key] = [attr];
|
|
2595
2603
|
}
|
|
2596
|
-
this.attr[key].push(new Attribute(key, value, keyLocation, valueLocation, originalAttribute));
|
|
2597
2604
|
}
|
|
2598
2605
|
/**
|
|
2599
2606
|
* Get parsed tabindex for this element.
|
|
@@ -2650,7 +2657,7 @@ class HtmlElement extends DOMNode {
|
|
|
2650
2657
|
const matches = this.attr[key];
|
|
2651
2658
|
return all ? matches : matches[0];
|
|
2652
2659
|
} else {
|
|
2653
|
-
return null;
|
|
2660
|
+
return all ? [] : null;
|
|
2654
2661
|
}
|
|
2655
2662
|
}
|
|
2656
2663
|
/**
|
|
@@ -2756,20 +2763,6 @@ class HtmlElement extends DOMNode {
|
|
|
2756
2763
|
yield* pattern.match(this);
|
|
2757
2764
|
}
|
|
2758
2765
|
}
|
|
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
2766
|
/**
|
|
2774
2767
|
* Evaluates callbackk on all descendants, returning true if any are true.
|
|
2775
2768
|
*
|
|
@@ -2833,591 +2826,133 @@ function isClosed(endToken, meta) {
|
|
|
2833
2826
|
return closed;
|
|
2834
2827
|
}
|
|
2835
2828
|
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
pushActive(node) {
|
|
2843
|
-
this.active = node;
|
|
2829
|
+
function escape(value) {
|
|
2830
|
+
return JSON.stringify(value);
|
|
2831
|
+
}
|
|
2832
|
+
function format(value, quote = false) {
|
|
2833
|
+
if (value === null) {
|
|
2834
|
+
return "null";
|
|
2844
2835
|
}
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
return;
|
|
2848
|
-
}
|
|
2849
|
-
this.active = this.active.parent ?? this.root;
|
|
2836
|
+
if (typeof value === "number") {
|
|
2837
|
+
return value.toString();
|
|
2850
2838
|
}
|
|
2851
|
-
|
|
2852
|
-
return
|
|
2839
|
+
if (typeof value === "string") {
|
|
2840
|
+
return quote ? escape(value) : value;
|
|
2853
2841
|
}
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
resolveMeta(table) {
|
|
2858
|
-
this.visitDepthFirst((node) => {
|
|
2859
|
-
table.resolve(node);
|
|
2860
|
-
});
|
|
2842
|
+
if (Array.isArray(value)) {
|
|
2843
|
+
const content = value.map((it) => format(it, true)).join(", ");
|
|
2844
|
+
return `[ ${content} ]`;
|
|
2861
2845
|
}
|
|
2862
|
-
|
|
2863
|
-
|
|
2846
|
+
if (typeof value === "object") {
|
|
2847
|
+
const content = Object.entries(value).map(([key, nested]) => `${key}: ${format(nested, true)}`).join(", ");
|
|
2848
|
+
return `{ ${content} }`;
|
|
2864
2849
|
}
|
|
2865
|
-
|
|
2866
|
-
|
|
2850
|
+
return String(value);
|
|
2851
|
+
}
|
|
2852
|
+
function interpolate(text, data) {
|
|
2853
|
+
return text.replace(/{{\s*([^\s{}]+)\s*}}/g, (match, key) => {
|
|
2854
|
+
return typeof data[key] !== "undefined" ? format(data[key]) : match;
|
|
2855
|
+
});
|
|
2856
|
+
}
|
|
2857
|
+
|
|
2858
|
+
function isThenable(value) {
|
|
2859
|
+
return value && typeof value === "object" && "then" in value && typeof value.then === "function";
|
|
2860
|
+
}
|
|
2861
|
+
|
|
2862
|
+
var Severity = /* @__PURE__ */ ((Severity2) => {
|
|
2863
|
+
Severity2[Severity2["DISABLED"] = 0] = "DISABLED";
|
|
2864
|
+
Severity2[Severity2["WARN"] = 1] = "WARN";
|
|
2865
|
+
Severity2[Severity2["ERROR"] = 2] = "ERROR";
|
|
2866
|
+
return Severity2;
|
|
2867
|
+
})(Severity || {});
|
|
2868
|
+
function parseSeverity(value) {
|
|
2869
|
+
switch (value) {
|
|
2870
|
+
case 0:
|
|
2871
|
+
case "off":
|
|
2872
|
+
return 0 /* DISABLED */;
|
|
2873
|
+
case 1:
|
|
2874
|
+
case "warn":
|
|
2875
|
+
return 1 /* WARN */;
|
|
2876
|
+
case 2:
|
|
2877
|
+
case "error":
|
|
2878
|
+
return 2 /* ERROR */;
|
|
2879
|
+
default:
|
|
2880
|
+
throw new Error(`Invalid severity "${String(value)}"`);
|
|
2867
2881
|
}
|
|
2868
|
-
|
|
2869
|
-
|
|
2882
|
+
}
|
|
2883
|
+
|
|
2884
|
+
const cacheKey = Symbol("aria-naming");
|
|
2885
|
+
const defaultValue = "allowed";
|
|
2886
|
+
const prohibitedRoles = [
|
|
2887
|
+
"caption",
|
|
2888
|
+
"code",
|
|
2889
|
+
"deletion",
|
|
2890
|
+
"emphasis",
|
|
2891
|
+
"generic",
|
|
2892
|
+
"insertion",
|
|
2893
|
+
"paragraph",
|
|
2894
|
+
"presentation",
|
|
2895
|
+
"strong",
|
|
2896
|
+
"subscript",
|
|
2897
|
+
"superscript"
|
|
2898
|
+
];
|
|
2899
|
+
function byRole(role) {
|
|
2900
|
+
return prohibitedRoles.includes(role) ? "prohibited" : "allowed";
|
|
2901
|
+
}
|
|
2902
|
+
function byMeta(element, meta) {
|
|
2903
|
+
return meta.aria.naming(element._adapter);
|
|
2904
|
+
}
|
|
2905
|
+
function ariaNaming(element) {
|
|
2906
|
+
var _a;
|
|
2907
|
+
const cached = element.cacheGet(cacheKey);
|
|
2908
|
+
if (cached) {
|
|
2909
|
+
return cached;
|
|
2870
2910
|
}
|
|
2871
|
-
|
|
2872
|
-
|
|
2911
|
+
const role = (_a = element.getAttribute("role")) == null ? void 0 : _a.value;
|
|
2912
|
+
if (role) {
|
|
2913
|
+
if (role instanceof DynamicValue) {
|
|
2914
|
+
return element.cacheSet(cacheKey, defaultValue);
|
|
2915
|
+
} else {
|
|
2916
|
+
return element.cacheSet(cacheKey, byRole(role));
|
|
2917
|
+
}
|
|
2873
2918
|
}
|
|
2874
|
-
|
|
2875
|
-
|
|
2919
|
+
const meta = element.meta;
|
|
2920
|
+
if (!meta) {
|
|
2921
|
+
return element.cacheSet(cacheKey, defaultValue);
|
|
2876
2922
|
}
|
|
2923
|
+
return element.cacheSet(cacheKey, byMeta(element, meta));
|
|
2877
2924
|
}
|
|
2878
2925
|
|
|
2879
|
-
const
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
}
|
|
2892
|
-
return rules.some((rule) => {
|
|
2893
|
-
return Validator.validatePermittedRule(node, rule);
|
|
2894
|
-
});
|
|
2926
|
+
const patternCache = /* @__PURE__ */ new Map();
|
|
2927
|
+
function compileStringPattern(pattern) {
|
|
2928
|
+
const regexp = pattern.replace(/[*]+/g, ".+");
|
|
2929
|
+
return new RegExp(`^${regexp}$`);
|
|
2930
|
+
}
|
|
2931
|
+
function compileRegExpPattern(pattern) {
|
|
2932
|
+
return new RegExp(`^${pattern}$`);
|
|
2933
|
+
}
|
|
2934
|
+
function compilePattern(pattern) {
|
|
2935
|
+
const cached = patternCache.get(pattern);
|
|
2936
|
+
if (cached) {
|
|
2937
|
+
return cached;
|
|
2895
2938
|
}
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
* @param rules - List of rules of the parent element.
|
|
2906
|
-
* @returns `true` if the parent element of the children passes the test.
|
|
2907
|
-
*/
|
|
2908
|
-
static validateOccurrences(children, rules, cb) {
|
|
2909
|
-
if (!rules) {
|
|
2939
|
+
const match = pattern.match(/^\/(.*)\/$/);
|
|
2940
|
+
const regexp = match ? compileRegExpPattern(match[1]) : compileStringPattern(pattern);
|
|
2941
|
+
patternCache.set(pattern, regexp);
|
|
2942
|
+
return regexp;
|
|
2943
|
+
}
|
|
2944
|
+
function keywordPatternMatcher(list, keyword) {
|
|
2945
|
+
for (const pattern of list) {
|
|
2946
|
+
const regexp = compilePattern(pattern);
|
|
2947
|
+
if (regexp.test(keyword)) {
|
|
2910
2948
|
return true;
|
|
2911
2949
|
}
|
|
2912
|
-
let valid = true;
|
|
2913
|
-
for (const rule of rules) {
|
|
2914
|
-
if (typeof rule !== "string") {
|
|
2915
|
-
return false;
|
|
2916
|
-
}
|
|
2917
|
-
const [, category, quantifier] = rule.match(/^(@?.*?)([?*]?)$/);
|
|
2918
|
-
const limit = category && quantifier && parseQuantifier(quantifier);
|
|
2919
|
-
if (limit) {
|
|
2920
|
-
const siblings = children.filter(
|
|
2921
|
-
(cur) => Validator.validatePermittedCategory(cur, rule, true)
|
|
2922
|
-
);
|
|
2923
|
-
if (siblings.length > limit) {
|
|
2924
|
-
for (const child of siblings.slice(limit)) {
|
|
2925
|
-
cb(child, category);
|
|
2926
|
-
}
|
|
2927
|
-
valid = false;
|
|
2928
|
-
}
|
|
2929
|
-
}
|
|
2930
|
-
}
|
|
2931
|
-
return valid;
|
|
2932
2950
|
}
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
* specified order.
|
|
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;
|
|
2967
|
-
}
|
|
2968
|
-
return true;
|
|
2969
|
-
}
|
|
2970
|
-
/**
|
|
2971
|
-
* Validate element ancestors.
|
|
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));
|
|
2981
|
-
}
|
|
2982
|
-
/**
|
|
2983
|
-
* Validate element required content.
|
|
2984
|
-
*
|
|
2985
|
-
* Check if an element has the required set of elements. At least one of the
|
|
2986
|
-
* selectors must match.
|
|
2987
|
-
*
|
|
2988
|
-
* Returns `[]` when valid or a list of required but missing tagnames or
|
|
2989
|
-
* categories.
|
|
2990
|
-
*/
|
|
2991
|
-
static validateRequiredContent(node, rules) {
|
|
2992
|
-
if (!rules || rules.length === 0) {
|
|
2993
|
-
return [];
|
|
2994
|
-
}
|
|
2995
|
-
return rules.filter((tagName) => {
|
|
2996
|
-
const haveMatchingChild = node.childElements.some(
|
|
2997
|
-
(child) => Validator.validatePermittedCategory(child, tagName, false)
|
|
2998
|
-
);
|
|
2999
|
-
return !haveMatchingChild;
|
|
3000
|
-
});
|
|
3001
|
-
}
|
|
3002
|
-
/**
|
|
3003
|
-
* Test if an attribute has an allowed value and/or format.
|
|
3004
|
-
*
|
|
3005
|
-
* @param attr - Attribute to test.
|
|
3006
|
-
* @param rules - Element attribute metadta.
|
|
3007
|
-
* @returns `true` if attribute passes all tests.
|
|
3008
|
-
*/
|
|
3009
|
-
static validateAttribute(attr, rules) {
|
|
3010
|
-
const rule = rules[attr.key];
|
|
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) {
|
|
3023
|
-
return true;
|
|
3024
|
-
}
|
|
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
|
-
}
|
|
3033
|
-
static validateAttributeValue(value, rule) {
|
|
3034
|
-
if (!rule.enum) {
|
|
3035
|
-
return true;
|
|
3036
|
-
}
|
|
3037
|
-
if (value === null || value === void 0) {
|
|
3038
|
-
return false;
|
|
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
|
-
});
|
|
3048
|
-
}
|
|
3049
|
-
static validatePermittedRule(node, rule, isExclude = false) {
|
|
3050
|
-
if (typeof rule === "string") {
|
|
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
|
-
}
|
|
3070
|
-
}
|
|
3071
|
-
/**
|
|
3072
|
-
* Validate node against a content category.
|
|
3073
|
-
*
|
|
3074
|
-
* When matching parent nodes against permitted parents use the superset
|
|
3075
|
-
* parameter to also match for `@flow`. E.g. if a node expects a `@phrasing`
|
|
3076
|
-
* parent it should also allow `@flow` parent since `@phrasing` is a subset of
|
|
3077
|
-
* `@flow`.
|
|
3078
|
-
*
|
|
3079
|
-
* @param node - The node to test against
|
|
3080
|
-
* @param category - Name of category with `@` prefix or tag name.
|
|
3081
|
-
* @param defaultMatch - The default return value when node categories is not known.
|
|
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
|
-
}
|
|
3114
|
-
}
|
|
3115
|
-
}
|
|
3116
|
-
function validateKeys(rule) {
|
|
3117
|
-
for (const key of Object.keys(rule)) {
|
|
3118
|
-
if (!allowedKeys.includes(key)) {
|
|
3119
|
-
const str = JSON.stringify(rule);
|
|
3120
|
-
throw new Error(`Permitted rule "${str}" contains unknown property "${key}"`);
|
|
3121
|
-
}
|
|
3122
|
-
}
|
|
3123
|
-
}
|
|
3124
|
-
function parseQuantifier(quantifier) {
|
|
3125
|
-
switch (quantifier) {
|
|
3126
|
-
case "?":
|
|
3127
|
-
return 1;
|
|
3128
|
-
case "*":
|
|
3129
|
-
return null;
|
|
3130
|
-
default:
|
|
3131
|
-
throw new Error(`Invalid quantifier "${quantifier}" used`);
|
|
3132
|
-
}
|
|
3133
|
-
}
|
|
3134
|
-
|
|
3135
|
-
const $schema = "http://json-schema.org/draft-06/schema#";
|
|
3136
|
-
const $id = "https://html-validate.org/schemas/config.json";
|
|
3137
|
-
const type = "object";
|
|
3138
|
-
const additionalProperties = false;
|
|
3139
|
-
const properties = {
|
|
3140
|
-
$schema: {
|
|
3141
|
-
type: "string"
|
|
3142
|
-
},
|
|
3143
|
-
root: {
|
|
3144
|
-
type: "boolean",
|
|
3145
|
-
title: "Mark as root configuration",
|
|
3146
|
-
description: "If this is set to true no further configurations will be searched.",
|
|
3147
|
-
"default": false
|
|
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)}"`);
|
|
3317
|
-
}
|
|
3318
|
-
}
|
|
3319
|
-
|
|
3320
|
-
function escape(value) {
|
|
3321
|
-
return JSON.stringify(value);
|
|
3322
|
-
}
|
|
3323
|
-
function format(value, quote = false) {
|
|
3324
|
-
if (value === null) {
|
|
3325
|
-
return "null";
|
|
3326
|
-
}
|
|
3327
|
-
if (typeof value === "number") {
|
|
3328
|
-
return value.toString();
|
|
3329
|
-
}
|
|
3330
|
-
if (typeof value === "string") {
|
|
3331
|
-
return quote ? escape(value) : value;
|
|
3332
|
-
}
|
|
3333
|
-
if (Array.isArray(value)) {
|
|
3334
|
-
const content = value.map((it) => format(it, true)).join(", ");
|
|
3335
|
-
return `[ ${content} ]`;
|
|
3336
|
-
}
|
|
3337
|
-
if (typeof value === "object") {
|
|
3338
|
-
const content = Object.entries(value).map(([key, nested]) => `${key}: ${format(nested, true)}`).join(", ");
|
|
3339
|
-
return `{ ${content} }`;
|
|
3340
|
-
}
|
|
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
|
-
}
|
|
3348
|
-
|
|
3349
|
-
const cacheKey = Symbol("aria-naming");
|
|
3350
|
-
const defaultValue = "allowed";
|
|
3351
|
-
const prohibitedRoles = [
|
|
3352
|
-
"caption",
|
|
3353
|
-
"code",
|
|
3354
|
-
"deletion",
|
|
3355
|
-
"emphasis",
|
|
3356
|
-
"generic",
|
|
3357
|
-
"insertion",
|
|
3358
|
-
"paragraph",
|
|
3359
|
-
"presentation",
|
|
3360
|
-
"strong",
|
|
3361
|
-
"subscript",
|
|
3362
|
-
"superscript"
|
|
3363
|
-
];
|
|
3364
|
-
function byRole(role) {
|
|
3365
|
-
return prohibitedRoles.includes(role) ? "prohibited" : "allowed";
|
|
3366
|
-
}
|
|
3367
|
-
function byMeta(element, meta) {
|
|
3368
|
-
return meta.aria.naming(element._adapter);
|
|
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)) {
|
|
2951
|
+
return false;
|
|
2952
|
+
}
|
|
2953
|
+
function isKeywordIgnored(options, keyword, matcher = (list, it) => list.includes(it)) {
|
|
2954
|
+
const { include, exclude } = options;
|
|
2955
|
+
if (include && !matcher(include, keyword)) {
|
|
3421
2956
|
return true;
|
|
3422
2957
|
}
|
|
3423
2958
|
if (exclude && matcher(exclude, keyword)) {
|
|
@@ -5022,7 +4557,7 @@ class AttributeAllowedValues extends Rule {
|
|
|
5022
4557
|
setup() {
|
|
5023
4558
|
this.on("dom:ready", (event) => {
|
|
5024
4559
|
const doc = event.document;
|
|
5025
|
-
|
|
4560
|
+
walk.depthFirst(doc, (node) => {
|
|
5026
4561
|
const meta = node.meta;
|
|
5027
4562
|
if (!(meta == null ? void 0 : meta.attributes))
|
|
5028
4563
|
return;
|
|
@@ -5053,11 +4588,7 @@ class AttributeAllowedValues extends Rule {
|
|
|
5053
4588
|
}
|
|
5054
4589
|
}
|
|
5055
4590
|
getLocation(attr) {
|
|
5056
|
-
|
|
5057
|
-
return attr.valueLocation;
|
|
5058
|
-
} else {
|
|
5059
|
-
return attr.keyLocation;
|
|
5060
|
-
}
|
|
4591
|
+
return attr.valueLocation ?? attr.keyLocation;
|
|
5061
4592
|
}
|
|
5062
4593
|
}
|
|
5063
4594
|
|
|
@@ -5086,7 +4617,7 @@ class AttributeBooleanStyle extends Rule {
|
|
|
5086
4617
|
setup() {
|
|
5087
4618
|
this.on("dom:ready", (event) => {
|
|
5088
4619
|
const doc = event.document;
|
|
5089
|
-
|
|
4620
|
+
walk.depthFirst(doc, (node) => {
|
|
5090
4621
|
const meta = node.meta;
|
|
5091
4622
|
if (!(meta == null ? void 0 : meta.attributes))
|
|
5092
4623
|
return;
|
|
@@ -5158,7 +4689,7 @@ class AttributeEmptyStyle extends Rule {
|
|
|
5158
4689
|
setup() {
|
|
5159
4690
|
this.on("dom:ready", (event) => {
|
|
5160
4691
|
const doc = event.document;
|
|
5161
|
-
|
|
4692
|
+
walk.depthFirst(doc, (node) => {
|
|
5162
4693
|
const meta = node.meta;
|
|
5163
4694
|
if (!(meta == null ? void 0 : meta.attributes))
|
|
5164
4695
|
return;
|
|
@@ -5810,7 +5341,7 @@ class ElementPermittedContent extends Rule {
|
|
|
5810
5341
|
setup() {
|
|
5811
5342
|
this.on("dom:ready", (event) => {
|
|
5812
5343
|
const doc = event.document;
|
|
5813
|
-
|
|
5344
|
+
walk.depthFirst(doc, (node) => {
|
|
5814
5345
|
const parent = node.parent;
|
|
5815
5346
|
if (!parent) {
|
|
5816
5347
|
return;
|
|
@@ -5889,7 +5420,7 @@ class ElementPermittedOccurrences extends Rule {
|
|
|
5889
5420
|
setup() {
|
|
5890
5421
|
this.on("dom:ready", (event) => {
|
|
5891
5422
|
const doc = event.document;
|
|
5892
|
-
|
|
5423
|
+
walk.depthFirst(doc, (node) => {
|
|
5893
5424
|
if (!node.meta) {
|
|
5894
5425
|
return;
|
|
5895
5426
|
}
|
|
@@ -5922,7 +5453,7 @@ class ElementPermittedOrder extends Rule {
|
|
|
5922
5453
|
setup() {
|
|
5923
5454
|
this.on("dom:ready", (event) => {
|
|
5924
5455
|
const doc = event.document;
|
|
5925
|
-
|
|
5456
|
+
walk.depthFirst(doc, (node) => {
|
|
5926
5457
|
if (!node.meta) {
|
|
5927
5458
|
return;
|
|
5928
5459
|
}
|
|
@@ -5992,7 +5523,7 @@ class ElementPermittedParent extends Rule {
|
|
|
5992
5523
|
setup() {
|
|
5993
5524
|
this.on("dom:ready", (event) => {
|
|
5994
5525
|
const doc = event.document;
|
|
5995
|
-
|
|
5526
|
+
walk.depthFirst(doc, (node) => {
|
|
5996
5527
|
var _a;
|
|
5997
5528
|
const parent = node.parent;
|
|
5998
5529
|
if (!parent) {
|
|
@@ -6040,7 +5571,7 @@ class ElementRequiredAncestor extends Rule {
|
|
|
6040
5571
|
setup() {
|
|
6041
5572
|
this.on("dom:ready", (event) => {
|
|
6042
5573
|
const doc = event.document;
|
|
6043
|
-
|
|
5574
|
+
walk.depthFirst(doc, (node) => {
|
|
6044
5575
|
const parent = node.parent;
|
|
6045
5576
|
if (!parent) {
|
|
6046
5577
|
return;
|
|
@@ -6124,7 +5655,7 @@ class ElementRequiredContent extends Rule {
|
|
|
6124
5655
|
setup() {
|
|
6125
5656
|
this.on("dom:ready", (event) => {
|
|
6126
5657
|
const doc = event.document;
|
|
6127
|
-
|
|
5658
|
+
walk.depthFirst(doc, (node) => {
|
|
6128
5659
|
if (!node.meta) {
|
|
6129
5660
|
return;
|
|
6130
5661
|
}
|
|
@@ -6644,7 +6175,7 @@ function isFocusableImpl(element) {
|
|
|
6644
6175
|
if (isDisabled(element, meta)) {
|
|
6645
6176
|
return false;
|
|
6646
6177
|
}
|
|
6647
|
-
return Boolean(meta
|
|
6178
|
+
return Boolean(meta.focusable);
|
|
6648
6179
|
}
|
|
6649
6180
|
function isFocusable(element) {
|
|
6650
6181
|
const cached = element.cacheGet(FOCUSABLE_CACHE);
|
|
@@ -8554,1778 +8085,2291 @@ class RequireSri extends Rule {
|
|
|
8554
8085
|
items: {
|
|
8555
8086
|
type: "string"
|
|
8556
8087
|
},
|
|
8557
|
-
type: "array"
|
|
8558
|
-
},
|
|
8559
|
-
{
|
|
8560
|
-
type: "null"
|
|
8561
|
-
}
|
|
8562
|
-
]
|
|
8563
|
-
},
|
|
8564
|
-
exclude: {
|
|
8565
|
-
anyOf: [
|
|
8566
|
-
{
|
|
8567
|
-
items: {
|
|
8088
|
+
type: "array"
|
|
8089
|
+
},
|
|
8090
|
+
{
|
|
8091
|
+
type: "null"
|
|
8092
|
+
}
|
|
8093
|
+
]
|
|
8094
|
+
},
|
|
8095
|
+
exclude: {
|
|
8096
|
+
anyOf: [
|
|
8097
|
+
{
|
|
8098
|
+
items: {
|
|
8099
|
+
type: "string"
|
|
8100
|
+
},
|
|
8101
|
+
type: "array"
|
|
8102
|
+
},
|
|
8103
|
+
{
|
|
8104
|
+
type: "null"
|
|
8105
|
+
}
|
|
8106
|
+
]
|
|
8107
|
+
}
|
|
8108
|
+
};
|
|
8109
|
+
}
|
|
8110
|
+
documentation() {
|
|
8111
|
+
return {
|
|
8112
|
+
description: `Subresource Integrity (SRI) \`integrity\` attribute is required to prevent tampering or manipulation from Content Delivery Networks (CDN), rouge proxies, malicious entities, etc.`,
|
|
8113
|
+
url: "https://html-validate.org/rules/require-sri.html"
|
|
8114
|
+
};
|
|
8115
|
+
}
|
|
8116
|
+
setup() {
|
|
8117
|
+
this.on("tag:end", (event) => {
|
|
8118
|
+
const node = event.previous;
|
|
8119
|
+
if (!(this.supportSri(node) && this.needSri(node))) {
|
|
8120
|
+
return;
|
|
8121
|
+
}
|
|
8122
|
+
if (node.hasAttribute("integrity")) {
|
|
8123
|
+
return;
|
|
8124
|
+
}
|
|
8125
|
+
this.report(
|
|
8126
|
+
node,
|
|
8127
|
+
`SRI "integrity" attribute is required on <${node.tagName}> element`,
|
|
8128
|
+
node.location
|
|
8129
|
+
);
|
|
8130
|
+
});
|
|
8131
|
+
}
|
|
8132
|
+
supportSri(node) {
|
|
8133
|
+
return Object.keys(supportSri).includes(node.tagName);
|
|
8134
|
+
}
|
|
8135
|
+
needSri(node) {
|
|
8136
|
+
const attr = this.elementSourceAttr(node);
|
|
8137
|
+
if (!attr) {
|
|
8138
|
+
return false;
|
|
8139
|
+
}
|
|
8140
|
+
if (attr.value === null || attr.value === "" || attr.isDynamic) {
|
|
8141
|
+
return false;
|
|
8142
|
+
}
|
|
8143
|
+
const url = attr.value.toString();
|
|
8144
|
+
if (this.target === "all" || crossorigin.test(url)) {
|
|
8145
|
+
return !this.isIgnored(url);
|
|
8146
|
+
}
|
|
8147
|
+
return false;
|
|
8148
|
+
}
|
|
8149
|
+
elementSourceAttr(node) {
|
|
8150
|
+
const key = supportSri[node.tagName];
|
|
8151
|
+
return node.getAttribute(key);
|
|
8152
|
+
}
|
|
8153
|
+
isIgnored(url) {
|
|
8154
|
+
return this.isKeywordIgnored(url, (list, it) => {
|
|
8155
|
+
return list.some((pattern) => it.includes(pattern));
|
|
8156
|
+
});
|
|
8157
|
+
}
|
|
8158
|
+
}
|
|
8159
|
+
|
|
8160
|
+
class ScriptElement extends Rule {
|
|
8161
|
+
documentation() {
|
|
8162
|
+
return {
|
|
8163
|
+
description: "The end tag for `<script>` is a hard requirement and must never be omitted even when using the `src` attribute.",
|
|
8164
|
+
url: "https://html-validate.org/rules/script-element.html"
|
|
8165
|
+
};
|
|
8166
|
+
}
|
|
8167
|
+
setup() {
|
|
8168
|
+
this.on("tag:end", (event) => {
|
|
8169
|
+
const node = event.target;
|
|
8170
|
+
if (!node || node.tagName !== "script") {
|
|
8171
|
+
return;
|
|
8172
|
+
}
|
|
8173
|
+
if (node.closed !== NodeClosed.EndTag) {
|
|
8174
|
+
this.report(node, `End tag for <${node.tagName}> must not be omitted`);
|
|
8175
|
+
}
|
|
8176
|
+
});
|
|
8177
|
+
}
|
|
8178
|
+
}
|
|
8179
|
+
|
|
8180
|
+
const javascript = [
|
|
8181
|
+
"",
|
|
8182
|
+
"application/ecmascript",
|
|
8183
|
+
"application/javascript",
|
|
8184
|
+
"text/ecmascript",
|
|
8185
|
+
"text/javascript"
|
|
8186
|
+
];
|
|
8187
|
+
class ScriptType extends Rule {
|
|
8188
|
+
documentation() {
|
|
8189
|
+
return {
|
|
8190
|
+
description: "While valid the HTML5 standard encourages authors to omit the type element for JavaScript resources.",
|
|
8191
|
+
url: "https://html-validate.org/rules/script-type.html"
|
|
8192
|
+
};
|
|
8193
|
+
}
|
|
8194
|
+
setup() {
|
|
8195
|
+
this.on("tag:end", (event) => {
|
|
8196
|
+
const node = event.previous;
|
|
8197
|
+
if (node.tagName !== "script") {
|
|
8198
|
+
return;
|
|
8199
|
+
}
|
|
8200
|
+
const attr = node.getAttribute("type");
|
|
8201
|
+
if (!attr || attr.isDynamic) {
|
|
8202
|
+
return;
|
|
8203
|
+
}
|
|
8204
|
+
const value = attr.value ? attr.value.toString() : "";
|
|
8205
|
+
if (!this.isJavascript(value)) {
|
|
8206
|
+
return;
|
|
8207
|
+
}
|
|
8208
|
+
this.report(
|
|
8209
|
+
node,
|
|
8210
|
+
'"type" attribute is unnecessary for javascript resources',
|
|
8211
|
+
attr.keyLocation
|
|
8212
|
+
);
|
|
8213
|
+
});
|
|
8214
|
+
}
|
|
8215
|
+
isJavascript(mime) {
|
|
8216
|
+
const type = mime.replace(/;.*/, "");
|
|
8217
|
+
return javascript.includes(type);
|
|
8218
|
+
}
|
|
8219
|
+
}
|
|
8220
|
+
|
|
8221
|
+
class SvgFocusable extends Rule {
|
|
8222
|
+
documentation() {
|
|
8223
|
+
return {
|
|
8224
|
+
description: `Inline SVG elements in IE are focusable by default which may cause issues with tab-ordering. The \`focusable\` attribute should explicitly be set to avoid unintended behaviour.`,
|
|
8225
|
+
url: "https://html-validate.org/rules/svg-focusable.html"
|
|
8226
|
+
};
|
|
8227
|
+
}
|
|
8228
|
+
setup() {
|
|
8229
|
+
this.on("element:ready", (event) => {
|
|
8230
|
+
if (event.target.is("svg")) {
|
|
8231
|
+
this.validate(event.target);
|
|
8232
|
+
}
|
|
8233
|
+
});
|
|
8234
|
+
}
|
|
8235
|
+
validate(svg) {
|
|
8236
|
+
if (svg.hasAttribute("focusable")) {
|
|
8237
|
+
return;
|
|
8238
|
+
}
|
|
8239
|
+
this.report(svg, `<${svg.tagName}> is missing required "focusable" attribute`);
|
|
8240
|
+
}
|
|
8241
|
+
}
|
|
8242
|
+
|
|
8243
|
+
const defaults$5 = {
|
|
8244
|
+
characters: [
|
|
8245
|
+
{ pattern: " ", replacement: " ", description: "non-breaking space" },
|
|
8246
|
+
{ pattern: "-", replacement: "‑", description: "non-breaking hyphen" }
|
|
8247
|
+
],
|
|
8248
|
+
ignoreClasses: [],
|
|
8249
|
+
ignoreStyle: true
|
|
8250
|
+
};
|
|
8251
|
+
function constructRegex(characters) {
|
|
8252
|
+
const disallowed = characters.map((it) => {
|
|
8253
|
+
return it.pattern;
|
|
8254
|
+
}).join("|");
|
|
8255
|
+
const pattern = `(${disallowed})`;
|
|
8256
|
+
return new RegExp(pattern, "g");
|
|
8257
|
+
}
|
|
8258
|
+
function getText(node) {
|
|
8259
|
+
const match = node.textContent.match(/^(\s*)(.*)$/);
|
|
8260
|
+
const [, leading, text] = match;
|
|
8261
|
+
return [leading.length, text.trimEnd()];
|
|
8262
|
+
}
|
|
8263
|
+
function matchAll(text, regexp) {
|
|
8264
|
+
const copy = new RegExp(regexp);
|
|
8265
|
+
const matches = [];
|
|
8266
|
+
let match;
|
|
8267
|
+
while (match = copy.exec(text)) {
|
|
8268
|
+
matches.push(match);
|
|
8269
|
+
}
|
|
8270
|
+
return matches;
|
|
8271
|
+
}
|
|
8272
|
+
class TelNonBreaking extends Rule {
|
|
8273
|
+
constructor(options) {
|
|
8274
|
+
super({ ...defaults$5, ...options });
|
|
8275
|
+
this.regex = constructRegex(this.options.characters);
|
|
8276
|
+
}
|
|
8277
|
+
static schema() {
|
|
8278
|
+
return {
|
|
8279
|
+
characters: {
|
|
8280
|
+
type: "array",
|
|
8281
|
+
items: {
|
|
8282
|
+
type: "object",
|
|
8283
|
+
additionalProperties: false,
|
|
8284
|
+
properties: {
|
|
8285
|
+
pattern: {
|
|
8286
|
+
type: "string"
|
|
8287
|
+
},
|
|
8288
|
+
replacement: {
|
|
8568
8289
|
type: "string"
|
|
8569
8290
|
},
|
|
8570
|
-
|
|
8571
|
-
|
|
8572
|
-
|
|
8573
|
-
type: "null"
|
|
8291
|
+
description: {
|
|
8292
|
+
type: "string"
|
|
8293
|
+
}
|
|
8574
8294
|
}
|
|
8575
|
-
|
|
8295
|
+
}
|
|
8296
|
+
},
|
|
8297
|
+
ignoreClasses: {
|
|
8298
|
+
type: "array",
|
|
8299
|
+
items: {
|
|
8300
|
+
type: "string"
|
|
8301
|
+
}
|
|
8302
|
+
},
|
|
8303
|
+
ignoreStyle: {
|
|
8304
|
+
type: "boolean"
|
|
8576
8305
|
}
|
|
8577
8306
|
};
|
|
8578
8307
|
}
|
|
8579
|
-
documentation() {
|
|
8308
|
+
documentation(context) {
|
|
8309
|
+
const { characters } = this.options;
|
|
8310
|
+
const replacements = characters.map((it) => {
|
|
8311
|
+
return ` - \`${it.pattern}\` - replace with \`${it.replacement}\` (${it.description}).`;
|
|
8312
|
+
});
|
|
8580
8313
|
return {
|
|
8581
|
-
description:
|
|
8582
|
-
|
|
8314
|
+
description: [
|
|
8315
|
+
`The \`${context.pattern}\` character should be replaced with \`${context.replacement}\` character (${context.description}) when used in a telephone number.`,
|
|
8316
|
+
"",
|
|
8317
|
+
"Unless non-breaking characters is used there could be a line break inserted at that character.",
|
|
8318
|
+
"Line breaks make is harder to read and understand the telephone number.",
|
|
8319
|
+
"",
|
|
8320
|
+
"The following characters should be avoided:",
|
|
8321
|
+
"",
|
|
8322
|
+
...replacements
|
|
8323
|
+
].join("\n"),
|
|
8324
|
+
url: "https://html-validate.org/rules/tel-non-breaking.html"
|
|
8583
8325
|
};
|
|
8584
8326
|
}
|
|
8585
8327
|
setup() {
|
|
8586
|
-
this.on("
|
|
8587
|
-
const
|
|
8588
|
-
if (
|
|
8589
|
-
return;
|
|
8590
|
-
}
|
|
8591
|
-
if (node.hasAttribute("integrity")) {
|
|
8328
|
+
this.on("element:ready", this.isRelevant, (event) => {
|
|
8329
|
+
const { target } = event;
|
|
8330
|
+
if (this.isIgnored(target)) {
|
|
8592
8331
|
return;
|
|
8593
8332
|
}
|
|
8594
|
-
this.
|
|
8595
|
-
node,
|
|
8596
|
-
`SRI "integrity" attribute is required on <${node.tagName}> element`,
|
|
8597
|
-
node.location
|
|
8598
|
-
);
|
|
8333
|
+
this.walk(target, target);
|
|
8599
8334
|
});
|
|
8600
8335
|
}
|
|
8601
|
-
|
|
8602
|
-
|
|
8603
|
-
|
|
8604
|
-
needSri(node) {
|
|
8605
|
-
const attr = this.elementSourceAttr(node);
|
|
8606
|
-
if (!attr) {
|
|
8336
|
+
isRelevant(event) {
|
|
8337
|
+
const { target } = event;
|
|
8338
|
+
if (!target.is("a")) {
|
|
8607
8339
|
return false;
|
|
8608
8340
|
}
|
|
8609
|
-
|
|
8341
|
+
const attr = target.getAttribute("href");
|
|
8342
|
+
if (!(attr == null ? void 0 : attr.valueMatches(/^tel:/, false))) {
|
|
8610
8343
|
return false;
|
|
8611
8344
|
}
|
|
8612
|
-
|
|
8613
|
-
|
|
8614
|
-
|
|
8345
|
+
return true;
|
|
8346
|
+
}
|
|
8347
|
+
isIgnoredClass(node) {
|
|
8348
|
+
const { ignoreClasses } = this.options;
|
|
8349
|
+
const { classList } = node;
|
|
8350
|
+
return ignoreClasses.some((it) => classList.contains(it));
|
|
8351
|
+
}
|
|
8352
|
+
isIgnoredStyle(node) {
|
|
8353
|
+
const { ignoreStyle } = this.options;
|
|
8354
|
+
const { style } = node;
|
|
8355
|
+
if (!ignoreStyle) {
|
|
8356
|
+
return false;
|
|
8357
|
+
}
|
|
8358
|
+
if (style["white-space"] === "nowrap" || style["white-space"] === "pre") {
|
|
8359
|
+
return true;
|
|
8615
8360
|
}
|
|
8616
8361
|
return false;
|
|
8617
8362
|
}
|
|
8618
|
-
|
|
8619
|
-
|
|
8620
|
-
|
|
8363
|
+
isIgnored(node) {
|
|
8364
|
+
return this.isIgnoredClass(node) || this.isIgnoredStyle(node);
|
|
8365
|
+
}
|
|
8366
|
+
walk(anchor, node) {
|
|
8367
|
+
for (const child of node.childNodes) {
|
|
8368
|
+
if (isTextNode(child)) {
|
|
8369
|
+
this.detectDisallowed(anchor, child);
|
|
8370
|
+
} else if (isElementNode(child)) {
|
|
8371
|
+
this.walk(anchor, child);
|
|
8372
|
+
}
|
|
8373
|
+
}
|
|
8374
|
+
}
|
|
8375
|
+
detectDisallowed(anchor, node) {
|
|
8376
|
+
const [offset, text] = getText(node);
|
|
8377
|
+
const matches = matchAll(text, this.regex);
|
|
8378
|
+
for (const match of matches) {
|
|
8379
|
+
const detected = match[0];
|
|
8380
|
+
const entry = this.options.characters.find((it) => it.pattern === detected);
|
|
8381
|
+
if (!entry) {
|
|
8382
|
+
throw new Error(`Failed to find entry for "${detected}" when searching text "${text}"`);
|
|
8383
|
+
}
|
|
8384
|
+
const message = `"${detected}" should be replaced with "${entry.replacement}" (${entry.description}) in telephone number`;
|
|
8385
|
+
const begin = offset + match.index;
|
|
8386
|
+
const end = begin + detected.length;
|
|
8387
|
+
const location = sliceLocation(node.location, begin, end);
|
|
8388
|
+
const context = entry;
|
|
8389
|
+
this.report(anchor, message, location, context);
|
|
8390
|
+
}
|
|
8391
|
+
}
|
|
8392
|
+
}
|
|
8393
|
+
|
|
8394
|
+
function hasNonEmptyAttribute(node, key) {
|
|
8395
|
+
const attr = node.getAttribute(key);
|
|
8396
|
+
return Boolean(attr == null ? void 0 : attr.valueMatches(/.+/, true));
|
|
8397
|
+
}
|
|
8398
|
+
function hasDefaultText(node) {
|
|
8399
|
+
if (!node.is("input")) {
|
|
8400
|
+
return false;
|
|
8401
|
+
}
|
|
8402
|
+
if (node.hasAttribute("value")) {
|
|
8403
|
+
return false;
|
|
8404
|
+
}
|
|
8405
|
+
const type = node.getAttribute("type");
|
|
8406
|
+
return Boolean(type == null ? void 0 : type.valueMatches(/submit|reset/, false));
|
|
8407
|
+
}
|
|
8408
|
+
function isNonEmptyText(node) {
|
|
8409
|
+
if (isTextNode(node)) {
|
|
8410
|
+
return node.isDynamic || node.textContent.trim() !== "";
|
|
8411
|
+
} else {
|
|
8412
|
+
return false;
|
|
8413
|
+
}
|
|
8414
|
+
}
|
|
8415
|
+
function haveAccessibleText(node) {
|
|
8416
|
+
if (!inAccessibilityTree(node)) {
|
|
8417
|
+
return false;
|
|
8418
|
+
}
|
|
8419
|
+
const haveText = node.childNodes.some((child) => isNonEmptyText(child));
|
|
8420
|
+
if (haveText) {
|
|
8421
|
+
return true;
|
|
8422
|
+
}
|
|
8423
|
+
if (hasNonEmptyAttribute(node, "aria-label")) {
|
|
8424
|
+
return true;
|
|
8425
|
+
}
|
|
8426
|
+
if (hasNonEmptyAttribute(node, "aria-labelledby")) {
|
|
8427
|
+
return true;
|
|
8428
|
+
}
|
|
8429
|
+
if (node.is("img") && hasNonEmptyAttribute(node, "alt")) {
|
|
8430
|
+
return true;
|
|
8431
|
+
}
|
|
8432
|
+
if (hasDefaultText(node)) {
|
|
8433
|
+
return true;
|
|
8434
|
+
}
|
|
8435
|
+
return node.childElements.some((child) => {
|
|
8436
|
+
return haveAccessibleText(child);
|
|
8437
|
+
});
|
|
8438
|
+
}
|
|
8439
|
+
class TextContent extends Rule {
|
|
8440
|
+
documentation(context) {
|
|
8441
|
+
const doc = {
|
|
8442
|
+
description: `The textual content for this element is not valid.`,
|
|
8443
|
+
url: "https://html-validate.org/rules/text-content.html"
|
|
8444
|
+
};
|
|
8445
|
+
switch (context.textContent) {
|
|
8446
|
+
case TextContent$1.NONE:
|
|
8447
|
+
doc.description = `The \`<${context.tagName}>\` element must not have textual content.`;
|
|
8448
|
+
break;
|
|
8449
|
+
case TextContent$1.REQUIRED:
|
|
8450
|
+
doc.description = `The \`<${context.tagName}>\` element must have textual content.`;
|
|
8451
|
+
break;
|
|
8452
|
+
case TextContent$1.ACCESSIBLE:
|
|
8453
|
+
doc.description = `The \`<${context.tagName}>\` element must have accessible text.`;
|
|
8454
|
+
break;
|
|
8455
|
+
}
|
|
8456
|
+
return doc;
|
|
8457
|
+
}
|
|
8458
|
+
static filter(event) {
|
|
8459
|
+
const { target } = event;
|
|
8460
|
+
if (!target.meta) {
|
|
8461
|
+
return false;
|
|
8462
|
+
}
|
|
8463
|
+
const { textContent } = target.meta;
|
|
8464
|
+
if (!textContent || textContent === TextContent$1.DEFAULT) {
|
|
8465
|
+
return false;
|
|
8466
|
+
}
|
|
8467
|
+
return true;
|
|
8468
|
+
}
|
|
8469
|
+
setup() {
|
|
8470
|
+
this.on("element:ready", TextContent.filter, (event) => {
|
|
8471
|
+
const target = event.target;
|
|
8472
|
+
const { textContent } = target.meta;
|
|
8473
|
+
switch (textContent) {
|
|
8474
|
+
case TextContent$1.NONE:
|
|
8475
|
+
this.validateNone(target);
|
|
8476
|
+
break;
|
|
8477
|
+
case TextContent$1.REQUIRED:
|
|
8478
|
+
this.validateRequired(target);
|
|
8479
|
+
break;
|
|
8480
|
+
case TextContent$1.ACCESSIBLE:
|
|
8481
|
+
this.validateAccessible(target);
|
|
8482
|
+
break;
|
|
8483
|
+
}
|
|
8484
|
+
});
|
|
8485
|
+
}
|
|
8486
|
+
/**
|
|
8487
|
+
* Validate element has empty text (inter-element whitespace is not considered text)
|
|
8488
|
+
*/
|
|
8489
|
+
validateNone(node) {
|
|
8490
|
+
if (classifyNodeText(node) === TextClassification.EMPTY_TEXT) {
|
|
8491
|
+
return;
|
|
8492
|
+
}
|
|
8493
|
+
this.reportError(node, node.meta, `${node.annotatedName} must not have text content`);
|
|
8494
|
+
}
|
|
8495
|
+
/**
|
|
8496
|
+
* Validate element has any text (inter-element whitespace is not considered text)
|
|
8497
|
+
*/
|
|
8498
|
+
validateRequired(node) {
|
|
8499
|
+
if (classifyNodeText(node) !== TextClassification.EMPTY_TEXT) {
|
|
8500
|
+
return;
|
|
8501
|
+
}
|
|
8502
|
+
this.reportError(node, node.meta, `${node.annotatedName} must have text content`);
|
|
8503
|
+
}
|
|
8504
|
+
/**
|
|
8505
|
+
* Validate element has accessible text (either regular text or text only
|
|
8506
|
+
* exposed in accessibility tree via aria-label or similar)
|
|
8507
|
+
*/
|
|
8508
|
+
validateAccessible(node) {
|
|
8509
|
+
if (!inAccessibilityTree(node)) {
|
|
8510
|
+
return;
|
|
8511
|
+
}
|
|
8512
|
+
if (haveAccessibleText(node)) {
|
|
8513
|
+
return;
|
|
8514
|
+
}
|
|
8515
|
+
this.reportError(node, node.meta, `${node.annotatedName} must have accessible text`);
|
|
8621
8516
|
}
|
|
8622
|
-
|
|
8623
|
-
|
|
8624
|
-
|
|
8517
|
+
reportError(node, meta, message) {
|
|
8518
|
+
this.report(node, message, null, {
|
|
8519
|
+
tagName: node.tagName,
|
|
8520
|
+
textContent: meta.textContent
|
|
8625
8521
|
});
|
|
8626
8522
|
}
|
|
8627
8523
|
}
|
|
8628
8524
|
|
|
8629
|
-
|
|
8630
|
-
|
|
8631
|
-
|
|
8632
|
-
|
|
8633
|
-
|
|
8634
|
-
|
|
8525
|
+
const roles = ["complementary", "contentinfo", "form", "banner", "main", "navigation", "region"];
|
|
8526
|
+
const selectors = [
|
|
8527
|
+
"aside",
|
|
8528
|
+
"footer",
|
|
8529
|
+
"form",
|
|
8530
|
+
"header",
|
|
8531
|
+
"main",
|
|
8532
|
+
"nav",
|
|
8533
|
+
"section",
|
|
8534
|
+
...roles.map((it) => `[role="${it}"]`)
|
|
8535
|
+
/* <search> does not (yet?) require a unique name */
|
|
8536
|
+
];
|
|
8537
|
+
function getTextFromReference(document, id) {
|
|
8538
|
+
if (!id || id instanceof DynamicValue) {
|
|
8539
|
+
return id;
|
|
8635
8540
|
}
|
|
8636
|
-
|
|
8637
|
-
|
|
8638
|
-
|
|
8639
|
-
|
|
8640
|
-
|
|
8641
|
-
|
|
8642
|
-
if (node.closed !== NodeClosed.EndTag) {
|
|
8643
|
-
this.report(node, `End tag for <${node.tagName}> must not be omitted`);
|
|
8644
|
-
}
|
|
8645
|
-
});
|
|
8541
|
+
const selector = `#${id}`;
|
|
8542
|
+
const ref = document.querySelector(selector);
|
|
8543
|
+
if (ref) {
|
|
8544
|
+
return ref.textContent;
|
|
8545
|
+
} else {
|
|
8546
|
+
return selector;
|
|
8646
8547
|
}
|
|
8647
8548
|
}
|
|
8648
|
-
|
|
8649
|
-
const
|
|
8650
|
-
|
|
8651
|
-
|
|
8652
|
-
|
|
8653
|
-
|
|
8654
|
-
|
|
8655
|
-
];
|
|
8656
|
-
|
|
8657
|
-
|
|
8549
|
+
function groupBy(values, callback) {
|
|
8550
|
+
const result = {};
|
|
8551
|
+
for (const value of values) {
|
|
8552
|
+
const key = callback(value);
|
|
8553
|
+
if (key in result) {
|
|
8554
|
+
result[key].push(value);
|
|
8555
|
+
} else {
|
|
8556
|
+
result[key] = [value];
|
|
8557
|
+
}
|
|
8558
|
+
}
|
|
8559
|
+
return result;
|
|
8560
|
+
}
|
|
8561
|
+
function getTextEntryFromElement(document, node) {
|
|
8562
|
+
const ariaLabel = node.getAttribute("aria-label");
|
|
8563
|
+
if (ariaLabel) {
|
|
8658
8564
|
return {
|
|
8659
|
-
|
|
8660
|
-
|
|
8565
|
+
node,
|
|
8566
|
+
text: ariaLabel.value,
|
|
8567
|
+
location: ariaLabel.keyLocation
|
|
8661
8568
|
};
|
|
8662
8569
|
}
|
|
8663
|
-
|
|
8664
|
-
|
|
8665
|
-
|
|
8666
|
-
|
|
8667
|
-
|
|
8668
|
-
|
|
8669
|
-
|
|
8670
|
-
|
|
8671
|
-
return;
|
|
8672
|
-
}
|
|
8673
|
-
const value = attr.value ? attr.value.toString() : "";
|
|
8674
|
-
if (!this.isJavascript(value)) {
|
|
8675
|
-
return;
|
|
8676
|
-
}
|
|
8677
|
-
this.report(
|
|
8678
|
-
node,
|
|
8679
|
-
'"type" attribute is unnecessary for javascript resources',
|
|
8680
|
-
attr.keyLocation
|
|
8681
|
-
);
|
|
8682
|
-
});
|
|
8570
|
+
const ariaLabelledby = node.getAttribute("aria-labelledby");
|
|
8571
|
+
if (ariaLabelledby) {
|
|
8572
|
+
const text = getTextFromReference(document, ariaLabelledby.value);
|
|
8573
|
+
return {
|
|
8574
|
+
node,
|
|
8575
|
+
text,
|
|
8576
|
+
location: ariaLabelledby.keyLocation
|
|
8577
|
+
};
|
|
8683
8578
|
}
|
|
8684
|
-
|
|
8685
|
-
|
|
8686
|
-
|
|
8579
|
+
return {
|
|
8580
|
+
node,
|
|
8581
|
+
text: null,
|
|
8582
|
+
location: node.location
|
|
8583
|
+
};
|
|
8584
|
+
}
|
|
8585
|
+
function isExcluded(entry) {
|
|
8586
|
+
const { node, text } = entry;
|
|
8587
|
+
if (text === null) {
|
|
8588
|
+
return !(node.is("form") || node.is("section"));
|
|
8687
8589
|
}
|
|
8590
|
+
return true;
|
|
8688
8591
|
}
|
|
8689
|
-
|
|
8690
|
-
class SvgFocusable extends Rule {
|
|
8592
|
+
class UniqueLandmark extends Rule {
|
|
8691
8593
|
documentation() {
|
|
8692
8594
|
return {
|
|
8693
|
-
description:
|
|
8694
|
-
|
|
8595
|
+
description: [
|
|
8596
|
+
"When the same type of landmark is present more than once in the same document each must be uniquely identifiable with a non-empty and unique name.",
|
|
8597
|
+
"For instance, if the document has two `<nav>` elements each of them need an accessible name to be distinguished from each other.",
|
|
8598
|
+
"",
|
|
8599
|
+
"The following elements / roles are considered landmarks:",
|
|
8600
|
+
"",
|
|
8601
|
+
' - `aside` or `[role="complementary"]`',
|
|
8602
|
+
' - `footer` or `[role="contentinfo"]`',
|
|
8603
|
+
' - `form` or `[role="form"]`',
|
|
8604
|
+
' - `header` or `[role="banner"]`',
|
|
8605
|
+
' - `main` or `[role="main"]`',
|
|
8606
|
+
' - `nav` or `[role="navigation"]`',
|
|
8607
|
+
' - `section` or `[role="region"]`',
|
|
8608
|
+
"",
|
|
8609
|
+
"To fix this either:",
|
|
8610
|
+
"",
|
|
8611
|
+
" - Add `aria-label`.",
|
|
8612
|
+
" - Add `aria-labelledby`.",
|
|
8613
|
+
" - Remove one of the landmarks."
|
|
8614
|
+
].join("\n"),
|
|
8615
|
+
url: "https://html-validate.org/rules/unique-landmark.html"
|
|
8695
8616
|
};
|
|
8696
8617
|
}
|
|
8697
8618
|
setup() {
|
|
8698
|
-
this.on("
|
|
8699
|
-
|
|
8700
|
-
|
|
8619
|
+
this.on("dom:ready", (event) => {
|
|
8620
|
+
const { document } = event;
|
|
8621
|
+
const elements = document.querySelectorAll(selectors.join(",")).filter((it) => typeof it.role === "string" && roles.includes(it.role));
|
|
8622
|
+
const grouped = groupBy(elements, (it) => it.role);
|
|
8623
|
+
for (const nodes of Object.values(grouped)) {
|
|
8624
|
+
if (nodes.length <= 1) {
|
|
8625
|
+
continue;
|
|
8626
|
+
}
|
|
8627
|
+
const entries = nodes.map((it) => getTextEntryFromElement(document, it));
|
|
8628
|
+
const filteredEntries = entries.filter(isExcluded);
|
|
8629
|
+
for (const entry of filteredEntries) {
|
|
8630
|
+
if (entry.text instanceof DynamicValue) {
|
|
8631
|
+
continue;
|
|
8632
|
+
}
|
|
8633
|
+
const dup = entries.filter((it) => it.text === entry.text).length > 1;
|
|
8634
|
+
if (!entry.text || dup) {
|
|
8635
|
+
const message = `Landmarks must have a non-empty and unique accessible name (aria-label or aria-labelledby)`;
|
|
8636
|
+
const location = entry.location;
|
|
8637
|
+
this.report({
|
|
8638
|
+
node: entry.node,
|
|
8639
|
+
message,
|
|
8640
|
+
location
|
|
8641
|
+
});
|
|
8642
|
+
}
|
|
8643
|
+
}
|
|
8701
8644
|
}
|
|
8702
8645
|
});
|
|
8703
8646
|
}
|
|
8704
|
-
validate(svg) {
|
|
8705
|
-
if (svg.hasAttribute("focusable")) {
|
|
8706
|
-
return;
|
|
8707
|
-
}
|
|
8708
|
-
this.report(svg, `<${svg.tagName}> is missing required "focusable" attribute`);
|
|
8709
|
-
}
|
|
8710
8647
|
}
|
|
8711
8648
|
|
|
8712
|
-
const defaults$
|
|
8713
|
-
|
|
8714
|
-
|
|
8715
|
-
{ pattern: "-", replacement: "‑", description: "non-breaking hyphen" }
|
|
8716
|
-
],
|
|
8717
|
-
ignoreClasses: [],
|
|
8718
|
-
ignoreStyle: true
|
|
8649
|
+
const defaults$4 = {
|
|
8650
|
+
ignoreCase: false,
|
|
8651
|
+
requireSemicolon: true
|
|
8719
8652
|
};
|
|
8720
|
-
|
|
8721
|
-
|
|
8722
|
-
|
|
8723
|
-
|
|
8724
|
-
const pattern = `(${disallowed})`;
|
|
8725
|
-
return new RegExp(pattern, "g");
|
|
8726
|
-
}
|
|
8727
|
-
function getText(node) {
|
|
8728
|
-
const match = node.textContent.match(/^(\s*)(.*)$/);
|
|
8729
|
-
const [, leading, text] = match;
|
|
8730
|
-
return [leading.length, text.trimEnd()];
|
|
8653
|
+
const regexp$1 = /&(?:[a-z0-9]+|#x?[0-9a-f]+)(;|[^a-z0-9]|$)/gi;
|
|
8654
|
+
const lowercaseEntities = elements.entities.map((it) => it.toLowerCase());
|
|
8655
|
+
function isNumerical(entity) {
|
|
8656
|
+
return entity.startsWith("&#");
|
|
8731
8657
|
}
|
|
8732
|
-
function
|
|
8733
|
-
const
|
|
8734
|
-
|
|
8735
|
-
let match;
|
|
8736
|
-
while (match = copy.exec(text)) {
|
|
8737
|
-
matches.push(match);
|
|
8738
|
-
}
|
|
8739
|
-
return matches;
|
|
8658
|
+
function getLocation(location, entity, match) {
|
|
8659
|
+
const index = match.index ?? 0;
|
|
8660
|
+
return sliceLocation(location, index, index + entity.length);
|
|
8740
8661
|
}
|
|
8741
|
-
|
|
8742
|
-
|
|
8743
|
-
|
|
8744
|
-
|
|
8662
|
+
function getDescription(context, options) {
|
|
8663
|
+
const url = "https://html.spec.whatwg.org/multipage/named-characters.html";
|
|
8664
|
+
let message;
|
|
8665
|
+
if (context.terminated) {
|
|
8666
|
+
message = `Unrecognized character reference \`${context.entity}\`.`;
|
|
8667
|
+
} else {
|
|
8668
|
+
message = `Character reference \`${context.entity}\` must be terminated by a semicolon.`;
|
|
8745
8669
|
}
|
|
8746
|
-
|
|
8747
|
-
|
|
8748
|
-
|
|
8749
|
-
|
|
8750
|
-
|
|
8751
|
-
|
|
8752
|
-
|
|
8753
|
-
|
|
8754
|
-
|
|
8755
|
-
|
|
8756
|
-
|
|
8757
|
-
|
|
8758
|
-
|
|
8759
|
-
|
|
8760
|
-
|
|
8761
|
-
|
|
8762
|
-
|
|
8763
|
-
|
|
8764
|
-
|
|
8765
|
-
},
|
|
8766
|
-
ignoreClasses: {
|
|
8767
|
-
type: "array",
|
|
8768
|
-
items: {
|
|
8769
|
-
type: "string"
|
|
8770
|
-
}
|
|
8670
|
+
return [
|
|
8671
|
+
message,
|
|
8672
|
+
`HTML5 defines a set of [valid character references](${url}) but this is not a valid one.`,
|
|
8673
|
+
"",
|
|
8674
|
+
"Ensure that:",
|
|
8675
|
+
"",
|
|
8676
|
+
"1. The character is one of the listed names.",
|
|
8677
|
+
...options.ignoreCase ? [] : ["1. The case is correct (names are case sensitive)."],
|
|
8678
|
+
...options.requireSemicolon ? ["1. The name is terminated with a `;`."] : []
|
|
8679
|
+
].join("\n");
|
|
8680
|
+
}
|
|
8681
|
+
class UnknownCharReference extends Rule {
|
|
8682
|
+
constructor(options) {
|
|
8683
|
+
super({ ...defaults$4, ...options });
|
|
8684
|
+
}
|
|
8685
|
+
static schema() {
|
|
8686
|
+
return {
|
|
8687
|
+
ignoreCase: {
|
|
8688
|
+
type: "boolean"
|
|
8771
8689
|
},
|
|
8772
|
-
|
|
8690
|
+
requireSemicolon: {
|
|
8773
8691
|
type: "boolean"
|
|
8774
8692
|
}
|
|
8775
8693
|
};
|
|
8776
8694
|
}
|
|
8777
8695
|
documentation(context) {
|
|
8778
|
-
const { characters } = this.options;
|
|
8779
|
-
const replacements = characters.map((it) => {
|
|
8780
|
-
return ` - \`${it.pattern}\` - replace with \`${it.replacement}\` (${it.description}).`;
|
|
8781
|
-
});
|
|
8782
8696
|
return {
|
|
8783
|
-
description:
|
|
8784
|
-
|
|
8785
|
-
"",
|
|
8786
|
-
"Unless non-breaking characters is used there could be a line break inserted at that character.",
|
|
8787
|
-
"Line breaks make is harder to read and understand the telephone number.",
|
|
8788
|
-
"",
|
|
8789
|
-
"The following characters should be avoided:",
|
|
8790
|
-
"",
|
|
8791
|
-
...replacements
|
|
8792
|
-
].join("\n"),
|
|
8793
|
-
url: "https://html-validate.org/rules/tel-non-breaking.html"
|
|
8697
|
+
description: getDescription(context, this.options),
|
|
8698
|
+
url: "https://html-validate.org/rules/unrecognized-char-ref.html"
|
|
8794
8699
|
};
|
|
8795
8700
|
}
|
|
8796
8701
|
setup() {
|
|
8797
|
-
this.on("element:ready",
|
|
8798
|
-
const
|
|
8799
|
-
|
|
8702
|
+
this.on("element:ready", (event) => {
|
|
8703
|
+
const node = event.target;
|
|
8704
|
+
for (const child of node.childNodes) {
|
|
8705
|
+
if (child.nodeType !== NodeType.TEXT_NODE) {
|
|
8706
|
+
continue;
|
|
8707
|
+
}
|
|
8708
|
+
this.findCharacterReferences(node, child.textContent, child.location, {
|
|
8709
|
+
isAttribute: false
|
|
8710
|
+
});
|
|
8711
|
+
}
|
|
8712
|
+
});
|
|
8713
|
+
this.on("attr", (event) => {
|
|
8714
|
+
if (!event.value) {
|
|
8800
8715
|
return;
|
|
8801
8716
|
}
|
|
8802
|
-
this.
|
|
8717
|
+
this.findCharacterReferences(event.target, event.value.toString(), event.valueLocation, {
|
|
8718
|
+
isAttribute: true
|
|
8719
|
+
});
|
|
8803
8720
|
});
|
|
8804
8721
|
}
|
|
8805
|
-
|
|
8806
|
-
|
|
8807
|
-
|
|
8808
|
-
|
|
8722
|
+
get entities() {
|
|
8723
|
+
if (this.options.ignoreCase) {
|
|
8724
|
+
return lowercaseEntities;
|
|
8725
|
+
} else {
|
|
8726
|
+
return elements.entities;
|
|
8809
8727
|
}
|
|
8810
|
-
|
|
8811
|
-
|
|
8812
|
-
|
|
8728
|
+
}
|
|
8729
|
+
findCharacterReferences(node, text, location, { isAttribute }) {
|
|
8730
|
+
const isQuerystring = isAttribute && text.includes("?");
|
|
8731
|
+
for (const match of this.getMatches(text)) {
|
|
8732
|
+
this.validateCharacterReference(node, location, match, { isQuerystring });
|
|
8813
8733
|
}
|
|
8814
|
-
return true;
|
|
8815
8734
|
}
|
|
8816
|
-
|
|
8817
|
-
const {
|
|
8818
|
-
const {
|
|
8819
|
-
|
|
8735
|
+
validateCharacterReference(node, location, foobar, { isQuerystring }) {
|
|
8736
|
+
const { requireSemicolon } = this.options;
|
|
8737
|
+
const { match, entity, raw, terminated } = foobar;
|
|
8738
|
+
if (isNumerical(entity)) {
|
|
8739
|
+
return;
|
|
8740
|
+
}
|
|
8741
|
+
if (isQuerystring && !terminated) {
|
|
8742
|
+
return;
|
|
8743
|
+
}
|
|
8744
|
+
const found = this.entities.includes(entity);
|
|
8745
|
+
if (found && (terminated || !requireSemicolon)) {
|
|
8746
|
+
return;
|
|
8747
|
+
}
|
|
8748
|
+
if (found && !terminated) {
|
|
8749
|
+
const entityLocation2 = getLocation(location, entity, match);
|
|
8750
|
+
const message2 = `Character reference "{{ entity }}" must be terminated by a semicolon`;
|
|
8751
|
+
const context2 = {
|
|
8752
|
+
entity: raw,
|
|
8753
|
+
terminated: false
|
|
8754
|
+
};
|
|
8755
|
+
this.report(node, message2, entityLocation2, context2);
|
|
8756
|
+
return;
|
|
8757
|
+
}
|
|
8758
|
+
const entityLocation = getLocation(location, entity, match);
|
|
8759
|
+
const message = `Unrecognized character reference "{{ entity }}"`;
|
|
8760
|
+
const context = {
|
|
8761
|
+
entity: raw,
|
|
8762
|
+
terminated: true
|
|
8763
|
+
};
|
|
8764
|
+
this.report(node, message, entityLocation, context);
|
|
8765
|
+
}
|
|
8766
|
+
*getMatches(text) {
|
|
8767
|
+
let match;
|
|
8768
|
+
do {
|
|
8769
|
+
match = regexp$1.exec(text);
|
|
8770
|
+
if (match) {
|
|
8771
|
+
const terminator = match[1];
|
|
8772
|
+
const terminated = terminator === ";";
|
|
8773
|
+
const needSlice = terminator !== ";" && terminator.length > 0;
|
|
8774
|
+
const entity = needSlice ? match[0].slice(0, -1) : match[0];
|
|
8775
|
+
if (this.options.ignoreCase) {
|
|
8776
|
+
yield { match, entity: entity.toLowerCase(), raw: entity, terminated };
|
|
8777
|
+
} else {
|
|
8778
|
+
yield { match, entity, raw: entity, terminated };
|
|
8779
|
+
}
|
|
8780
|
+
}
|
|
8781
|
+
} while (match);
|
|
8782
|
+
}
|
|
8783
|
+
}
|
|
8784
|
+
|
|
8785
|
+
const expectedOrder = ["section", "hint", "contact", "field1", "field2", "webauthn"];
|
|
8786
|
+
const fieldNames1 = [
|
|
8787
|
+
"name",
|
|
8788
|
+
"honorific-prefix",
|
|
8789
|
+
"given-name",
|
|
8790
|
+
"additional-name",
|
|
8791
|
+
"family-name",
|
|
8792
|
+
"honorific-suffix",
|
|
8793
|
+
"nickname",
|
|
8794
|
+
"username",
|
|
8795
|
+
"new-password",
|
|
8796
|
+
"current-password",
|
|
8797
|
+
"one-time-code",
|
|
8798
|
+
"organization-title",
|
|
8799
|
+
"organization",
|
|
8800
|
+
"street-address",
|
|
8801
|
+
"address-line1",
|
|
8802
|
+
"address-line2",
|
|
8803
|
+
"address-line3",
|
|
8804
|
+
"address-level4",
|
|
8805
|
+
"address-level3",
|
|
8806
|
+
"address-level2",
|
|
8807
|
+
"address-level1",
|
|
8808
|
+
"country",
|
|
8809
|
+
"country-name",
|
|
8810
|
+
"postal-code",
|
|
8811
|
+
"cc-name",
|
|
8812
|
+
"cc-given-name",
|
|
8813
|
+
"cc-additional-name",
|
|
8814
|
+
"cc-family-name",
|
|
8815
|
+
"cc-number",
|
|
8816
|
+
"cc-exp",
|
|
8817
|
+
"cc-exp-month",
|
|
8818
|
+
"cc-exp-year",
|
|
8819
|
+
"cc-csc",
|
|
8820
|
+
"cc-type",
|
|
8821
|
+
"transaction-currency",
|
|
8822
|
+
"transaction-amount",
|
|
8823
|
+
"language",
|
|
8824
|
+
"bday",
|
|
8825
|
+
"bday-day",
|
|
8826
|
+
"bday-month",
|
|
8827
|
+
"bday-year",
|
|
8828
|
+
"sex",
|
|
8829
|
+
"url",
|
|
8830
|
+
"photo"
|
|
8831
|
+
];
|
|
8832
|
+
const fieldNames2 = [
|
|
8833
|
+
"tel",
|
|
8834
|
+
"tel-country-code",
|
|
8835
|
+
"tel-national",
|
|
8836
|
+
"tel-area-code",
|
|
8837
|
+
"tel-local",
|
|
8838
|
+
"tel-local-prefix",
|
|
8839
|
+
"tel-local-suffix",
|
|
8840
|
+
"tel-extension",
|
|
8841
|
+
"email",
|
|
8842
|
+
"impp"
|
|
8843
|
+
];
|
|
8844
|
+
const fieldNameGroup = {
|
|
8845
|
+
name: "text",
|
|
8846
|
+
"honorific-prefix": "text",
|
|
8847
|
+
"given-name": "text",
|
|
8848
|
+
"additional-name": "text",
|
|
8849
|
+
"family-name": "text",
|
|
8850
|
+
"honorific-suffix": "text",
|
|
8851
|
+
nickname: "text",
|
|
8852
|
+
username: "username",
|
|
8853
|
+
"new-password": "password",
|
|
8854
|
+
"current-password": "password",
|
|
8855
|
+
"one-time-code": "password",
|
|
8856
|
+
"organization-title": "text",
|
|
8857
|
+
organization: "text",
|
|
8858
|
+
"street-address": "multiline",
|
|
8859
|
+
"address-line1": "text",
|
|
8860
|
+
"address-line2": "text",
|
|
8861
|
+
"address-line3": "text",
|
|
8862
|
+
"address-level4": "text",
|
|
8863
|
+
"address-level3": "text",
|
|
8864
|
+
"address-level2": "text",
|
|
8865
|
+
"address-level1": "text",
|
|
8866
|
+
country: "text",
|
|
8867
|
+
"country-name": "text",
|
|
8868
|
+
"postal-code": "text",
|
|
8869
|
+
"cc-name": "text",
|
|
8870
|
+
"cc-given-name": "text",
|
|
8871
|
+
"cc-additional-name": "text",
|
|
8872
|
+
"cc-family-name": "text",
|
|
8873
|
+
"cc-number": "text",
|
|
8874
|
+
"cc-exp": "month",
|
|
8875
|
+
"cc-exp-month": "numeric",
|
|
8876
|
+
"cc-exp-year": "numeric",
|
|
8877
|
+
"cc-csc": "text",
|
|
8878
|
+
"cc-type": "text",
|
|
8879
|
+
"transaction-currency": "text",
|
|
8880
|
+
"transaction-amount": "numeric",
|
|
8881
|
+
language: "text",
|
|
8882
|
+
bday: "date",
|
|
8883
|
+
"bday-day": "numeric",
|
|
8884
|
+
"bday-month": "numeric",
|
|
8885
|
+
"bday-year": "numeric",
|
|
8886
|
+
sex: "text",
|
|
8887
|
+
url: "url",
|
|
8888
|
+
photo: "url",
|
|
8889
|
+
tel: "tel",
|
|
8890
|
+
"tel-country-code": "text",
|
|
8891
|
+
"tel-national": "text",
|
|
8892
|
+
"tel-area-code": "text",
|
|
8893
|
+
"tel-local": "text",
|
|
8894
|
+
"tel-local-prefix": "text",
|
|
8895
|
+
"tel-local-suffix": "text",
|
|
8896
|
+
"tel-extension": "text",
|
|
8897
|
+
email: "username",
|
|
8898
|
+
impp: "url"
|
|
8899
|
+
};
|
|
8900
|
+
const disallowedInputTypes = ["checkbox", "radio", "file", "submit", "image", "reset", "button"];
|
|
8901
|
+
function matchSection(token) {
|
|
8902
|
+
return token.startsWith("section-");
|
|
8903
|
+
}
|
|
8904
|
+
function matchHint(token) {
|
|
8905
|
+
return token === "shipping" || token === "billing";
|
|
8906
|
+
}
|
|
8907
|
+
function matchFieldNames1(token) {
|
|
8908
|
+
return fieldNames1.includes(token);
|
|
8909
|
+
}
|
|
8910
|
+
function matchContact(token) {
|
|
8911
|
+
const haystack = ["home", "work", "mobile", "fax", "pager"];
|
|
8912
|
+
return haystack.includes(token);
|
|
8913
|
+
}
|
|
8914
|
+
function matchFieldNames2(token) {
|
|
8915
|
+
return fieldNames2.includes(token);
|
|
8916
|
+
}
|
|
8917
|
+
function matchWebauthn(token) {
|
|
8918
|
+
return token === "webauthn";
|
|
8919
|
+
}
|
|
8920
|
+
function matchToken(token) {
|
|
8921
|
+
if (matchSection(token)) {
|
|
8922
|
+
return "section";
|
|
8923
|
+
}
|
|
8924
|
+
if (matchHint(token)) {
|
|
8925
|
+
return "hint";
|
|
8820
8926
|
}
|
|
8821
|
-
|
|
8822
|
-
|
|
8823
|
-
const { style } = node;
|
|
8824
|
-
if (!ignoreStyle) {
|
|
8825
|
-
return false;
|
|
8826
|
-
}
|
|
8827
|
-
if (style["white-space"] === "nowrap" || style["white-space"] === "pre") {
|
|
8828
|
-
return true;
|
|
8829
|
-
}
|
|
8830
|
-
return false;
|
|
8927
|
+
if (matchFieldNames1(token)) {
|
|
8928
|
+
return "field1";
|
|
8831
8929
|
}
|
|
8832
|
-
|
|
8833
|
-
return
|
|
8930
|
+
if (matchFieldNames2(token)) {
|
|
8931
|
+
return "field2";
|
|
8834
8932
|
}
|
|
8835
|
-
|
|
8836
|
-
|
|
8837
|
-
if (isTextNode(child)) {
|
|
8838
|
-
this.detectDisallowed(anchor, child);
|
|
8839
|
-
} else if (isElementNode(child)) {
|
|
8840
|
-
this.walk(anchor, child);
|
|
8841
|
-
}
|
|
8842
|
-
}
|
|
8933
|
+
if (matchContact(token)) {
|
|
8934
|
+
return "contact";
|
|
8843
8935
|
}
|
|
8844
|
-
|
|
8845
|
-
|
|
8846
|
-
const matches = matchAll(text, this.regex);
|
|
8847
|
-
for (const match of matches) {
|
|
8848
|
-
const detected = match[0];
|
|
8849
|
-
const entry = this.options.characters.find((it) => it.pattern === detected);
|
|
8850
|
-
if (!entry) {
|
|
8851
|
-
throw new Error(`Failed to find entry for "${detected}" when searching text "${text}"`);
|
|
8852
|
-
}
|
|
8853
|
-
const message = `"${detected}" should be replaced with "${entry.replacement}" (${entry.description}) in telephone number`;
|
|
8854
|
-
const begin = offset + match.index;
|
|
8855
|
-
const end = begin + detected.length;
|
|
8856
|
-
const location = sliceLocation(node.location, begin, end);
|
|
8857
|
-
const context = entry;
|
|
8858
|
-
this.report(anchor, message, location, context);
|
|
8859
|
-
}
|
|
8936
|
+
if (matchWebauthn(token)) {
|
|
8937
|
+
return "webauthn";
|
|
8860
8938
|
}
|
|
8939
|
+
return null;
|
|
8861
8940
|
}
|
|
8862
|
-
|
|
8863
|
-
|
|
8864
|
-
|
|
8865
|
-
|
|
8941
|
+
function getControlGroups(type) {
|
|
8942
|
+
const allGroups = [
|
|
8943
|
+
"text",
|
|
8944
|
+
"multiline",
|
|
8945
|
+
"password",
|
|
8946
|
+
"url",
|
|
8947
|
+
"username",
|
|
8948
|
+
"tel",
|
|
8949
|
+
"numeric",
|
|
8950
|
+
"month",
|
|
8951
|
+
"date"
|
|
8952
|
+
];
|
|
8953
|
+
const mapping = {
|
|
8954
|
+
hidden: allGroups,
|
|
8955
|
+
text: allGroups.filter((it) => it !== "multiline"),
|
|
8956
|
+
search: allGroups.filter((it) => it !== "multiline"),
|
|
8957
|
+
password: ["password"],
|
|
8958
|
+
url: ["url"],
|
|
8959
|
+
email: ["username"],
|
|
8960
|
+
tel: ["tel"],
|
|
8961
|
+
number: ["numeric"],
|
|
8962
|
+
month: ["month"],
|
|
8963
|
+
date: ["date"]
|
|
8964
|
+
};
|
|
8965
|
+
return mapping[type] ?? [];
|
|
8866
8966
|
}
|
|
8867
|
-
function
|
|
8967
|
+
function isDisallowedType(node, type) {
|
|
8868
8968
|
if (!node.is("input")) {
|
|
8869
8969
|
return false;
|
|
8870
8970
|
}
|
|
8871
|
-
|
|
8872
|
-
return false;
|
|
8873
|
-
}
|
|
8874
|
-
const type = node.getAttribute("type");
|
|
8875
|
-
return Boolean(type == null ? void 0 : type.valueMatches(/submit|reset/, false));
|
|
8971
|
+
return disallowedInputTypes.includes(type);
|
|
8876
8972
|
}
|
|
8877
|
-
function
|
|
8878
|
-
|
|
8879
|
-
|
|
8880
|
-
|
|
8881
|
-
|
|
8973
|
+
function getTerminalMessage(context) {
|
|
8974
|
+
switch (context.msg) {
|
|
8975
|
+
case 0 /* InvalidAttribute */:
|
|
8976
|
+
return "autocomplete attribute cannot be used on {{ what }}";
|
|
8977
|
+
case 1 /* InvalidValue */:
|
|
8978
|
+
return '"{{ value }}" cannot be used on {{ what }}';
|
|
8979
|
+
case 2 /* InvalidOrder */:
|
|
8980
|
+
return '"{{ second }}" must appear before "{{ first }}"';
|
|
8981
|
+
case 3 /* InvalidToken */:
|
|
8982
|
+
return '"{{ token }}" is not a valid autocomplete token or field name';
|
|
8983
|
+
case 4 /* InvalidCombination */:
|
|
8984
|
+
return '"{{ second }}" cannot be combined with "{{ first }}"';
|
|
8985
|
+
case 5 /* MissingField */:
|
|
8986
|
+
return "autocomplete attribute is missing field name";
|
|
8882
8987
|
}
|
|
8883
8988
|
}
|
|
8884
|
-
function
|
|
8885
|
-
|
|
8886
|
-
|
|
8887
|
-
|
|
8888
|
-
|
|
8889
|
-
|
|
8890
|
-
|
|
8891
|
-
|
|
8892
|
-
|
|
8893
|
-
|
|
8894
|
-
|
|
8895
|
-
|
|
8896
|
-
|
|
8897
|
-
|
|
8898
|
-
|
|
8899
|
-
|
|
8900
|
-
|
|
8901
|
-
|
|
8902
|
-
|
|
8989
|
+
function getMarkdownMessage(context) {
|
|
8990
|
+
switch (context.msg) {
|
|
8991
|
+
case 0 /* InvalidAttribute */:
|
|
8992
|
+
return [
|
|
8993
|
+
`\`autocomplete\` attribute cannot be used on \`${context.what}\``,
|
|
8994
|
+
"",
|
|
8995
|
+
"The following input types cannot use the `autocomplete` attribute:",
|
|
8996
|
+
"",
|
|
8997
|
+
...disallowedInputTypes.map((it) => `- \`${it}\``)
|
|
8998
|
+
].join("\n");
|
|
8999
|
+
case 1 /* InvalidValue */: {
|
|
9000
|
+
const message = `\`"${context.value}"\` cannot be used on \`${context.what}\``;
|
|
9001
|
+
if (context.type === "form") {
|
|
9002
|
+
return [
|
|
9003
|
+
message,
|
|
9004
|
+
"",
|
|
9005
|
+
'The `<form>` element can only use the values `"on"` and `"off"`.'
|
|
9006
|
+
].join("\n");
|
|
9007
|
+
}
|
|
9008
|
+
if (context.type === "hidden") {
|
|
9009
|
+
return [
|
|
9010
|
+
message,
|
|
9011
|
+
"",
|
|
9012
|
+
'`<input type="hidden">` cannot use the values `"on"` and `"off"`.'
|
|
9013
|
+
].join("\n");
|
|
9014
|
+
}
|
|
9015
|
+
const controlGroups = getControlGroups(context.type);
|
|
9016
|
+
const currentGroup = fieldNameGroup[context.value];
|
|
9017
|
+
return [
|
|
9018
|
+
message,
|
|
9019
|
+
"",
|
|
9020
|
+
`\`${context.what}\` allows autocomplete fields from the following group${controlGroups.length > 1 ? "s" : ""}:`,
|
|
9021
|
+
"",
|
|
9022
|
+
...controlGroups.map((it) => `- ${it}`),
|
|
9023
|
+
"",
|
|
9024
|
+
`The field \`"${context.value}"\` belongs to the group /${currentGroup}/ which cannot be used with this input type.`
|
|
9025
|
+
].join("\n");
|
|
9026
|
+
}
|
|
9027
|
+
case 2 /* InvalidOrder */:
|
|
9028
|
+
return [
|
|
9029
|
+
`\`"${context.second}"\` must appear before \`"${context.first}"\``,
|
|
9030
|
+
"",
|
|
9031
|
+
"The autocomplete tokens must appear in the following order:",
|
|
9032
|
+
"",
|
|
9033
|
+
"- Optional section name (`section-` prefix).",
|
|
9034
|
+
"- Optional `shipping` or `billing` token.",
|
|
9035
|
+
"- Optional `home`, `work`, `mobile`, `fax` or `pager` token (for fields supporting it).",
|
|
9036
|
+
"- Field name",
|
|
9037
|
+
"- Optional `webauthn` token."
|
|
9038
|
+
].join("\n");
|
|
9039
|
+
case 3 /* InvalidToken */:
|
|
9040
|
+
return `\`"${context.token}"\` is not a valid autocomplete token or field name`;
|
|
9041
|
+
case 4 /* InvalidCombination */:
|
|
9042
|
+
return `\`"${context.second}"\` cannot be combined with \`"${context.first}"\``;
|
|
9043
|
+
case 5 /* MissingField */:
|
|
9044
|
+
return "Autocomplete attribute is missing field name";
|
|
8903
9045
|
}
|
|
8904
|
-
return node.childElements.some((child) => {
|
|
8905
|
-
return haveAccessibleText(child);
|
|
8906
|
-
});
|
|
8907
9046
|
}
|
|
8908
|
-
class
|
|
9047
|
+
class ValidAutocomplete extends Rule {
|
|
8909
9048
|
documentation(context) {
|
|
8910
|
-
|
|
8911
|
-
description:
|
|
8912
|
-
url: "https://html-validate.org/rules/
|
|
9049
|
+
return {
|
|
9050
|
+
description: getMarkdownMessage(context),
|
|
9051
|
+
url: "https://html-validate.org/rules/valid-autocomplete.html"
|
|
8913
9052
|
};
|
|
8914
|
-
switch (context.textContent) {
|
|
8915
|
-
case TextContent$1.NONE:
|
|
8916
|
-
doc.description = `The \`<${context.tagName}>\` element must not have textual content.`;
|
|
8917
|
-
break;
|
|
8918
|
-
case TextContent$1.REQUIRED:
|
|
8919
|
-
doc.description = `The \`<${context.tagName}>\` element must have textual content.`;
|
|
8920
|
-
break;
|
|
8921
|
-
case TextContent$1.ACCESSIBLE:
|
|
8922
|
-
doc.description = `The \`<${context.tagName}>\` element must have accessible text.`;
|
|
8923
|
-
break;
|
|
8924
|
-
}
|
|
8925
|
-
return doc;
|
|
8926
|
-
}
|
|
8927
|
-
static filter(event) {
|
|
8928
|
-
const { target } = event;
|
|
8929
|
-
if (!target.meta) {
|
|
8930
|
-
return false;
|
|
8931
|
-
}
|
|
8932
|
-
const { textContent } = target.meta;
|
|
8933
|
-
if (!textContent || textContent === TextContent$1.DEFAULT) {
|
|
8934
|
-
return false;
|
|
8935
|
-
}
|
|
8936
|
-
return true;
|
|
8937
9053
|
}
|
|
8938
9054
|
setup() {
|
|
8939
|
-
this.on("
|
|
8940
|
-
const
|
|
8941
|
-
const
|
|
8942
|
-
|
|
8943
|
-
|
|
8944
|
-
|
|
8945
|
-
|
|
8946
|
-
|
|
8947
|
-
|
|
8948
|
-
|
|
8949
|
-
|
|
8950
|
-
|
|
8951
|
-
|
|
9055
|
+
this.on("dom:ready", (event) => {
|
|
9056
|
+
const { document } = event;
|
|
9057
|
+
const elements = document.querySelectorAll("[autocomplete]");
|
|
9058
|
+
for (const element of elements) {
|
|
9059
|
+
const autocomplete = element.getAttribute("autocomplete");
|
|
9060
|
+
if (autocomplete.value === null || autocomplete.value instanceof DynamicValue) {
|
|
9061
|
+
continue;
|
|
9062
|
+
}
|
|
9063
|
+
const location = autocomplete.valueLocation;
|
|
9064
|
+
const value = autocomplete.value.toLowerCase();
|
|
9065
|
+
const tokens = new DOMTokenList(value, location);
|
|
9066
|
+
if (tokens.length === 0) {
|
|
9067
|
+
continue;
|
|
9068
|
+
}
|
|
9069
|
+
this.validate(element, value, tokens, autocomplete.keyLocation, location);
|
|
8952
9070
|
}
|
|
8953
9071
|
});
|
|
8954
9072
|
}
|
|
8955
|
-
|
|
8956
|
-
|
|
8957
|
-
|
|
8958
|
-
|
|
8959
|
-
|
|
8960
|
-
|
|
9073
|
+
validate(node, value, tokens, keyLocation, valueLocation) {
|
|
9074
|
+
switch (node.tagName) {
|
|
9075
|
+
case "form":
|
|
9076
|
+
this.validateFormAutocomplete(node, value, valueLocation);
|
|
9077
|
+
break;
|
|
9078
|
+
case "input":
|
|
9079
|
+
case "textarea":
|
|
9080
|
+
case "select":
|
|
9081
|
+
this.validateControlAutocomplete(node, tokens, keyLocation);
|
|
9082
|
+
break;
|
|
8961
9083
|
}
|
|
8962
|
-
this.reportError(node, node.meta, `${node.annotatedName} must not have text content`);
|
|
8963
9084
|
}
|
|
8964
|
-
|
|
8965
|
-
|
|
8966
|
-
|
|
8967
|
-
|
|
8968
|
-
|
|
9085
|
+
validateControlAutocomplete(node, tokens, keyLocation) {
|
|
9086
|
+
const type = node.getAttributeValue("type") ?? "text";
|
|
9087
|
+
const mantle = type !== "hidden" ? "expectation" : "anchor";
|
|
9088
|
+
if (isDisallowedType(node, type)) {
|
|
9089
|
+
const context = {
|
|
9090
|
+
msg: 0 /* InvalidAttribute */,
|
|
9091
|
+
what: `<input type="${type}">`
|
|
9092
|
+
};
|
|
9093
|
+
this.report({
|
|
9094
|
+
node,
|
|
9095
|
+
message: getTerminalMessage(context),
|
|
9096
|
+
location: keyLocation,
|
|
9097
|
+
context
|
|
9098
|
+
});
|
|
8969
9099
|
return;
|
|
8970
9100
|
}
|
|
8971
|
-
|
|
8972
|
-
|
|
8973
|
-
/**
|
|
8974
|
-
* Validate element has accessible text (either regular text or text only
|
|
8975
|
-
* exposed in accessibility tree via aria-label or similar)
|
|
8976
|
-
*/
|
|
8977
|
-
validateAccessible(node) {
|
|
8978
|
-
if (!inAccessibilityTree(node)) {
|
|
9101
|
+
if (tokens.includes("on") || tokens.includes("off")) {
|
|
9102
|
+
this.validateOnOff(node, mantle, tokens);
|
|
8979
9103
|
return;
|
|
8980
9104
|
}
|
|
8981
|
-
|
|
9105
|
+
this.validateTokens(node, tokens, keyLocation);
|
|
9106
|
+
}
|
|
9107
|
+
validateFormAutocomplete(node, value, location) {
|
|
9108
|
+
const trimmed = value.trim();
|
|
9109
|
+
if (["on", "off"].includes(trimmed)) {
|
|
8982
9110
|
return;
|
|
8983
9111
|
}
|
|
8984
|
-
|
|
8985
|
-
|
|
8986
|
-
|
|
8987
|
-
|
|
8988
|
-
|
|
8989
|
-
|
|
9112
|
+
const context = {
|
|
9113
|
+
msg: 1 /* InvalidValue */,
|
|
9114
|
+
type: "form",
|
|
9115
|
+
value: trimmed,
|
|
9116
|
+
what: "<form>"
|
|
9117
|
+
};
|
|
9118
|
+
this.report({
|
|
9119
|
+
node,
|
|
9120
|
+
message: getTerminalMessage(context),
|
|
9121
|
+
location,
|
|
9122
|
+
context
|
|
8990
9123
|
});
|
|
8991
9124
|
}
|
|
8992
|
-
|
|
8993
|
-
|
|
8994
|
-
const
|
|
8995
|
-
const
|
|
8996
|
-
|
|
8997
|
-
|
|
8998
|
-
|
|
8999
|
-
|
|
9000
|
-
|
|
9001
|
-
|
|
9002
|
-
|
|
9003
|
-
|
|
9004
|
-
|
|
9005
|
-
|
|
9006
|
-
|
|
9007
|
-
|
|
9008
|
-
|
|
9009
|
-
|
|
9010
|
-
|
|
9011
|
-
|
|
9012
|
-
|
|
9013
|
-
|
|
9014
|
-
|
|
9015
|
-
|
|
9016
|
-
|
|
9017
|
-
|
|
9018
|
-
|
|
9019
|
-
|
|
9020
|
-
|
|
9021
|
-
|
|
9022
|
-
|
|
9023
|
-
|
|
9024
|
-
|
|
9025
|
-
|
|
9125
|
+
validateOnOff(node, mantle, tokens) {
|
|
9126
|
+
const index = tokens.findIndex((it) => it === "on" || it === "off");
|
|
9127
|
+
const value = tokens.item(index);
|
|
9128
|
+
const location = tokens.location(index);
|
|
9129
|
+
if (tokens.length > 1) {
|
|
9130
|
+
const context = {
|
|
9131
|
+
msg: 4 /* InvalidCombination */,
|
|
9132
|
+
/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
|
|
9133
|
+
first: tokens.item(index > 0 ? 0 : 1),
|
|
9134
|
+
second: value
|
|
9135
|
+
};
|
|
9136
|
+
this.report({
|
|
9137
|
+
node,
|
|
9138
|
+
message: getTerminalMessage(context),
|
|
9139
|
+
location,
|
|
9140
|
+
context
|
|
9141
|
+
});
|
|
9142
|
+
}
|
|
9143
|
+
switch (mantle) {
|
|
9144
|
+
case "expectation":
|
|
9145
|
+
return;
|
|
9146
|
+
case "anchor": {
|
|
9147
|
+
const context = {
|
|
9148
|
+
msg: 1 /* InvalidValue */,
|
|
9149
|
+
type: "hidden",
|
|
9150
|
+
value,
|
|
9151
|
+
what: `<input type="hidden">`
|
|
9152
|
+
};
|
|
9153
|
+
this.report({
|
|
9154
|
+
node,
|
|
9155
|
+
message: getTerminalMessage(context),
|
|
9156
|
+
location: tokens.location(0),
|
|
9157
|
+
context
|
|
9158
|
+
});
|
|
9159
|
+
}
|
|
9026
9160
|
}
|
|
9027
9161
|
}
|
|
9028
|
-
|
|
9029
|
-
|
|
9030
|
-
|
|
9031
|
-
|
|
9032
|
-
|
|
9033
|
-
|
|
9034
|
-
|
|
9035
|
-
|
|
9036
|
-
|
|
9037
|
-
|
|
9038
|
-
|
|
9039
|
-
|
|
9040
|
-
|
|
9041
|
-
|
|
9042
|
-
|
|
9043
|
-
|
|
9044
|
-
|
|
9045
|
-
|
|
9046
|
-
|
|
9162
|
+
validateTokens(node, tokens, keyLocation) {
|
|
9163
|
+
const order = [];
|
|
9164
|
+
for (const { item, location } of tokens.iterator()) {
|
|
9165
|
+
const tokenType = matchToken(item);
|
|
9166
|
+
if (tokenType) {
|
|
9167
|
+
order.push(tokenType);
|
|
9168
|
+
} else {
|
|
9169
|
+
const context = {
|
|
9170
|
+
msg: 3 /* InvalidToken */,
|
|
9171
|
+
token: item
|
|
9172
|
+
};
|
|
9173
|
+
this.report({
|
|
9174
|
+
node,
|
|
9175
|
+
message: getTerminalMessage(context),
|
|
9176
|
+
location,
|
|
9177
|
+
context
|
|
9178
|
+
});
|
|
9179
|
+
return;
|
|
9180
|
+
}
|
|
9181
|
+
}
|
|
9182
|
+
const fieldTokens = order.map((it) => it === "field1" || it === "field2");
|
|
9183
|
+
this.validateFieldPresence(node, tokens, fieldTokens, keyLocation);
|
|
9184
|
+
this.validateContact(node, tokens, order);
|
|
9185
|
+
this.validateOrder(node, tokens, order);
|
|
9186
|
+
this.validateControlGroup(node, tokens, fieldTokens);
|
|
9047
9187
|
}
|
|
9048
|
-
|
|
9049
|
-
|
|
9050
|
-
|
|
9051
|
-
|
|
9052
|
-
|
|
9053
|
-
|
|
9054
|
-
|
|
9055
|
-
|
|
9056
|
-
|
|
9057
|
-
|
|
9188
|
+
/**
|
|
9189
|
+
* Ensure that exactly one field name is present from the two field lists.
|
|
9190
|
+
*/
|
|
9191
|
+
validateFieldPresence(node, tokens, fieldTokens, keyLocation) {
|
|
9192
|
+
const numFields = fieldTokens.filter(Boolean).length;
|
|
9193
|
+
if (numFields === 0) {
|
|
9194
|
+
const context = {
|
|
9195
|
+
msg: 5 /* MissingField */
|
|
9196
|
+
};
|
|
9197
|
+
this.report({
|
|
9198
|
+
node,
|
|
9199
|
+
message: getTerminalMessage(context),
|
|
9200
|
+
location: keyLocation,
|
|
9201
|
+
context
|
|
9202
|
+
});
|
|
9203
|
+
} else if (numFields > 1) {
|
|
9204
|
+
const a = fieldTokens.indexOf(true);
|
|
9205
|
+
const b = fieldTokens.lastIndexOf(true);
|
|
9206
|
+
const context = {
|
|
9207
|
+
msg: 4 /* InvalidCombination */,
|
|
9208
|
+
/* eslint-disable @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
|
|
9209
|
+
first: tokens.item(a),
|
|
9210
|
+
second: tokens.item(b)
|
|
9211
|
+
/* eslint-enable @typescript-eslint/no-non-null-assertion */
|
|
9212
|
+
};
|
|
9213
|
+
this.report({
|
|
9214
|
+
node,
|
|
9215
|
+
message: getTerminalMessage(context),
|
|
9216
|
+
location: tokens.location(b),
|
|
9217
|
+
context
|
|
9218
|
+
});
|
|
9219
|
+
}
|
|
9058
9220
|
}
|
|
9059
|
-
|
|
9060
|
-
|
|
9061
|
-
|
|
9062
|
-
|
|
9063
|
-
|
|
9064
|
-
|
|
9065
|
-
|
|
9066
|
-
|
|
9067
|
-
|
|
9068
|
-
|
|
9069
|
-
|
|
9070
|
-
|
|
9071
|
-
|
|
9072
|
-
|
|
9073
|
-
|
|
9074
|
-
|
|
9075
|
-
|
|
9076
|
-
|
|
9077
|
-
|
|
9078
|
-
|
|
9079
|
-
|
|
9080
|
-
" - Add `aria-label`.",
|
|
9081
|
-
" - Add `aria-labelledby`.",
|
|
9082
|
-
" - Remove one of the landmarks."
|
|
9083
|
-
].join("\n"),
|
|
9084
|
-
url: "https://html-validate.org/rules/unique-landmark.html"
|
|
9085
|
-
};
|
|
9221
|
+
/**
|
|
9222
|
+
* Ensure contact token is only used with field names from the second list.
|
|
9223
|
+
*/
|
|
9224
|
+
validateContact(node, tokens, order) {
|
|
9225
|
+
if (order.includes("contact") && order.includes("field1")) {
|
|
9226
|
+
const a = order.indexOf("field1");
|
|
9227
|
+
const b = order.indexOf("contact");
|
|
9228
|
+
const context = {
|
|
9229
|
+
msg: 4 /* InvalidCombination */,
|
|
9230
|
+
/* eslint-disable @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
|
|
9231
|
+
first: tokens.item(a),
|
|
9232
|
+
second: tokens.item(b)
|
|
9233
|
+
/* eslint-enable @typescript-eslint/no-non-null-assertion */
|
|
9234
|
+
};
|
|
9235
|
+
this.report({
|
|
9236
|
+
node,
|
|
9237
|
+
message: getTerminalMessage(context),
|
|
9238
|
+
location: tokens.location(b),
|
|
9239
|
+
context
|
|
9240
|
+
});
|
|
9241
|
+
}
|
|
9086
9242
|
}
|
|
9087
|
-
|
|
9088
|
-
|
|
9089
|
-
|
|
9090
|
-
|
|
9091
|
-
|
|
9092
|
-
|
|
9093
|
-
|
|
9094
|
-
|
|
9095
|
-
|
|
9096
|
-
|
|
9097
|
-
|
|
9098
|
-
|
|
9099
|
-
|
|
9100
|
-
|
|
9101
|
-
|
|
9102
|
-
|
|
9103
|
-
|
|
9104
|
-
const message = `Landmarks must have a non-empty and unique accessible name (aria-label or aria-labelledby)`;
|
|
9105
|
-
const location = entry.location;
|
|
9106
|
-
this.report({
|
|
9107
|
-
node: entry.node,
|
|
9108
|
-
message,
|
|
9109
|
-
location
|
|
9110
|
-
});
|
|
9111
|
-
}
|
|
9112
|
-
}
|
|
9243
|
+
validateOrder(node, tokens, order) {
|
|
9244
|
+
const indicies = order.map((it) => expectedOrder.indexOf(it));
|
|
9245
|
+
for (let i = 0; i < indicies.length - 1; i++) {
|
|
9246
|
+
if (indicies[0] > indicies[i + 1]) {
|
|
9247
|
+
const context = {
|
|
9248
|
+
msg: 2 /* InvalidOrder */,
|
|
9249
|
+
/* eslint-disable @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
|
|
9250
|
+
first: tokens.item(i),
|
|
9251
|
+
second: tokens.item(i + 1)
|
|
9252
|
+
/* eslint-enable @typescript-eslint/no-non-null-assertion */
|
|
9253
|
+
};
|
|
9254
|
+
this.report({
|
|
9255
|
+
node,
|
|
9256
|
+
message: getTerminalMessage(context),
|
|
9257
|
+
location: tokens.location(i + 1),
|
|
9258
|
+
context
|
|
9259
|
+
});
|
|
9113
9260
|
}
|
|
9114
|
-
}
|
|
9261
|
+
}
|
|
9262
|
+
}
|
|
9263
|
+
validateControlGroup(node, tokens, fieldTokens) {
|
|
9264
|
+
const numFields = fieldTokens.filter(Boolean).length;
|
|
9265
|
+
if (numFields === 0) {
|
|
9266
|
+
return;
|
|
9267
|
+
}
|
|
9268
|
+
if (!node.is("input")) {
|
|
9269
|
+
return;
|
|
9270
|
+
}
|
|
9271
|
+
const attr = node.getAttribute("type");
|
|
9272
|
+
const type = (attr == null ? void 0 : attr.value) ?? "text";
|
|
9273
|
+
if (type instanceof DynamicValue) {
|
|
9274
|
+
return;
|
|
9275
|
+
}
|
|
9276
|
+
const controlGroups = getControlGroups(type);
|
|
9277
|
+
const fieldIndex = fieldTokens.indexOf(true);
|
|
9278
|
+
const fieldToken = tokens.item(fieldIndex);
|
|
9279
|
+
const fieldGroup = fieldNameGroup[fieldToken];
|
|
9280
|
+
if (!controlGroups.includes(fieldGroup)) {
|
|
9281
|
+
const context = {
|
|
9282
|
+
msg: 1 /* InvalidValue */,
|
|
9283
|
+
type,
|
|
9284
|
+
value: fieldToken,
|
|
9285
|
+
what: `<input type="${type}">`
|
|
9286
|
+
};
|
|
9287
|
+
this.report({
|
|
9288
|
+
node,
|
|
9289
|
+
message: getTerminalMessage(context),
|
|
9290
|
+
location: tokens.location(fieldIndex),
|
|
9291
|
+
context
|
|
9292
|
+
});
|
|
9293
|
+
}
|
|
9115
9294
|
}
|
|
9116
9295
|
}
|
|
9117
9296
|
|
|
9118
|
-
const defaults$
|
|
9119
|
-
|
|
9120
|
-
requireSemicolon: true
|
|
9297
|
+
const defaults$3 = {
|
|
9298
|
+
relaxed: false
|
|
9121
9299
|
};
|
|
9122
|
-
|
|
9123
|
-
const lowercaseEntities = elements.entities.map((it) => it.toLowerCase());
|
|
9124
|
-
function isNumerical(entity) {
|
|
9125
|
-
return entity.startsWith("&#");
|
|
9126
|
-
}
|
|
9127
|
-
function getLocation(location, entity, match) {
|
|
9128
|
-
const index = match.index ?? 0;
|
|
9129
|
-
return sliceLocation(location, index, index + entity.length);
|
|
9130
|
-
}
|
|
9131
|
-
function getDescription(context, options) {
|
|
9132
|
-
const url = "https://html.spec.whatwg.org/multipage/named-characters.html";
|
|
9133
|
-
let message;
|
|
9134
|
-
if (context.terminated) {
|
|
9135
|
-
message = `Unrecognized character reference \`${context.entity}\`.`;
|
|
9136
|
-
} else {
|
|
9137
|
-
message = `Character reference \`${context.entity}\` must be terminated by a semicolon.`;
|
|
9138
|
-
}
|
|
9139
|
-
return [
|
|
9140
|
-
message,
|
|
9141
|
-
`HTML5 defines a set of [valid character references](${url}) but this is not a valid one.`,
|
|
9142
|
-
"",
|
|
9143
|
-
"Ensure that:",
|
|
9144
|
-
"",
|
|
9145
|
-
"1. The character is one of the listed names.",
|
|
9146
|
-
...options.ignoreCase ? [] : ["1. The case is correct (names are case sensitive)."],
|
|
9147
|
-
...options.requireSemicolon ? ["1. The name is terminated with a `;`."] : []
|
|
9148
|
-
].join("\n");
|
|
9149
|
-
}
|
|
9150
|
-
class UnknownCharReference extends Rule {
|
|
9300
|
+
class ValidID extends Rule {
|
|
9151
9301
|
constructor(options) {
|
|
9152
|
-
super({ ...defaults$
|
|
9302
|
+
super({ ...defaults$3, ...options });
|
|
9153
9303
|
}
|
|
9154
9304
|
static schema() {
|
|
9155
9305
|
return {
|
|
9156
|
-
|
|
9157
|
-
type: "boolean"
|
|
9158
|
-
},
|
|
9159
|
-
requireSemicolon: {
|
|
9306
|
+
relaxed: {
|
|
9160
9307
|
type: "boolean"
|
|
9161
9308
|
}
|
|
9162
9309
|
};
|
|
9163
9310
|
}
|
|
9164
9311
|
documentation(context) {
|
|
9312
|
+
const { relaxed } = this.options;
|
|
9313
|
+
const message = this.messages[context].replace("id", "ID").replace(/^(.)/, (m) => m.toUpperCase());
|
|
9314
|
+
const relaxedDescription = relaxed ? [] : [
|
|
9315
|
+
" - ID must begin with a letter",
|
|
9316
|
+
" - ID must only contain letters, digits, `-` and `_`"
|
|
9317
|
+
];
|
|
9165
9318
|
return {
|
|
9166
|
-
description:
|
|
9167
|
-
|
|
9319
|
+
description: [
|
|
9320
|
+
`${message}.`,
|
|
9321
|
+
"",
|
|
9322
|
+
"Under the current configuration the following rules are applied:",
|
|
9323
|
+
"",
|
|
9324
|
+
" - ID must not be empty",
|
|
9325
|
+
" - ID must not contain any whitespace characters",
|
|
9326
|
+
...relaxedDescription
|
|
9327
|
+
].join("\n"),
|
|
9328
|
+
url: "https://html-validate.org/rules/valid-id.html"
|
|
9168
9329
|
};
|
|
9169
9330
|
}
|
|
9170
9331
|
setup() {
|
|
9171
|
-
this.on("
|
|
9172
|
-
const
|
|
9173
|
-
|
|
9174
|
-
|
|
9175
|
-
continue;
|
|
9176
|
-
}
|
|
9177
|
-
this.findCharacterReferences(node, child.textContent, child.location, {
|
|
9178
|
-
isAttribute: false
|
|
9179
|
-
});
|
|
9332
|
+
this.on("attr", this.isRelevant, (event) => {
|
|
9333
|
+
const { value } = event;
|
|
9334
|
+
if (value === null || value instanceof DynamicValue) {
|
|
9335
|
+
return;
|
|
9180
9336
|
}
|
|
9181
|
-
|
|
9182
|
-
|
|
9183
|
-
|
|
9337
|
+
if (value === "") {
|
|
9338
|
+
const context = 1 /* EMPTY */;
|
|
9339
|
+
this.report(event.target, this.messages[context], event.location, context);
|
|
9184
9340
|
return;
|
|
9185
9341
|
}
|
|
9186
|
-
|
|
9187
|
-
|
|
9188
|
-
|
|
9342
|
+
if (value.match(/\s/)) {
|
|
9343
|
+
const context = 2 /* WHITESPACE */;
|
|
9344
|
+
this.report(event.target, this.messages[context], event.valueLocation, context);
|
|
9345
|
+
return;
|
|
9346
|
+
}
|
|
9347
|
+
const { relaxed } = this.options;
|
|
9348
|
+
if (relaxed) {
|
|
9349
|
+
return;
|
|
9350
|
+
}
|
|
9351
|
+
if (value.match(/^[^\p{L}]/u)) {
|
|
9352
|
+
const context = 3 /* LEADING_CHARACTER */;
|
|
9353
|
+
this.report(event.target, this.messages[context], event.valueLocation, context);
|
|
9354
|
+
return;
|
|
9355
|
+
}
|
|
9356
|
+
if (value.match(/[^\p{L}\p{N}_-]/u)) {
|
|
9357
|
+
const context = 4 /* DISALLOWED_CHARACTER */;
|
|
9358
|
+
this.report(event.target, this.messages[context], event.valueLocation, context);
|
|
9359
|
+
}
|
|
9189
9360
|
});
|
|
9190
9361
|
}
|
|
9191
|
-
get
|
|
9192
|
-
|
|
9193
|
-
|
|
9194
|
-
|
|
9195
|
-
|
|
9196
|
-
|
|
9197
|
-
}
|
|
9198
|
-
findCharacterReferences(node, text, location, { isAttribute }) {
|
|
9199
|
-
const isQuerystring = isAttribute && text.includes("?");
|
|
9200
|
-
for (const match of this.getMatches(text)) {
|
|
9201
|
-
this.validateCharacterReference(node, location, match, { isQuerystring });
|
|
9202
|
-
}
|
|
9203
|
-
}
|
|
9204
|
-
validateCharacterReference(node, location, foobar, { isQuerystring }) {
|
|
9205
|
-
const { requireSemicolon } = this.options;
|
|
9206
|
-
const { match, entity, raw, terminated } = foobar;
|
|
9207
|
-
if (isNumerical(entity)) {
|
|
9208
|
-
return;
|
|
9209
|
-
}
|
|
9210
|
-
if (isQuerystring && !terminated) {
|
|
9211
|
-
return;
|
|
9212
|
-
}
|
|
9213
|
-
const found = this.entities.includes(entity);
|
|
9214
|
-
if (found && (terminated || !requireSemicolon)) {
|
|
9215
|
-
return;
|
|
9216
|
-
}
|
|
9217
|
-
if (found && !terminated) {
|
|
9218
|
-
const entityLocation2 = getLocation(location, entity, match);
|
|
9219
|
-
const message2 = `Character reference "{{ entity }}" must be terminated by a semicolon`;
|
|
9220
|
-
const context2 = {
|
|
9221
|
-
entity: raw,
|
|
9222
|
-
terminated: false
|
|
9223
|
-
};
|
|
9224
|
-
this.report(node, message2, entityLocation2, context2);
|
|
9225
|
-
return;
|
|
9226
|
-
}
|
|
9227
|
-
const entityLocation = getLocation(location, entity, match);
|
|
9228
|
-
const message = `Unrecognized character reference "{{ entity }}"`;
|
|
9229
|
-
const context = {
|
|
9230
|
-
entity: raw,
|
|
9231
|
-
terminated: true
|
|
9362
|
+
get messages() {
|
|
9363
|
+
return {
|
|
9364
|
+
[1 /* EMPTY */]: "element id must not be empty",
|
|
9365
|
+
[2 /* WHITESPACE */]: "element id must not contain whitespace",
|
|
9366
|
+
[3 /* LEADING_CHARACTER */]: "element id must begin with a letter",
|
|
9367
|
+
[4 /* DISALLOWED_CHARACTER */]: "element id must only contain letters, digits, dash and underscore characters"
|
|
9232
9368
|
};
|
|
9233
|
-
this.report(node, message, entityLocation, context);
|
|
9234
9369
|
}
|
|
9235
|
-
|
|
9236
|
-
|
|
9237
|
-
|
|
9238
|
-
|
|
9239
|
-
|
|
9240
|
-
|
|
9241
|
-
|
|
9242
|
-
|
|
9243
|
-
|
|
9244
|
-
|
|
9245
|
-
|
|
9246
|
-
|
|
9247
|
-
|
|
9248
|
-
|
|
9370
|
+
isRelevant(event) {
|
|
9371
|
+
return event.key === "id";
|
|
9372
|
+
}
|
|
9373
|
+
}
|
|
9374
|
+
|
|
9375
|
+
class VoidContent extends Rule {
|
|
9376
|
+
documentation(tagName) {
|
|
9377
|
+
const doc = {
|
|
9378
|
+
description: "HTML void elements cannot have any content and must not have content or end tag.",
|
|
9379
|
+
url: "https://html-validate.org/rules/void-content.html"
|
|
9380
|
+
};
|
|
9381
|
+
if (tagName) {
|
|
9382
|
+
doc.description = `<${tagName}> is a void element and must not have content or end tag.`;
|
|
9383
|
+
}
|
|
9384
|
+
return doc;
|
|
9385
|
+
}
|
|
9386
|
+
setup() {
|
|
9387
|
+
this.on("tag:end", (event) => {
|
|
9388
|
+
const node = event.target;
|
|
9389
|
+
if (!node) {
|
|
9390
|
+
return;
|
|
9249
9391
|
}
|
|
9250
|
-
|
|
9392
|
+
if (!node.voidElement) {
|
|
9393
|
+
return;
|
|
9394
|
+
}
|
|
9395
|
+
if (node.closed === NodeClosed.EndTag) {
|
|
9396
|
+
this.report(
|
|
9397
|
+
null,
|
|
9398
|
+
`End tag for <${node.tagName}> must be omitted`,
|
|
9399
|
+
node.location,
|
|
9400
|
+
node.tagName
|
|
9401
|
+
);
|
|
9402
|
+
}
|
|
9403
|
+
});
|
|
9251
9404
|
}
|
|
9252
9405
|
}
|
|
9253
9406
|
|
|
9254
|
-
const
|
|
9255
|
-
|
|
9256
|
-
"name",
|
|
9257
|
-
"honorific-prefix",
|
|
9258
|
-
"given-name",
|
|
9259
|
-
"additional-name",
|
|
9260
|
-
"family-name",
|
|
9261
|
-
"honorific-suffix",
|
|
9262
|
-
"nickname",
|
|
9263
|
-
"username",
|
|
9264
|
-
"new-password",
|
|
9265
|
-
"current-password",
|
|
9266
|
-
"one-time-code",
|
|
9267
|
-
"organization-title",
|
|
9268
|
-
"organization",
|
|
9269
|
-
"street-address",
|
|
9270
|
-
"address-line1",
|
|
9271
|
-
"address-line2",
|
|
9272
|
-
"address-line3",
|
|
9273
|
-
"address-level4",
|
|
9274
|
-
"address-level3",
|
|
9275
|
-
"address-level2",
|
|
9276
|
-
"address-level1",
|
|
9277
|
-
"country",
|
|
9278
|
-
"country-name",
|
|
9279
|
-
"postal-code",
|
|
9280
|
-
"cc-name",
|
|
9281
|
-
"cc-given-name",
|
|
9282
|
-
"cc-additional-name",
|
|
9283
|
-
"cc-family-name",
|
|
9284
|
-
"cc-number",
|
|
9285
|
-
"cc-exp",
|
|
9286
|
-
"cc-exp-month",
|
|
9287
|
-
"cc-exp-year",
|
|
9288
|
-
"cc-csc",
|
|
9289
|
-
"cc-type",
|
|
9290
|
-
"transaction-currency",
|
|
9291
|
-
"transaction-amount",
|
|
9292
|
-
"language",
|
|
9293
|
-
"bday",
|
|
9294
|
-
"bday-day",
|
|
9295
|
-
"bday-month",
|
|
9296
|
-
"bday-year",
|
|
9297
|
-
"sex",
|
|
9298
|
-
"url",
|
|
9299
|
-
"photo"
|
|
9300
|
-
];
|
|
9301
|
-
const fieldNames2 = [
|
|
9302
|
-
"tel",
|
|
9303
|
-
"tel-country-code",
|
|
9304
|
-
"tel-national",
|
|
9305
|
-
"tel-area-code",
|
|
9306
|
-
"tel-local",
|
|
9307
|
-
"tel-local-prefix",
|
|
9308
|
-
"tel-local-suffix",
|
|
9309
|
-
"tel-extension",
|
|
9310
|
-
"email",
|
|
9311
|
-
"impp"
|
|
9312
|
-
];
|
|
9313
|
-
const fieldNameGroup = {
|
|
9314
|
-
name: "text",
|
|
9315
|
-
"honorific-prefix": "text",
|
|
9316
|
-
"given-name": "text",
|
|
9317
|
-
"additional-name": "text",
|
|
9318
|
-
"family-name": "text",
|
|
9319
|
-
"honorific-suffix": "text",
|
|
9320
|
-
nickname: "text",
|
|
9321
|
-
username: "username",
|
|
9322
|
-
"new-password": "password",
|
|
9323
|
-
"current-password": "password",
|
|
9324
|
-
"one-time-code": "password",
|
|
9325
|
-
"organization-title": "text",
|
|
9326
|
-
organization: "text",
|
|
9327
|
-
"street-address": "multiline",
|
|
9328
|
-
"address-line1": "text",
|
|
9329
|
-
"address-line2": "text",
|
|
9330
|
-
"address-line3": "text",
|
|
9331
|
-
"address-level4": "text",
|
|
9332
|
-
"address-level3": "text",
|
|
9333
|
-
"address-level2": "text",
|
|
9334
|
-
"address-level1": "text",
|
|
9335
|
-
country: "text",
|
|
9336
|
-
"country-name": "text",
|
|
9337
|
-
"postal-code": "text",
|
|
9338
|
-
"cc-name": "text",
|
|
9339
|
-
"cc-given-name": "text",
|
|
9340
|
-
"cc-additional-name": "text",
|
|
9341
|
-
"cc-family-name": "text",
|
|
9342
|
-
"cc-number": "text",
|
|
9343
|
-
"cc-exp": "month",
|
|
9344
|
-
"cc-exp-month": "numeric",
|
|
9345
|
-
"cc-exp-year": "numeric",
|
|
9346
|
-
"cc-csc": "text",
|
|
9347
|
-
"cc-type": "text",
|
|
9348
|
-
"transaction-currency": "text",
|
|
9349
|
-
"transaction-amount": "numeric",
|
|
9350
|
-
language: "text",
|
|
9351
|
-
bday: "date",
|
|
9352
|
-
"bday-day": "numeric",
|
|
9353
|
-
"bday-month": "numeric",
|
|
9354
|
-
"bday-year": "numeric",
|
|
9355
|
-
sex: "text",
|
|
9356
|
-
url: "url",
|
|
9357
|
-
photo: "url",
|
|
9358
|
-
tel: "tel",
|
|
9359
|
-
"tel-country-code": "text",
|
|
9360
|
-
"tel-national": "text",
|
|
9361
|
-
"tel-area-code": "text",
|
|
9362
|
-
"tel-local": "text",
|
|
9363
|
-
"tel-local-prefix": "text",
|
|
9364
|
-
"tel-local-suffix": "text",
|
|
9365
|
-
"tel-extension": "text",
|
|
9366
|
-
email: "username",
|
|
9367
|
-
impp: "url"
|
|
9407
|
+
const defaults$2 = {
|
|
9408
|
+
style: "omit"
|
|
9368
9409
|
};
|
|
9369
|
-
|
|
9370
|
-
|
|
9371
|
-
|
|
9372
|
-
|
|
9373
|
-
function matchHint(token) {
|
|
9374
|
-
return token === "shipping" || token === "billing";
|
|
9375
|
-
}
|
|
9376
|
-
function matchFieldNames1(token) {
|
|
9377
|
-
return fieldNames1.includes(token);
|
|
9378
|
-
}
|
|
9379
|
-
function matchContact(token) {
|
|
9380
|
-
const haystack = ["home", "work", "mobile", "fax", "pager"];
|
|
9381
|
-
return haystack.includes(token);
|
|
9382
|
-
}
|
|
9383
|
-
function matchFieldNames2(token) {
|
|
9384
|
-
return fieldNames2.includes(token);
|
|
9385
|
-
}
|
|
9386
|
-
function matchWebauthn(token) {
|
|
9387
|
-
return token === "webauthn";
|
|
9388
|
-
}
|
|
9389
|
-
function matchToken(token) {
|
|
9390
|
-
if (matchSection(token)) {
|
|
9391
|
-
return "section";
|
|
9410
|
+
class VoidStyle extends Rule {
|
|
9411
|
+
constructor(options) {
|
|
9412
|
+
super({ ...defaults$2, ...options });
|
|
9413
|
+
this.style = parseStyle(this.options.style);
|
|
9392
9414
|
}
|
|
9393
|
-
|
|
9394
|
-
return
|
|
9415
|
+
static schema() {
|
|
9416
|
+
return {
|
|
9417
|
+
style: {
|
|
9418
|
+
enum: ["omit", "selfclose", "selfclosing"],
|
|
9419
|
+
type: "string"
|
|
9420
|
+
}
|
|
9421
|
+
};
|
|
9395
9422
|
}
|
|
9396
|
-
|
|
9397
|
-
|
|
9423
|
+
documentation(context) {
|
|
9424
|
+
const [desc, end] = styleDescription(context.style);
|
|
9425
|
+
return {
|
|
9426
|
+
description: `The current configuration requires void elements to ${desc}, use <${context.tagName}${end}> instead.`,
|
|
9427
|
+
url: "https://html-validate.org/rules/void-style.html"
|
|
9428
|
+
};
|
|
9398
9429
|
}
|
|
9399
|
-
|
|
9400
|
-
|
|
9430
|
+
setup() {
|
|
9431
|
+
this.on("tag:end", (event) => {
|
|
9432
|
+
const active = event.previous;
|
|
9433
|
+
if (active.meta) {
|
|
9434
|
+
this.validateActive(active);
|
|
9435
|
+
}
|
|
9436
|
+
});
|
|
9401
9437
|
}
|
|
9402
|
-
|
|
9403
|
-
|
|
9438
|
+
validateActive(node) {
|
|
9439
|
+
if (!node.voidElement) {
|
|
9440
|
+
return;
|
|
9441
|
+
}
|
|
9442
|
+
if (this.shouldBeOmitted(node)) {
|
|
9443
|
+
this.reportError(
|
|
9444
|
+
node,
|
|
9445
|
+
`Expected omitted end tag <${node.tagName}> instead of self-closing element <${node.tagName}/>`
|
|
9446
|
+
);
|
|
9447
|
+
}
|
|
9448
|
+
if (this.shouldBeSelfClosed(node)) {
|
|
9449
|
+
this.reportError(
|
|
9450
|
+
node,
|
|
9451
|
+
`Expected self-closing element <${node.tagName}/> instead of omitted end-tag <${node.tagName}>`
|
|
9452
|
+
);
|
|
9453
|
+
}
|
|
9404
9454
|
}
|
|
9405
|
-
|
|
9406
|
-
|
|
9455
|
+
reportError(node, message) {
|
|
9456
|
+
const context = {
|
|
9457
|
+
style: this.style,
|
|
9458
|
+
tagName: node.tagName
|
|
9459
|
+
};
|
|
9460
|
+
super.report(node, message, null, context);
|
|
9461
|
+
}
|
|
9462
|
+
shouldBeOmitted(node) {
|
|
9463
|
+
return this.style === 1 /* AlwaysOmit */ && node.closed === NodeClosed.VoidSelfClosed;
|
|
9464
|
+
}
|
|
9465
|
+
shouldBeSelfClosed(node) {
|
|
9466
|
+
return this.style === 2 /* AlwaysSelfclose */ && node.closed === NodeClosed.VoidOmitted;
|
|
9407
9467
|
}
|
|
9408
|
-
return null;
|
|
9409
9468
|
}
|
|
9410
|
-
function
|
|
9411
|
-
|
|
9412
|
-
"
|
|
9413
|
-
|
|
9414
|
-
"
|
|
9415
|
-
"
|
|
9416
|
-
|
|
9417
|
-
|
|
9418
|
-
|
|
9419
|
-
"month",
|
|
9420
|
-
"date"
|
|
9421
|
-
];
|
|
9422
|
-
const mapping = {
|
|
9423
|
-
hidden: allGroups,
|
|
9424
|
-
text: allGroups.filter((it) => it !== "multiline"),
|
|
9425
|
-
search: allGroups.filter((it) => it !== "multiline"),
|
|
9426
|
-
password: ["password"],
|
|
9427
|
-
url: ["url"],
|
|
9428
|
-
email: ["username"],
|
|
9429
|
-
tel: ["tel"],
|
|
9430
|
-
number: ["numeric"],
|
|
9431
|
-
month: ["month"],
|
|
9432
|
-
date: ["date"]
|
|
9433
|
-
};
|
|
9434
|
-
const groups = mapping[type];
|
|
9435
|
-
if (groups) {
|
|
9436
|
-
return groups;
|
|
9469
|
+
function parseStyle(name) {
|
|
9470
|
+
switch (name) {
|
|
9471
|
+
case "omit":
|
|
9472
|
+
return 1 /* AlwaysOmit */;
|
|
9473
|
+
case "selfclose":
|
|
9474
|
+
case "selfclosing":
|
|
9475
|
+
return 2 /* AlwaysSelfclose */;
|
|
9476
|
+
default:
|
|
9477
|
+
throw new Error(`Invalid style "${name}" for "void-style" rule`);
|
|
9437
9478
|
}
|
|
9438
|
-
return [];
|
|
9439
9479
|
}
|
|
9440
|
-
function
|
|
9441
|
-
|
|
9442
|
-
|
|
9480
|
+
function styleDescription(style) {
|
|
9481
|
+
switch (style) {
|
|
9482
|
+
case 1 /* AlwaysOmit */:
|
|
9483
|
+
return ["omit end tag", ""];
|
|
9484
|
+
case 2 /* AlwaysSelfclose */:
|
|
9485
|
+
return ["be self-closed", "/"];
|
|
9486
|
+
default:
|
|
9487
|
+
throw new Error(`Unknown style`);
|
|
9443
9488
|
}
|
|
9444
|
-
return disallowedInputTypes.includes(type);
|
|
9445
9489
|
}
|
|
9446
|
-
|
|
9447
|
-
|
|
9448
|
-
|
|
9449
|
-
|
|
9450
|
-
|
|
9451
|
-
|
|
9452
|
-
|
|
9453
|
-
return '"{{ second }}" must appear before "{{ first }}"';
|
|
9454
|
-
case 3 /* InvalidToken */:
|
|
9455
|
-
return '"{{ token }}" is not a valid autocomplete token or field name';
|
|
9456
|
-
case 4 /* InvalidCombination */:
|
|
9457
|
-
return '"{{ second }}" cannot be combined with "{{ first }}"';
|
|
9458
|
-
case 5 /* MissingField */:
|
|
9459
|
-
return "autocomplete attribute is missing field name";
|
|
9490
|
+
|
|
9491
|
+
class H30 extends Rule {
|
|
9492
|
+
documentation() {
|
|
9493
|
+
return {
|
|
9494
|
+
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.",
|
|
9495
|
+
url: "https://html-validate.org/rules/wcag/h30.html"
|
|
9496
|
+
};
|
|
9460
9497
|
}
|
|
9461
|
-
|
|
9462
|
-
|
|
9463
|
-
|
|
9464
|
-
|
|
9465
|
-
|
|
9466
|
-
|
|
9467
|
-
|
|
9468
|
-
|
|
9469
|
-
|
|
9470
|
-
|
|
9471
|
-
|
|
9472
|
-
|
|
9473
|
-
|
|
9474
|
-
|
|
9475
|
-
|
|
9476
|
-
|
|
9477
|
-
|
|
9478
|
-
|
|
9479
|
-
|
|
9480
|
-
|
|
9481
|
-
|
|
9482
|
-
|
|
9483
|
-
|
|
9484
|
-
"",
|
|
9485
|
-
'`<input type="hidden">` cannot use the values `"on"` and `"off"`.'
|
|
9486
|
-
].join("\n");
|
|
9498
|
+
setup() {
|
|
9499
|
+
this.on("dom:ready", (event) => {
|
|
9500
|
+
const links = event.document.getElementsByTagName("a");
|
|
9501
|
+
for (const link of links) {
|
|
9502
|
+
if (!link.hasAttribute("href")) {
|
|
9503
|
+
continue;
|
|
9504
|
+
}
|
|
9505
|
+
if (!inAccessibilityTree(link)) {
|
|
9506
|
+
continue;
|
|
9507
|
+
}
|
|
9508
|
+
const textClassification = classifyNodeText(link, { ignoreHiddenRoot: true });
|
|
9509
|
+
if (textClassification !== TextClassification.EMPTY_TEXT) {
|
|
9510
|
+
continue;
|
|
9511
|
+
}
|
|
9512
|
+
const images = link.querySelectorAll("img");
|
|
9513
|
+
if (images.some((image) => hasAltText(image))) {
|
|
9514
|
+
continue;
|
|
9515
|
+
}
|
|
9516
|
+
const labels = link.querySelectorAll("[aria-label]");
|
|
9517
|
+
if (hasAriaLabel(link) || labels.some((cur) => hasAriaLabel(cur))) {
|
|
9518
|
+
continue;
|
|
9519
|
+
}
|
|
9520
|
+
this.report(link, "Anchor link must have a text describing its purpose");
|
|
9487
9521
|
}
|
|
9488
|
-
|
|
9489
|
-
const currentGroup = fieldNameGroup[context.value];
|
|
9490
|
-
return [
|
|
9491
|
-
message,
|
|
9492
|
-
"",
|
|
9493
|
-
`\`${context.what}\` allows autocomplete fields from the following group${controlGroups.length > 1 ? "s" : ""}:`,
|
|
9494
|
-
"",
|
|
9495
|
-
...controlGroups.map((it) => `- ${it}`),
|
|
9496
|
-
"",
|
|
9497
|
-
`The field \`"${context.value}"\` belongs to the group /${currentGroup}/ which cannot be used with this input type.`
|
|
9498
|
-
].join("\n");
|
|
9499
|
-
}
|
|
9500
|
-
case 2 /* InvalidOrder */:
|
|
9501
|
-
return [
|
|
9502
|
-
`\`"${context.second}"\` must appear before \`"${context.first}"\``,
|
|
9503
|
-
"",
|
|
9504
|
-
"The autocomplete tokens must appear in the following order:",
|
|
9505
|
-
"",
|
|
9506
|
-
"- Optional section name (`section-` prefix).",
|
|
9507
|
-
"- Optional `shipping` or `billing` token.",
|
|
9508
|
-
"- Optional `home`, `work`, `mobile`, `fax` or `pager` token (for fields supporting it).",
|
|
9509
|
-
"- Field name",
|
|
9510
|
-
"- Optional `webauthn` token."
|
|
9511
|
-
].join("\n");
|
|
9512
|
-
case 3 /* InvalidToken */:
|
|
9513
|
-
return `\`"${context.token}"\` is not a valid autocomplete token or field name`;
|
|
9514
|
-
case 4 /* InvalidCombination */:
|
|
9515
|
-
return `\`"${context.second}"\` cannot be combined with \`"${context.first}"\``;
|
|
9516
|
-
case 5 /* MissingField */:
|
|
9517
|
-
return "Autocomplete attribute is missing field name";
|
|
9522
|
+
});
|
|
9518
9523
|
}
|
|
9519
9524
|
}
|
|
9520
|
-
|
|
9521
|
-
|
|
9525
|
+
|
|
9526
|
+
class H32 extends Rule {
|
|
9527
|
+
documentation() {
|
|
9522
9528
|
return {
|
|
9523
|
-
description:
|
|
9524
|
-
url: "https://html-validate.org/rules/
|
|
9529
|
+
description: "WCAG 2.1 requires each `<form>` element to have at least one submit button.",
|
|
9530
|
+
url: "https://html-validate.org/rules/wcag/h32.html"
|
|
9525
9531
|
};
|
|
9526
9532
|
}
|
|
9527
9533
|
setup() {
|
|
9534
|
+
const formTags = this.getTagsWithProperty("form");
|
|
9535
|
+
const formSelector = formTags.join(",");
|
|
9528
9536
|
this.on("dom:ready", (event) => {
|
|
9529
9537
|
const { document } = event;
|
|
9530
|
-
const
|
|
9531
|
-
for (const
|
|
9532
|
-
|
|
9533
|
-
if (autocomplete.value === null || autocomplete.value instanceof DynamicValue) {
|
|
9538
|
+
const forms = document.querySelectorAll(formSelector);
|
|
9539
|
+
for (const form of forms) {
|
|
9540
|
+
if (hasNestedSubmit(form)) {
|
|
9534
9541
|
continue;
|
|
9535
9542
|
}
|
|
9536
|
-
|
|
9537
|
-
const value = autocomplete.value.toLowerCase();
|
|
9538
|
-
const tokens = new DOMTokenList(value, location);
|
|
9539
|
-
if (tokens.length === 0) {
|
|
9543
|
+
if (hasAssociatedSubmit(document, form)) {
|
|
9540
9544
|
continue;
|
|
9541
9545
|
}
|
|
9542
|
-
this.
|
|
9546
|
+
this.report(form, `<${form.tagName}> element must have a submit button`);
|
|
9543
9547
|
}
|
|
9544
9548
|
});
|
|
9545
9549
|
}
|
|
9546
|
-
|
|
9547
|
-
|
|
9548
|
-
|
|
9549
|
-
|
|
9550
|
-
|
|
9551
|
-
|
|
9552
|
-
|
|
9553
|
-
|
|
9554
|
-
|
|
9555
|
-
|
|
9556
|
-
|
|
9557
|
-
|
|
9558
|
-
|
|
9559
|
-
|
|
9560
|
-
|
|
9561
|
-
|
|
9562
|
-
|
|
9563
|
-
msg: 0 /* InvalidAttribute */,
|
|
9564
|
-
what: `<input type="${type}">`
|
|
9565
|
-
};
|
|
9566
|
-
this.report({
|
|
9567
|
-
node,
|
|
9568
|
-
message: getTerminalMessage(context),
|
|
9569
|
-
location: keyLocation,
|
|
9570
|
-
context
|
|
9571
|
-
});
|
|
9572
|
-
return;
|
|
9573
|
-
}
|
|
9574
|
-
if (tokens.includes("on") || tokens.includes("off")) {
|
|
9575
|
-
this.validateOnOff(node, mantle, tokens);
|
|
9576
|
-
return;
|
|
9577
|
-
}
|
|
9578
|
-
this.validateTokens(node, tokens, keyLocation);
|
|
9550
|
+
}
|
|
9551
|
+
function isSubmit(node) {
|
|
9552
|
+
const type = node.getAttribute("type");
|
|
9553
|
+
return Boolean(!type || type.valueMatches(/submit|image/));
|
|
9554
|
+
}
|
|
9555
|
+
function isAssociated(id, node) {
|
|
9556
|
+
const form = node.getAttribute("form");
|
|
9557
|
+
return Boolean(form == null ? void 0 : form.valueMatches(id, true));
|
|
9558
|
+
}
|
|
9559
|
+
function hasNestedSubmit(form) {
|
|
9560
|
+
const matches = form.querySelectorAll("button,input").filter(isSubmit).filter((node) => !node.hasAttribute("form"));
|
|
9561
|
+
return matches.length > 0;
|
|
9562
|
+
}
|
|
9563
|
+
function hasAssociatedSubmit(document, form) {
|
|
9564
|
+
const { id } = form;
|
|
9565
|
+
if (!id) {
|
|
9566
|
+
return false;
|
|
9579
9567
|
}
|
|
9580
|
-
|
|
9581
|
-
|
|
9582
|
-
|
|
9583
|
-
|
|
9584
|
-
|
|
9585
|
-
|
|
9586
|
-
|
|
9587
|
-
|
|
9588
|
-
|
|
9589
|
-
|
|
9568
|
+
const matches = document.querySelectorAll("button[form],input[form]").filter(isSubmit).filter((node) => isAssociated(id, node));
|
|
9569
|
+
return matches.length > 0;
|
|
9570
|
+
}
|
|
9571
|
+
|
|
9572
|
+
class H36 extends Rule {
|
|
9573
|
+
documentation() {
|
|
9574
|
+
return {
|
|
9575
|
+
description: [
|
|
9576
|
+
"WCAG 2.1 requires all images used as submit buttons to have a non-empty textual description using the `alt` attribute.",
|
|
9577
|
+
'The alt text cannot be empty (`alt=""`).'
|
|
9578
|
+
].join("\n"),
|
|
9579
|
+
url: "https://html-validate.org/rules/wcag/h36.html"
|
|
9590
9580
|
};
|
|
9591
|
-
this.report({
|
|
9592
|
-
node,
|
|
9593
|
-
message: getTerminalMessage(context),
|
|
9594
|
-
location,
|
|
9595
|
-
context
|
|
9596
|
-
});
|
|
9597
9581
|
}
|
|
9598
|
-
|
|
9599
|
-
|
|
9600
|
-
|
|
9601
|
-
|
|
9602
|
-
|
|
9603
|
-
|
|
9604
|
-
|
|
9605
|
-
/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
|
|
9606
|
-
first: tokens.item(index > 0 ? 0 : 1),
|
|
9607
|
-
second: value
|
|
9608
|
-
};
|
|
9609
|
-
this.report({
|
|
9610
|
-
node,
|
|
9611
|
-
message: getTerminalMessage(context),
|
|
9612
|
-
location,
|
|
9613
|
-
context
|
|
9614
|
-
});
|
|
9615
|
-
}
|
|
9616
|
-
switch (mantle) {
|
|
9617
|
-
case "expectation":
|
|
9618
|
-
return;
|
|
9619
|
-
case "anchor": {
|
|
9620
|
-
const context = {
|
|
9621
|
-
msg: 1 /* InvalidValue */,
|
|
9622
|
-
type: "hidden",
|
|
9623
|
-
value,
|
|
9624
|
-
what: `<input type="hidden">`
|
|
9625
|
-
};
|
|
9626
|
-
this.report({
|
|
9627
|
-
node,
|
|
9628
|
-
message: getTerminalMessage(context),
|
|
9629
|
-
location: tokens.location(0),
|
|
9630
|
-
context
|
|
9631
|
-
});
|
|
9582
|
+
setup() {
|
|
9583
|
+
this.on("tag:end", (event) => {
|
|
9584
|
+
const node = event.previous;
|
|
9585
|
+
if (node.tagName !== "input")
|
|
9586
|
+
return;
|
|
9587
|
+
if (node.getAttributeValue("type") !== "image") {
|
|
9588
|
+
return;
|
|
9632
9589
|
}
|
|
9633
|
-
|
|
9634
|
-
}
|
|
9635
|
-
validateTokens(node, tokens, keyLocation) {
|
|
9636
|
-
const order = [];
|
|
9637
|
-
for (const { item, location } of tokens.iterator()) {
|
|
9638
|
-
const tokenType = matchToken(item);
|
|
9639
|
-
if (tokenType) {
|
|
9640
|
-
order.push(tokenType);
|
|
9641
|
-
} else {
|
|
9642
|
-
const context = {
|
|
9643
|
-
msg: 3 /* InvalidToken */,
|
|
9644
|
-
token: item
|
|
9645
|
-
};
|
|
9646
|
-
this.report({
|
|
9647
|
-
node,
|
|
9648
|
-
message: getTerminalMessage(context),
|
|
9649
|
-
location,
|
|
9650
|
-
context
|
|
9651
|
-
});
|
|
9590
|
+
if (!inAccessibilityTree(node)) {
|
|
9652
9591
|
return;
|
|
9653
9592
|
}
|
|
9654
|
-
|
|
9655
|
-
|
|
9656
|
-
|
|
9657
|
-
this.validateContact(node, tokens, order);
|
|
9658
|
-
this.validateOrder(node, tokens, order);
|
|
9659
|
-
this.validateControlGroup(node, tokens, fieldTokens);
|
|
9660
|
-
}
|
|
9661
|
-
/**
|
|
9662
|
-
* Ensure that exactly one field name is present from the two field lists.
|
|
9663
|
-
*/
|
|
9664
|
-
validateFieldPresence(node, tokens, fieldTokens, keyLocation) {
|
|
9665
|
-
const numFields = fieldTokens.filter(Boolean).length;
|
|
9666
|
-
if (numFields === 0) {
|
|
9667
|
-
const context = {
|
|
9668
|
-
msg: 5 /* MissingField */
|
|
9669
|
-
};
|
|
9670
|
-
this.report({
|
|
9671
|
-
node,
|
|
9672
|
-
message: getTerminalMessage(context),
|
|
9673
|
-
location: keyLocation,
|
|
9674
|
-
context
|
|
9675
|
-
});
|
|
9676
|
-
} else if (numFields > 1) {
|
|
9677
|
-
const a = fieldTokens.indexOf(true);
|
|
9678
|
-
const b = fieldTokens.lastIndexOf(true);
|
|
9679
|
-
const context = {
|
|
9680
|
-
msg: 4 /* InvalidCombination */,
|
|
9681
|
-
/* eslint-disable @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
|
|
9682
|
-
first: tokens.item(a),
|
|
9683
|
-
second: tokens.item(b)
|
|
9684
|
-
/* eslint-enable @typescript-eslint/no-non-null-assertion */
|
|
9685
|
-
};
|
|
9686
|
-
this.report({
|
|
9687
|
-
node,
|
|
9688
|
-
message: getTerminalMessage(context),
|
|
9689
|
-
location: tokens.location(b),
|
|
9690
|
-
context
|
|
9691
|
-
});
|
|
9692
|
-
}
|
|
9693
|
-
}
|
|
9694
|
-
/**
|
|
9695
|
-
* Ensure contact token is only used with field names from the second list.
|
|
9696
|
-
*/
|
|
9697
|
-
validateContact(node, tokens, order) {
|
|
9698
|
-
if (order.includes("contact") && order.includes("field1")) {
|
|
9699
|
-
const a = order.indexOf("field1");
|
|
9700
|
-
const b = order.indexOf("contact");
|
|
9701
|
-
const context = {
|
|
9702
|
-
msg: 4 /* InvalidCombination */,
|
|
9703
|
-
/* eslint-disable @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
|
|
9704
|
-
first: tokens.item(a),
|
|
9705
|
-
second: tokens.item(b)
|
|
9706
|
-
/* eslint-enable @typescript-eslint/no-non-null-assertion */
|
|
9707
|
-
};
|
|
9708
|
-
this.report({
|
|
9709
|
-
node,
|
|
9710
|
-
message: getTerminalMessage(context),
|
|
9711
|
-
location: tokens.location(b),
|
|
9712
|
-
context
|
|
9713
|
-
});
|
|
9714
|
-
}
|
|
9715
|
-
}
|
|
9716
|
-
validateOrder(node, tokens, order) {
|
|
9717
|
-
const indicies = order.map((it) => expectedOrder.indexOf(it));
|
|
9718
|
-
for (let i = 0; i < indicies.length - 1; i++) {
|
|
9719
|
-
if (indicies[0] > indicies[i + 1]) {
|
|
9720
|
-
const context = {
|
|
9721
|
-
msg: 2 /* InvalidOrder */,
|
|
9722
|
-
/* eslint-disable @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
|
|
9723
|
-
first: tokens.item(i),
|
|
9724
|
-
second: tokens.item(i + 1)
|
|
9725
|
-
/* eslint-enable @typescript-eslint/no-non-null-assertion */
|
|
9726
|
-
};
|
|
9593
|
+
if (!hasAltText(node)) {
|
|
9594
|
+
const message = "image used as submit button must have non-empty alt text";
|
|
9595
|
+
const alt = node.getAttribute("alt");
|
|
9727
9596
|
this.report({
|
|
9728
9597
|
node,
|
|
9729
|
-
message
|
|
9730
|
-
location:
|
|
9731
|
-
context
|
|
9598
|
+
message,
|
|
9599
|
+
location: alt ? alt.keyLocation : node.location
|
|
9732
9600
|
});
|
|
9733
9601
|
}
|
|
9734
|
-
}
|
|
9735
|
-
}
|
|
9736
|
-
validateControlGroup(node, tokens, fieldTokens) {
|
|
9737
|
-
const numFields = fieldTokens.filter(Boolean).length;
|
|
9738
|
-
if (numFields === 0) {
|
|
9739
|
-
return;
|
|
9740
|
-
}
|
|
9741
|
-
if (!node.is("input")) {
|
|
9742
|
-
return;
|
|
9743
|
-
}
|
|
9744
|
-
const attr = node.getAttribute("type");
|
|
9745
|
-
const type = (attr == null ? void 0 : attr.value) ?? "text";
|
|
9746
|
-
if (type instanceof DynamicValue) {
|
|
9747
|
-
return;
|
|
9748
|
-
}
|
|
9749
|
-
const controlGroups = getControlGroups(type);
|
|
9750
|
-
const fieldIndex = fieldTokens.indexOf(true);
|
|
9751
|
-
const fieldToken = tokens.item(fieldIndex);
|
|
9752
|
-
const fieldGroup = fieldNameGroup[fieldToken];
|
|
9753
|
-
if (!controlGroups.includes(fieldGroup)) {
|
|
9754
|
-
const context = {
|
|
9755
|
-
msg: 1 /* InvalidValue */,
|
|
9756
|
-
type,
|
|
9757
|
-
value: fieldToken,
|
|
9758
|
-
what: `<input type="${type}">`
|
|
9759
|
-
};
|
|
9760
|
-
this.report({
|
|
9761
|
-
node,
|
|
9762
|
-
message: getTerminalMessage(context),
|
|
9763
|
-
location: tokens.location(fieldIndex),
|
|
9764
|
-
context
|
|
9765
|
-
});
|
|
9766
|
-
}
|
|
9602
|
+
});
|
|
9767
9603
|
}
|
|
9768
9604
|
}
|
|
9769
9605
|
|
|
9770
|
-
const defaults$
|
|
9771
|
-
|
|
9606
|
+
const defaults$1 = {
|
|
9607
|
+
allowEmpty: true,
|
|
9608
|
+
alias: []
|
|
9772
9609
|
};
|
|
9773
|
-
class
|
|
9610
|
+
class H37 extends Rule {
|
|
9774
9611
|
constructor(options) {
|
|
9775
|
-
super({ ...defaults$
|
|
9612
|
+
super({ ...defaults$1, ...options });
|
|
9613
|
+
if (!Array.isArray(this.options.alias)) {
|
|
9614
|
+
this.options.alias = [this.options.alias];
|
|
9615
|
+
}
|
|
9776
9616
|
}
|
|
9777
9617
|
static schema() {
|
|
9778
9618
|
return {
|
|
9779
|
-
|
|
9619
|
+
alias: {
|
|
9620
|
+
anyOf: [
|
|
9621
|
+
{
|
|
9622
|
+
items: {
|
|
9623
|
+
type: "string"
|
|
9624
|
+
},
|
|
9625
|
+
type: "array"
|
|
9626
|
+
},
|
|
9627
|
+
{
|
|
9628
|
+
type: "string"
|
|
9629
|
+
}
|
|
9630
|
+
]
|
|
9631
|
+
},
|
|
9632
|
+
allowEmpty: {
|
|
9780
9633
|
type: "boolean"
|
|
9781
9634
|
}
|
|
9782
9635
|
};
|
|
9783
9636
|
}
|
|
9784
|
-
documentation(
|
|
9785
|
-
const { relaxed } = this.options;
|
|
9786
|
-
const message = this.messages[context].replace("id", "ID").replace(/^(.)/, (m) => m.toUpperCase());
|
|
9787
|
-
const relaxedDescription = relaxed ? [] : [
|
|
9788
|
-
" - ID must begin with a letter",
|
|
9789
|
-
" - ID must only contain letters, digits, `-` and `_`"
|
|
9790
|
-
];
|
|
9637
|
+
documentation() {
|
|
9791
9638
|
return {
|
|
9792
|
-
description:
|
|
9793
|
-
|
|
9794
|
-
"",
|
|
9795
|
-
"Under the current configuration the following rules are applied:",
|
|
9796
|
-
"",
|
|
9797
|
-
" - ID must not be empty",
|
|
9798
|
-
" - ID must not contain any whitespace characters",
|
|
9799
|
-
...relaxedDescription
|
|
9800
|
-
].join("\n"),
|
|
9801
|
-
url: "https://html-validate.org/rules/valid-id.html"
|
|
9639
|
+
description: "Both HTML5 and WCAG 2.0 requires images to have a alternative text for each image.",
|
|
9640
|
+
url: "https://html-validate.org/rules/wcag/h37.html"
|
|
9802
9641
|
};
|
|
9803
9642
|
}
|
|
9804
9643
|
setup() {
|
|
9805
|
-
this.on("
|
|
9806
|
-
const {
|
|
9807
|
-
|
|
9808
|
-
|
|
9809
|
-
|
|
9810
|
-
if (value === "") {
|
|
9811
|
-
const context = 1 /* EMPTY */;
|
|
9812
|
-
this.report(event.target, this.messages[context], event.location, context);
|
|
9813
|
-
return;
|
|
9814
|
-
}
|
|
9815
|
-
if (value.match(/\s/)) {
|
|
9816
|
-
const context = 2 /* WHITESPACE */;
|
|
9817
|
-
this.report(event.target, this.messages[context], event.valueLocation, context);
|
|
9818
|
-
return;
|
|
9819
|
-
}
|
|
9820
|
-
const { relaxed } = this.options;
|
|
9821
|
-
if (relaxed) {
|
|
9822
|
-
return;
|
|
9823
|
-
}
|
|
9824
|
-
if (value.match(/^[^\p{L}]/u)) {
|
|
9825
|
-
const context = 3 /* LEADING_CHARACTER */;
|
|
9826
|
-
this.report(event.target, this.messages[context], event.valueLocation, context);
|
|
9827
|
-
return;
|
|
9828
|
-
}
|
|
9829
|
-
if (value.match(/[^\p{L}\p{N}_-]/u)) {
|
|
9830
|
-
const context = 4 /* DISALLOWED_CHARACTER */;
|
|
9831
|
-
this.report(event.target, this.messages[context], event.valueLocation, context);
|
|
9644
|
+
this.on("dom:ready", (event) => {
|
|
9645
|
+
const { document } = event;
|
|
9646
|
+
const nodes = document.querySelectorAll("img");
|
|
9647
|
+
for (const node of nodes) {
|
|
9648
|
+
this.validateNode(node);
|
|
9832
9649
|
}
|
|
9833
9650
|
});
|
|
9834
9651
|
}
|
|
9835
|
-
|
|
9836
|
-
|
|
9837
|
-
|
|
9838
|
-
|
|
9839
|
-
|
|
9840
|
-
|
|
9841
|
-
}
|
|
9842
|
-
|
|
9843
|
-
|
|
9844
|
-
|
|
9652
|
+
validateNode(node) {
|
|
9653
|
+
if (!inAccessibilityTree(node)) {
|
|
9654
|
+
return;
|
|
9655
|
+
}
|
|
9656
|
+
if (Boolean(node.getAttributeValue("alt")) || Boolean(node.hasAttribute("alt") && this.options.allowEmpty)) {
|
|
9657
|
+
return;
|
|
9658
|
+
}
|
|
9659
|
+
for (const attr of this.options.alias) {
|
|
9660
|
+
if (node.getAttribute(attr)) {
|
|
9661
|
+
return;
|
|
9662
|
+
}
|
|
9663
|
+
}
|
|
9664
|
+
const tag = node.annotatedName;
|
|
9665
|
+
if (node.hasAttribute("alt")) {
|
|
9666
|
+
const attr = node.getAttribute("alt");
|
|
9667
|
+
this.report(node, `${tag} cannot have empty "alt" attribute`, attr.keyLocation);
|
|
9668
|
+
} else {
|
|
9669
|
+
this.report(node, `${tag} is missing required "alt" attribute`, node.location);
|
|
9670
|
+
}
|
|
9845
9671
|
}
|
|
9846
9672
|
}
|
|
9847
9673
|
|
|
9848
|
-
|
|
9849
|
-
|
|
9850
|
-
|
|
9851
|
-
|
|
9852
|
-
|
|
9674
|
+
var _a;
|
|
9675
|
+
const { enum: validScopes } = (_a = elements.html5.th.attributes) == null ? void 0 : _a.scope;
|
|
9676
|
+
const joinedScopes = utils_naturalJoin.naturalJoin(validScopes);
|
|
9677
|
+
class H63 extends Rule {
|
|
9678
|
+
documentation() {
|
|
9679
|
+
return {
|
|
9680
|
+
description: "H63: Using the scope attribute to associate header cells and data cells in data tables",
|
|
9681
|
+
url: "https://html-validate.org/rules/wcag/h63.html"
|
|
9853
9682
|
};
|
|
9854
|
-
if (tagName) {
|
|
9855
|
-
doc.description = `<${tagName}> is a void element and must not have content or end tag.`;
|
|
9856
|
-
}
|
|
9857
|
-
return doc;
|
|
9858
9683
|
}
|
|
9859
9684
|
setup() {
|
|
9860
|
-
this.on("tag:
|
|
9685
|
+
this.on("tag:ready", (event) => {
|
|
9861
9686
|
const node = event.target;
|
|
9862
|
-
if (
|
|
9687
|
+
if (node.tagName !== "th") {
|
|
9863
9688
|
return;
|
|
9864
9689
|
}
|
|
9865
|
-
|
|
9690
|
+
const scope = node.getAttribute("scope");
|
|
9691
|
+
const value = scope == null ? void 0 : scope.value;
|
|
9692
|
+
if (value instanceof DynamicValue) {
|
|
9866
9693
|
return;
|
|
9867
9694
|
}
|
|
9868
|
-
if (
|
|
9869
|
-
|
|
9870
|
-
null,
|
|
9871
|
-
`End tag for <${node.tagName}> must be omitted`,
|
|
9872
|
-
node.location,
|
|
9873
|
-
node.tagName
|
|
9874
|
-
);
|
|
9695
|
+
if (value && validScopes.includes(value)) {
|
|
9696
|
+
return;
|
|
9875
9697
|
}
|
|
9698
|
+
const message = `<th> element must have a valid scope attribute: ${joinedScopes}`;
|
|
9699
|
+
const location = (scope == null ? void 0 : scope.valueLocation) ?? (scope == null ? void 0 : scope.keyLocation) ?? node.location;
|
|
9700
|
+
this.report(node, message, location);
|
|
9876
9701
|
});
|
|
9877
9702
|
}
|
|
9878
9703
|
}
|
|
9879
9704
|
|
|
9880
|
-
|
|
9881
|
-
|
|
9882
|
-
};
|
|
9883
|
-
class VoidStyle extends Rule {
|
|
9884
|
-
constructor(options) {
|
|
9885
|
-
super({ ...defaults$2, ...options });
|
|
9886
|
-
this.style = parseStyle(this.options.style);
|
|
9887
|
-
}
|
|
9888
|
-
static schema() {
|
|
9889
|
-
return {
|
|
9890
|
-
style: {
|
|
9891
|
-
enum: ["omit", "selfclose", "selfclosing"],
|
|
9892
|
-
type: "string"
|
|
9893
|
-
}
|
|
9894
|
-
};
|
|
9895
|
-
}
|
|
9896
|
-
documentation(context) {
|
|
9897
|
-
const [desc, end] = styleDescription(context.style);
|
|
9705
|
+
class H67 extends Rule {
|
|
9706
|
+
documentation() {
|
|
9898
9707
|
return {
|
|
9899
|
-
description:
|
|
9900
|
-
url: "https://html-validate.org/rules/
|
|
9708
|
+
description: "A decorative image cannot have a title attribute. Either remove `title` or add a descriptive `alt` text.",
|
|
9709
|
+
url: "https://html-validate.org/rules/wcag/h67.html"
|
|
9901
9710
|
};
|
|
9902
9711
|
}
|
|
9903
9712
|
setup() {
|
|
9904
9713
|
this.on("tag:end", (event) => {
|
|
9905
|
-
const
|
|
9906
|
-
if (
|
|
9907
|
-
|
|
9714
|
+
const node = event.target;
|
|
9715
|
+
if (!node || node.tagName !== "img") {
|
|
9716
|
+
return;
|
|
9717
|
+
}
|
|
9718
|
+
const title = node.getAttribute("title");
|
|
9719
|
+
if (!title || title.value === "") {
|
|
9720
|
+
return;
|
|
9721
|
+
}
|
|
9722
|
+
const alt = node.getAttributeValue("alt");
|
|
9723
|
+
if (alt && alt !== "") {
|
|
9724
|
+
return;
|
|
9908
9725
|
}
|
|
9726
|
+
this.report(node, "<img> with empty alt text cannot have title attribute", title.keyLocation);
|
|
9909
9727
|
});
|
|
9910
9728
|
}
|
|
9911
|
-
validateActive(node) {
|
|
9912
|
-
if (!node.voidElement) {
|
|
9913
|
-
return;
|
|
9914
|
-
}
|
|
9915
|
-
if (this.shouldBeOmitted(node)) {
|
|
9916
|
-
this.reportError(
|
|
9917
|
-
node,
|
|
9918
|
-
`Expected omitted end tag <${node.tagName}> instead of self-closing element <${node.tagName}/>`
|
|
9919
|
-
);
|
|
9920
|
-
}
|
|
9921
|
-
if (this.shouldBeSelfClosed(node)) {
|
|
9922
|
-
this.reportError(
|
|
9923
|
-
node,
|
|
9924
|
-
`Expected self-closing element <${node.tagName}/> instead of omitted end-tag <${node.tagName}>`
|
|
9925
|
-
);
|
|
9926
|
-
}
|
|
9927
|
-
}
|
|
9928
|
-
reportError(node, message) {
|
|
9929
|
-
const context = {
|
|
9930
|
-
style: this.style,
|
|
9931
|
-
tagName: node.tagName
|
|
9932
|
-
};
|
|
9933
|
-
super.report(node, message, null, context);
|
|
9934
|
-
}
|
|
9935
|
-
shouldBeOmitted(node) {
|
|
9936
|
-
return this.style === 1 /* AlwaysOmit */ && node.closed === NodeClosed.VoidSelfClosed;
|
|
9937
|
-
}
|
|
9938
|
-
shouldBeSelfClosed(node) {
|
|
9939
|
-
return this.style === 2 /* AlwaysSelfclose */ && node.closed === NodeClosed.VoidOmitted;
|
|
9940
|
-
}
|
|
9941
|
-
}
|
|
9942
|
-
function parseStyle(name) {
|
|
9943
|
-
switch (name) {
|
|
9944
|
-
case "omit":
|
|
9945
|
-
return 1 /* AlwaysOmit */;
|
|
9946
|
-
case "selfclose":
|
|
9947
|
-
case "selfclosing":
|
|
9948
|
-
return 2 /* AlwaysSelfclose */;
|
|
9949
|
-
default:
|
|
9950
|
-
throw new Error(`Invalid style "${name}" for "void-style" rule`);
|
|
9951
|
-
}
|
|
9952
|
-
}
|
|
9953
|
-
function styleDescription(style) {
|
|
9954
|
-
switch (style) {
|
|
9955
|
-
case 1 /* AlwaysOmit */:
|
|
9956
|
-
return ["omit end tag", ""];
|
|
9957
|
-
case 2 /* AlwaysSelfclose */:
|
|
9958
|
-
return ["be self-closed", "/"];
|
|
9959
|
-
default:
|
|
9960
|
-
throw new Error(`Unknown style`);
|
|
9961
|
-
}
|
|
9962
9729
|
}
|
|
9963
9730
|
|
|
9964
|
-
class
|
|
9731
|
+
class H71 extends Rule {
|
|
9965
9732
|
documentation() {
|
|
9966
9733
|
return {
|
|
9967
|
-
description: "
|
|
9968
|
-
url: "https://html-validate.org/rules/wcag/
|
|
9734
|
+
description: "H71: Providing a description for groups of form controls using fieldset and legend elements",
|
|
9735
|
+
url: "https://html-validate.org/rules/wcag/h71.html"
|
|
9969
9736
|
};
|
|
9970
9737
|
}
|
|
9971
9738
|
setup() {
|
|
9972
9739
|
this.on("dom:ready", (event) => {
|
|
9973
|
-
const
|
|
9974
|
-
|
|
9975
|
-
|
|
9976
|
-
|
|
9977
|
-
}
|
|
9978
|
-
if (!inAccessibilityTree(link)) {
|
|
9979
|
-
continue;
|
|
9980
|
-
}
|
|
9981
|
-
const textClassification = classifyNodeText(link, { ignoreHiddenRoot: true });
|
|
9982
|
-
if (textClassification !== TextClassification.EMPTY_TEXT) {
|
|
9983
|
-
continue;
|
|
9984
|
-
}
|
|
9985
|
-
const images = link.querySelectorAll("img");
|
|
9986
|
-
if (images.some((image) => hasAltText(image))) {
|
|
9987
|
-
continue;
|
|
9988
|
-
}
|
|
9989
|
-
const labels = link.querySelectorAll("[aria-label]");
|
|
9990
|
-
if (hasAriaLabel(link) || labels.some((cur) => hasAriaLabel(cur))) {
|
|
9991
|
-
continue;
|
|
9992
|
-
}
|
|
9993
|
-
this.report(link, "Anchor link must have a text describing its purpose");
|
|
9740
|
+
const { document } = event;
|
|
9741
|
+
const fieldsets = document.querySelectorAll(this.selector);
|
|
9742
|
+
for (const fieldset of fieldsets) {
|
|
9743
|
+
this.validate(fieldset);
|
|
9994
9744
|
}
|
|
9995
9745
|
});
|
|
9996
9746
|
}
|
|
9747
|
+
validate(fieldset) {
|
|
9748
|
+
const legend = fieldset.querySelectorAll("> legend");
|
|
9749
|
+
if (legend.length === 0) {
|
|
9750
|
+
this.reportNode(fieldset);
|
|
9751
|
+
}
|
|
9752
|
+
}
|
|
9753
|
+
reportNode(node) {
|
|
9754
|
+
super.report(node, `${node.annotatedName} must have a <legend> as the first child`);
|
|
9755
|
+
}
|
|
9756
|
+
get selector() {
|
|
9757
|
+
return this.getTagsDerivedFrom("fieldset").join(",");
|
|
9758
|
+
}
|
|
9759
|
+
}
|
|
9760
|
+
|
|
9761
|
+
const bundledRules$1 = {
|
|
9762
|
+
"wcag/h30": H30,
|
|
9763
|
+
"wcag/h32": H32,
|
|
9764
|
+
"wcag/h36": H36,
|
|
9765
|
+
"wcag/h37": H37,
|
|
9766
|
+
"wcag/h63": H63,
|
|
9767
|
+
"wcag/h67": H67,
|
|
9768
|
+
"wcag/h71": H71
|
|
9769
|
+
};
|
|
9770
|
+
|
|
9771
|
+
const bundledRules = {
|
|
9772
|
+
"allowed-links": AllowedLinks,
|
|
9773
|
+
"area-alt": AreaAlt,
|
|
9774
|
+
"aria-hidden-body": AriaHiddenBody,
|
|
9775
|
+
"aria-label-misuse": AriaLabelMisuse,
|
|
9776
|
+
"attr-case": AttrCase,
|
|
9777
|
+
"attr-delimiter": AttrDelimiter,
|
|
9778
|
+
"attr-pattern": AttrPattern,
|
|
9779
|
+
"attr-quotes": AttrQuotes,
|
|
9780
|
+
"attr-spacing": AttrSpacing,
|
|
9781
|
+
"attribute-allowed-values": AttributeAllowedValues,
|
|
9782
|
+
"attribute-boolean-style": AttributeBooleanStyle,
|
|
9783
|
+
"attribute-empty-style": AttributeEmptyStyle,
|
|
9784
|
+
"attribute-misuse": AttributeMisuse,
|
|
9785
|
+
"class-pattern": ClassPattern,
|
|
9786
|
+
"close-attr": CloseAttr,
|
|
9787
|
+
"close-order": CloseOrder,
|
|
9788
|
+
deprecated: Deprecated,
|
|
9789
|
+
"deprecated-rule": DeprecatedRule,
|
|
9790
|
+
"doctype-html": NoStyleTag$1,
|
|
9791
|
+
"doctype-style": DoctypeStyle,
|
|
9792
|
+
"element-case": ElementCase,
|
|
9793
|
+
"element-name": ElementName,
|
|
9794
|
+
"element-permitted-content": ElementPermittedContent,
|
|
9795
|
+
"element-permitted-occurrences": ElementPermittedOccurrences,
|
|
9796
|
+
"element-permitted-order": ElementPermittedOrder,
|
|
9797
|
+
"element-permitted-parent": ElementPermittedParent,
|
|
9798
|
+
"element-required-ancestor": ElementRequiredAncestor,
|
|
9799
|
+
"element-required-attributes": ElementRequiredAttributes,
|
|
9800
|
+
"element-required-content": ElementRequiredContent,
|
|
9801
|
+
"empty-heading": EmptyHeading,
|
|
9802
|
+
"empty-title": EmptyTitle,
|
|
9803
|
+
"form-dup-name": FormDupName,
|
|
9804
|
+
"heading-level": HeadingLevel,
|
|
9805
|
+
"hidden-focusable": HiddenFocusable,
|
|
9806
|
+
"id-pattern": IdPattern,
|
|
9807
|
+
"input-attributes": InputAttributes,
|
|
9808
|
+
"input-missing-label": InputMissingLabel,
|
|
9809
|
+
"long-title": LongTitle,
|
|
9810
|
+
"map-dup-name": MapDupName,
|
|
9811
|
+
"map-id-name": MapIdName,
|
|
9812
|
+
"meta-refresh": MetaRefresh,
|
|
9813
|
+
"missing-doctype": MissingDoctype,
|
|
9814
|
+
"multiple-labeled-controls": MultipleLabeledControls,
|
|
9815
|
+
"name-pattern": NamePattern,
|
|
9816
|
+
"no-abstract-role": NoAbstractRole,
|
|
9817
|
+
"no-autoplay": NoAutoplay,
|
|
9818
|
+
"no-conditional-comment": NoConditionalComment,
|
|
9819
|
+
"no-deprecated-attr": NoDeprecatedAttr,
|
|
9820
|
+
"no-dup-attr": NoDupAttr,
|
|
9821
|
+
"no-dup-class": NoDupClass,
|
|
9822
|
+
"no-dup-id": NoDupID,
|
|
9823
|
+
"no-implicit-button-type": NoImplicitButtonType,
|
|
9824
|
+
"no-implicit-input-type": NoImplicitInputType,
|
|
9825
|
+
"no-implicit-close": NoImplicitClose,
|
|
9826
|
+
"no-inline-style": NoInlineStyle,
|
|
9827
|
+
"no-missing-references": NoMissingReferences,
|
|
9828
|
+
"no-multiple-main": NoMultipleMain,
|
|
9829
|
+
"no-raw-characters": NoRawCharacters,
|
|
9830
|
+
"no-redundant-aria-label": NoRedundantAriaLabel,
|
|
9831
|
+
"no-redundant-for": NoRedundantFor,
|
|
9832
|
+
"no-redundant-role": NoRedundantRole,
|
|
9833
|
+
"no-self-closing": NoSelfClosing,
|
|
9834
|
+
"no-style-tag": NoStyleTag,
|
|
9835
|
+
"no-trailing-whitespace": NoTrailingWhitespace,
|
|
9836
|
+
"no-unknown-elements": NoUnknownElements,
|
|
9837
|
+
"no-unused-disable": NoUnusedDisable,
|
|
9838
|
+
"no-utf8-bom": NoUtf8Bom,
|
|
9839
|
+
"prefer-button": PreferButton,
|
|
9840
|
+
"prefer-native-element": PreferNativeElement,
|
|
9841
|
+
"prefer-tbody": PreferTbody,
|
|
9842
|
+
"require-csp-nonce": RequireCSPNonce,
|
|
9843
|
+
"require-sri": RequireSri,
|
|
9844
|
+
"script-element": ScriptElement,
|
|
9845
|
+
"script-type": ScriptType,
|
|
9846
|
+
"svg-focusable": SvgFocusable,
|
|
9847
|
+
"tel-non-breaking": TelNonBreaking,
|
|
9848
|
+
"text-content": TextContent,
|
|
9849
|
+
"unique-landmark": UniqueLandmark,
|
|
9850
|
+
"unrecognized-char-ref": UnknownCharReference,
|
|
9851
|
+
"valid-autocomplete": ValidAutocomplete,
|
|
9852
|
+
"valid-id": ValidID,
|
|
9853
|
+
"void-content": VoidContent,
|
|
9854
|
+
"void-style": VoidStyle,
|
|
9855
|
+
...bundledRules$1
|
|
9856
|
+
};
|
|
9857
|
+
|
|
9858
|
+
const ruleIds = new Set(Object.keys(bundledRules));
|
|
9859
|
+
function ruleExists(ruleId) {
|
|
9860
|
+
return ruleIds.has(ruleId);
|
|
9997
9861
|
}
|
|
9998
9862
|
|
|
9999
|
-
|
|
10000
|
-
|
|
10001
|
-
|
|
10002
|
-
|
|
10003
|
-
|
|
10004
|
-
|
|
10005
|
-
}
|
|
10006
|
-
setup() {
|
|
10007
|
-
const formTags = this.getTagsWithProperty("form");
|
|
10008
|
-
const formSelector = formTags.join(",");
|
|
10009
|
-
this.on("dom:ready", (event) => {
|
|
10010
|
-
const { document } = event;
|
|
10011
|
-
const forms = document.querySelectorAll(formSelector);
|
|
10012
|
-
for (const form of forms) {
|
|
10013
|
-
if (hasNestedSubmit(form)) {
|
|
10014
|
-
continue;
|
|
10015
|
-
}
|
|
10016
|
-
if (hasAssociatedSubmit(document, form)) {
|
|
10017
|
-
continue;
|
|
10018
|
-
}
|
|
10019
|
-
this.report(form, `<${form.tagName}> element must have a submit button`);
|
|
10020
|
-
}
|
|
10021
|
-
});
|
|
9863
|
+
function depthFirst(root, callback) {
|
|
9864
|
+
if (root instanceof DOMTree) {
|
|
9865
|
+
if (root.readyState !== "complete") {
|
|
9866
|
+
throw new Error(`Cannot call walk.depthFirst(..) before document is ready`);
|
|
9867
|
+
}
|
|
9868
|
+
root = root.root;
|
|
10022
9869
|
}
|
|
10023
|
-
|
|
10024
|
-
|
|
10025
|
-
|
|
10026
|
-
|
|
10027
|
-
}
|
|
10028
|
-
function isAssociated(id, node) {
|
|
10029
|
-
const form = node.getAttribute("form");
|
|
10030
|
-
return Boolean(form == null ? void 0 : form.valueMatches(id, true));
|
|
10031
|
-
}
|
|
10032
|
-
function hasNestedSubmit(form) {
|
|
10033
|
-
const matches = form.querySelectorAll("button,input").filter(isSubmit).filter((node) => !node.hasAttribute("form"));
|
|
10034
|
-
return matches.length > 0;
|
|
10035
|
-
}
|
|
10036
|
-
function hasAssociatedSubmit(document, form) {
|
|
10037
|
-
const { id } = form;
|
|
10038
|
-
if (!id) {
|
|
10039
|
-
return false;
|
|
9870
|
+
function visit(node) {
|
|
9871
|
+
node.childElements.forEach(visit);
|
|
9872
|
+
if (!node.isRootElement()) {
|
|
9873
|
+
callback(node);
|
|
9874
|
+
}
|
|
10040
9875
|
}
|
|
10041
|
-
|
|
10042
|
-
return matches.length > 0;
|
|
9876
|
+
visit(root);
|
|
10043
9877
|
}
|
|
9878
|
+
const walk = {
|
|
9879
|
+
depthFirst
|
|
9880
|
+
};
|
|
10044
9881
|
|
|
10045
|
-
class
|
|
10046
|
-
|
|
10047
|
-
|
|
10048
|
-
|
|
10049
|
-
|
|
10050
|
-
|
|
10051
|
-
|
|
10052
|
-
|
|
10053
|
-
|
|
9882
|
+
class DOMTree {
|
|
9883
|
+
/**
|
|
9884
|
+
* @internal
|
|
9885
|
+
*/
|
|
9886
|
+
constructor(location) {
|
|
9887
|
+
this.root = HtmlElement.rootNode(location);
|
|
9888
|
+
this.active = this.root;
|
|
9889
|
+
this.doctype = null;
|
|
9890
|
+
this._readyState = "loading";
|
|
10054
9891
|
}
|
|
10055
|
-
|
|
10056
|
-
|
|
10057
|
-
|
|
10058
|
-
|
|
10059
|
-
|
|
10060
|
-
|
|
10061
|
-
|
|
10062
|
-
|
|
10063
|
-
|
|
10064
|
-
|
|
10065
|
-
|
|
10066
|
-
|
|
10067
|
-
|
|
10068
|
-
|
|
10069
|
-
|
|
10070
|
-
|
|
10071
|
-
|
|
10072
|
-
|
|
10073
|
-
|
|
10074
|
-
|
|
9892
|
+
/**
|
|
9893
|
+
* @internal
|
|
9894
|
+
*/
|
|
9895
|
+
pushActive(node) {
|
|
9896
|
+
this.active = node;
|
|
9897
|
+
}
|
|
9898
|
+
/**
|
|
9899
|
+
* @internal
|
|
9900
|
+
*/
|
|
9901
|
+
popActive() {
|
|
9902
|
+
if (this.active.isRootElement()) {
|
|
9903
|
+
return;
|
|
9904
|
+
}
|
|
9905
|
+
this.active = this.active.parent ?? this.root;
|
|
9906
|
+
}
|
|
9907
|
+
/**
|
|
9908
|
+
* @internal
|
|
9909
|
+
*/
|
|
9910
|
+
getActive() {
|
|
9911
|
+
return this.active;
|
|
9912
|
+
}
|
|
9913
|
+
/**
|
|
9914
|
+
* Describes the loading state of the document.
|
|
9915
|
+
*
|
|
9916
|
+
* When `"loading"` it is still not safe to use functions such as
|
|
9917
|
+
* `querySelector` or presence of attributes, child nodes, etc.
|
|
9918
|
+
*/
|
|
9919
|
+
get readyState() {
|
|
9920
|
+
return this._readyState;
|
|
9921
|
+
}
|
|
9922
|
+
/**
|
|
9923
|
+
* Resolve dynamic meta expressions.
|
|
9924
|
+
*
|
|
9925
|
+
* @internal
|
|
9926
|
+
*/
|
|
9927
|
+
resolveMeta(table) {
|
|
9928
|
+
this._readyState = "complete";
|
|
9929
|
+
walk.depthFirst(this, (node) => {
|
|
9930
|
+
table.resolve(node);
|
|
10075
9931
|
});
|
|
10076
9932
|
}
|
|
9933
|
+
getElementsByTagName(tagName) {
|
|
9934
|
+
return this.root.getElementsByTagName(tagName);
|
|
9935
|
+
}
|
|
9936
|
+
/**
|
|
9937
|
+
* @deprecated use utility function `walk.depthFirst(..)` instead (since 8.21.0).
|
|
9938
|
+
*/
|
|
9939
|
+
visitDepthFirst(callback) {
|
|
9940
|
+
walk.depthFirst(this, callback);
|
|
9941
|
+
}
|
|
9942
|
+
/**
|
|
9943
|
+
* @deprecated use `querySelector(..)` instead (since 8.21.0)
|
|
9944
|
+
*/
|
|
9945
|
+
find(callback) {
|
|
9946
|
+
return this.root.find(callback);
|
|
9947
|
+
}
|
|
9948
|
+
querySelector(selector) {
|
|
9949
|
+
return this.root.querySelector(selector);
|
|
9950
|
+
}
|
|
9951
|
+
querySelectorAll(selector) {
|
|
9952
|
+
return this.root.querySelectorAll(selector);
|
|
9953
|
+
}
|
|
10077
9954
|
}
|
|
10078
9955
|
|
|
10079
|
-
const
|
|
10080
|
-
|
|
10081
|
-
|
|
10082
|
-
|
|
10083
|
-
|
|
10084
|
-
|
|
10085
|
-
|
|
10086
|
-
|
|
10087
|
-
|
|
9956
|
+
const allowedKeys = ["exclude"];
|
|
9957
|
+
class Validator {
|
|
9958
|
+
/**
|
|
9959
|
+
* Test if element is used in a proper context.
|
|
9960
|
+
*
|
|
9961
|
+
* @param node - Element to test.
|
|
9962
|
+
* @param rules - List of rules.
|
|
9963
|
+
* @returns `true` if element passes all tests.
|
|
9964
|
+
*/
|
|
9965
|
+
static validatePermitted(node, rules) {
|
|
9966
|
+
if (!rules) {
|
|
9967
|
+
return true;
|
|
10088
9968
|
}
|
|
9969
|
+
return rules.some((rule) => {
|
|
9970
|
+
return Validator.validatePermittedRule(node, rule);
|
|
9971
|
+
});
|
|
10089
9972
|
}
|
|
10090
|
-
|
|
10091
|
-
|
|
10092
|
-
|
|
10093
|
-
|
|
10094
|
-
|
|
10095
|
-
|
|
10096
|
-
|
|
10097
|
-
|
|
10098
|
-
|
|
10099
|
-
|
|
10100
|
-
|
|
10101
|
-
|
|
9973
|
+
/**
|
|
9974
|
+
* Test if an element is used the correct amount of times.
|
|
9975
|
+
*
|
|
9976
|
+
* For instance, a `<table>` element can only contain a single `<tbody>`
|
|
9977
|
+
* child. If multiple `<tbody>` exists this test will fail both nodes.
|
|
9978
|
+
* Note that this is called on the parent but will fail the children violating
|
|
9979
|
+
* the rule.
|
|
9980
|
+
*
|
|
9981
|
+
* @param children - Array of children to validate.
|
|
9982
|
+
* @param rules - List of rules of the parent element.
|
|
9983
|
+
* @returns `true` if the parent element of the children passes the test.
|
|
9984
|
+
*/
|
|
9985
|
+
static validateOccurrences(children, rules, cb) {
|
|
9986
|
+
if (!rules) {
|
|
9987
|
+
return true;
|
|
9988
|
+
}
|
|
9989
|
+
let valid = true;
|
|
9990
|
+
for (const rule of rules) {
|
|
9991
|
+
if (typeof rule !== "string") {
|
|
9992
|
+
return false;
|
|
9993
|
+
}
|
|
9994
|
+
const [, category, quantifier] = rule.match(/^(@?.*?)([?*]?)$/);
|
|
9995
|
+
const limit = category && quantifier && parseQuantifier(quantifier);
|
|
9996
|
+
if (limit) {
|
|
9997
|
+
const siblings = children.filter(
|
|
9998
|
+
(cur) => Validator.validatePermittedCategory(cur, rule, true)
|
|
9999
|
+
);
|
|
10000
|
+
if (siblings.length > limit) {
|
|
10001
|
+
for (const child of siblings.slice(limit)) {
|
|
10002
|
+
cb(child, category);
|
|
10102
10003
|
}
|
|
10103
|
-
|
|
10104
|
-
|
|
10105
|
-
allowEmpty: {
|
|
10106
|
-
type: "boolean"
|
|
10004
|
+
valid = false;
|
|
10005
|
+
}
|
|
10107
10006
|
}
|
|
10108
|
-
}
|
|
10109
|
-
|
|
10110
|
-
documentation() {
|
|
10111
|
-
return {
|
|
10112
|
-
description: "Both HTML5 and WCAG 2.0 requires images to have a alternative text for each image.",
|
|
10113
|
-
url: "https://html-validate.org/rules/wcag/h37.html"
|
|
10114
|
-
};
|
|
10007
|
+
}
|
|
10008
|
+
return valid;
|
|
10115
10009
|
}
|
|
10116
|
-
|
|
10117
|
-
|
|
10118
|
-
|
|
10119
|
-
|
|
10120
|
-
|
|
10121
|
-
|
|
10010
|
+
/**
|
|
10011
|
+
* Validate elements order.
|
|
10012
|
+
*
|
|
10013
|
+
* Given a parent element with children and metadata containing permitted
|
|
10014
|
+
* order it will validate each children and ensure each one exists in the
|
|
10015
|
+
* specified order.
|
|
10016
|
+
*
|
|
10017
|
+
* For instance, for a `<table>` element the `<caption>` element must come
|
|
10018
|
+
* before a `<thead>` which must come before `<tbody>`.
|
|
10019
|
+
*
|
|
10020
|
+
* @param children - Array of children to validate.
|
|
10021
|
+
*/
|
|
10022
|
+
static validateOrder(children, rules, cb) {
|
|
10023
|
+
if (!rules) {
|
|
10024
|
+
return true;
|
|
10025
|
+
}
|
|
10026
|
+
let i = 0;
|
|
10027
|
+
let prev = null;
|
|
10028
|
+
for (const node of children) {
|
|
10029
|
+
const old = i;
|
|
10030
|
+
while (rules[i] && !Validator.validatePermittedCategory(node, rules[i], true)) {
|
|
10031
|
+
i++;
|
|
10032
|
+
}
|
|
10033
|
+
if (i >= rules.length) {
|
|
10034
|
+
const orderSpecified = rules.find(
|
|
10035
|
+
(cur) => Validator.validatePermittedCategory(node, cur, true)
|
|
10036
|
+
);
|
|
10037
|
+
if (orderSpecified) {
|
|
10038
|
+
cb(node, prev);
|
|
10039
|
+
return false;
|
|
10040
|
+
}
|
|
10041
|
+
i = old;
|
|
10122
10042
|
}
|
|
10043
|
+
prev = node;
|
|
10044
|
+
}
|
|
10045
|
+
return true;
|
|
10046
|
+
}
|
|
10047
|
+
/**
|
|
10048
|
+
* Validate element ancestors.
|
|
10049
|
+
*
|
|
10050
|
+
* Check if an element has the required set of elements. At least one of the
|
|
10051
|
+
* selectors must match.
|
|
10052
|
+
*/
|
|
10053
|
+
static validateAncestors(node, rules) {
|
|
10054
|
+
if (!rules || rules.length === 0) {
|
|
10055
|
+
return true;
|
|
10056
|
+
}
|
|
10057
|
+
return rules.some((rule) => node.closest(rule));
|
|
10058
|
+
}
|
|
10059
|
+
/**
|
|
10060
|
+
* Validate element required content.
|
|
10061
|
+
*
|
|
10062
|
+
* Check if an element has the required set of elements. At least one of the
|
|
10063
|
+
* selectors must match.
|
|
10064
|
+
*
|
|
10065
|
+
* Returns `[]` when valid or a list of required but missing tagnames or
|
|
10066
|
+
* categories.
|
|
10067
|
+
*/
|
|
10068
|
+
static validateRequiredContent(node, rules) {
|
|
10069
|
+
if (!rules || rules.length === 0) {
|
|
10070
|
+
return [];
|
|
10071
|
+
}
|
|
10072
|
+
return rules.filter((tagName) => {
|
|
10073
|
+
const haveMatchingChild = node.childElements.some(
|
|
10074
|
+
(child) => Validator.validatePermittedCategory(child, tagName, false)
|
|
10075
|
+
);
|
|
10076
|
+
return !haveMatchingChild;
|
|
10123
10077
|
});
|
|
10124
10078
|
}
|
|
10125
|
-
|
|
10126
|
-
|
|
10127
|
-
|
|
10079
|
+
/**
|
|
10080
|
+
* Test if an attribute has an allowed value and/or format.
|
|
10081
|
+
*
|
|
10082
|
+
* @param attr - Attribute to test.
|
|
10083
|
+
* @param rules - Element attribute metadta.
|
|
10084
|
+
* @returns `true` if attribute passes all tests.
|
|
10085
|
+
*/
|
|
10086
|
+
static validateAttribute(attr, rules) {
|
|
10087
|
+
const rule = rules[attr.key];
|
|
10088
|
+
if (!rule) {
|
|
10089
|
+
return true;
|
|
10128
10090
|
}
|
|
10129
|
-
|
|
10130
|
-
|
|
10091
|
+
const value = attr.value;
|
|
10092
|
+
if (value instanceof DynamicValue) {
|
|
10093
|
+
return true;
|
|
10131
10094
|
}
|
|
10132
|
-
|
|
10133
|
-
|
|
10134
|
-
|
|
10135
|
-
}
|
|
10095
|
+
const empty = value === null || value === "";
|
|
10096
|
+
if (rule.boolean) {
|
|
10097
|
+
return empty || value === attr.key;
|
|
10136
10098
|
}
|
|
10137
|
-
|
|
10138
|
-
|
|
10139
|
-
const attr = node.getAttribute("alt");
|
|
10140
|
-
this.report(node, `${tag} cannot have empty "alt" attribute`, attr.keyLocation);
|
|
10141
|
-
} else {
|
|
10142
|
-
this.report(node, `${tag} is missing required "alt" attribute`, node.location);
|
|
10099
|
+
if (rule.omit && empty) {
|
|
10100
|
+
return true;
|
|
10143
10101
|
}
|
|
10102
|
+
if (rule.list) {
|
|
10103
|
+
const tokens = new DOMTokenList(value, attr.valueLocation);
|
|
10104
|
+
return tokens.every((token) => {
|
|
10105
|
+
return this.validateAttributeValue(token, rule);
|
|
10106
|
+
});
|
|
10107
|
+
}
|
|
10108
|
+
return this.validateAttributeValue(value, rule);
|
|
10144
10109
|
}
|
|
10145
|
-
|
|
10146
|
-
|
|
10147
|
-
|
|
10148
|
-
|
|
10149
|
-
|
|
10150
|
-
|
|
10151
|
-
|
|
10152
|
-
|
|
10153
|
-
|
|
10154
|
-
|
|
10155
|
-
|
|
10156
|
-
|
|
10157
|
-
|
|
10158
|
-
this.on("tag:ready", (event) => {
|
|
10159
|
-
const node = event.target;
|
|
10160
|
-
if (node.tagName !== "th") {
|
|
10161
|
-
return;
|
|
10162
|
-
}
|
|
10163
|
-
const scope = node.getAttribute("scope");
|
|
10164
|
-
const value = scope == null ? void 0 : scope.value;
|
|
10165
|
-
if (value instanceof DynamicValue) {
|
|
10166
|
-
return;
|
|
10167
|
-
}
|
|
10168
|
-
if (value && validScopes.includes(value)) {
|
|
10169
|
-
return;
|
|
10170
|
-
}
|
|
10171
|
-
const message = `<th> element must have a valid scope attribute: ${joinedScopes}`;
|
|
10172
|
-
const location = (scope == null ? void 0 : scope.valueLocation) ?? (scope == null ? void 0 : scope.keyLocation) ?? node.location;
|
|
10173
|
-
this.report(node, message, location);
|
|
10174
|
-
});
|
|
10175
|
-
}
|
|
10176
|
-
}
|
|
10177
|
-
|
|
10178
|
-
class H67 extends Rule {
|
|
10179
|
-
documentation() {
|
|
10180
|
-
return {
|
|
10181
|
-
description: "A decorative image cannot have a title attribute. Either remove `title` or add a descriptive `alt` text.",
|
|
10182
|
-
url: "https://html-validate.org/rules/wcag/h67.html"
|
|
10183
|
-
};
|
|
10184
|
-
}
|
|
10185
|
-
setup() {
|
|
10186
|
-
this.on("tag:end", (event) => {
|
|
10187
|
-
const node = event.target;
|
|
10188
|
-
if (!node || node.tagName !== "img") {
|
|
10189
|
-
return;
|
|
10190
|
-
}
|
|
10191
|
-
const title = node.getAttribute("title");
|
|
10192
|
-
if (!title || title.value === "") {
|
|
10193
|
-
return;
|
|
10194
|
-
}
|
|
10195
|
-
const alt = node.getAttributeValue("alt");
|
|
10196
|
-
if (alt && alt !== "") {
|
|
10197
|
-
return;
|
|
10110
|
+
static validateAttributeValue(value, rule) {
|
|
10111
|
+
if (!rule.enum) {
|
|
10112
|
+
return true;
|
|
10113
|
+
}
|
|
10114
|
+
if (value === null) {
|
|
10115
|
+
return false;
|
|
10116
|
+
}
|
|
10117
|
+
const caseInsensitiveValue = value.toLowerCase();
|
|
10118
|
+
return rule.enum.some((entry) => {
|
|
10119
|
+
if (entry instanceof RegExp) {
|
|
10120
|
+
return !!value.match(entry);
|
|
10121
|
+
} else {
|
|
10122
|
+
return caseInsensitiveValue === entry;
|
|
10198
10123
|
}
|
|
10199
|
-
this.report(node, "<img> with empty alt text cannot have title attribute", title.keyLocation);
|
|
10200
10124
|
});
|
|
10201
10125
|
}
|
|
10202
|
-
|
|
10203
|
-
|
|
10204
|
-
|
|
10205
|
-
|
|
10206
|
-
|
|
10207
|
-
|
|
10208
|
-
|
|
10209
|
-
}
|
|
10210
|
-
|
|
10211
|
-
|
|
10212
|
-
|
|
10213
|
-
|
|
10214
|
-
|
|
10215
|
-
|
|
10216
|
-
|
|
10126
|
+
static validatePermittedRule(node, rule, isExclude = false) {
|
|
10127
|
+
if (typeof rule === "string") {
|
|
10128
|
+
return Validator.validatePermittedCategory(node, rule, !isExclude);
|
|
10129
|
+
} else if (Array.isArray(rule)) {
|
|
10130
|
+
return rule.every((inner) => {
|
|
10131
|
+
return Validator.validatePermittedRule(node, inner, isExclude);
|
|
10132
|
+
});
|
|
10133
|
+
} else {
|
|
10134
|
+
validateKeys(rule);
|
|
10135
|
+
if (rule.exclude) {
|
|
10136
|
+
if (Array.isArray(rule.exclude)) {
|
|
10137
|
+
return !rule.exclude.some((inner) => {
|
|
10138
|
+
return Validator.validatePermittedRule(node, inner, true);
|
|
10139
|
+
});
|
|
10140
|
+
} else {
|
|
10141
|
+
return !Validator.validatePermittedRule(node, rule.exclude, true);
|
|
10142
|
+
}
|
|
10143
|
+
} else {
|
|
10144
|
+
return true;
|
|
10217
10145
|
}
|
|
10218
|
-
});
|
|
10219
|
-
}
|
|
10220
|
-
validate(fieldset) {
|
|
10221
|
-
const legend = fieldset.querySelectorAll("> legend");
|
|
10222
|
-
if (legend.length === 0) {
|
|
10223
|
-
this.reportNode(fieldset);
|
|
10224
10146
|
}
|
|
10225
10147
|
}
|
|
10226
|
-
|
|
10227
|
-
|
|
10148
|
+
/**
|
|
10149
|
+
* Validate node against a content category.
|
|
10150
|
+
*
|
|
10151
|
+
* When matching parent nodes against permitted parents use the superset
|
|
10152
|
+
* parameter to also match for `@flow`. E.g. if a node expects a `@phrasing`
|
|
10153
|
+
* parent it should also allow `@flow` parent since `@phrasing` is a subset of
|
|
10154
|
+
* `@flow`.
|
|
10155
|
+
*
|
|
10156
|
+
* @param node - The node to test against
|
|
10157
|
+
* @param category - Name of category with `@` prefix or tag name.
|
|
10158
|
+
* @param defaultMatch - The default return value when node categories is not known.
|
|
10159
|
+
*/
|
|
10160
|
+
/* eslint-disable-next-line complexity -- rule does not like switch */
|
|
10161
|
+
static validatePermittedCategory(node, category, defaultMatch) {
|
|
10162
|
+
const [, rawCategory] = category.match(/^(@?.*?)([?*]?)$/);
|
|
10163
|
+
if (!rawCategory.startsWith("@")) {
|
|
10164
|
+
return node.tagName === rawCategory;
|
|
10165
|
+
}
|
|
10166
|
+
if (!node.meta) {
|
|
10167
|
+
return defaultMatch;
|
|
10168
|
+
}
|
|
10169
|
+
switch (rawCategory) {
|
|
10170
|
+
case "@meta":
|
|
10171
|
+
return node.meta.metadata;
|
|
10172
|
+
case "@flow":
|
|
10173
|
+
return node.meta.flow;
|
|
10174
|
+
case "@sectioning":
|
|
10175
|
+
return node.meta.sectioning;
|
|
10176
|
+
case "@heading":
|
|
10177
|
+
return node.meta.heading;
|
|
10178
|
+
case "@phrasing":
|
|
10179
|
+
return node.meta.phrasing;
|
|
10180
|
+
case "@embedded":
|
|
10181
|
+
return node.meta.embedded;
|
|
10182
|
+
case "@interactive":
|
|
10183
|
+
return node.meta.interactive;
|
|
10184
|
+
case "@script":
|
|
10185
|
+
return Boolean(node.meta.scriptSupporting);
|
|
10186
|
+
case "@form":
|
|
10187
|
+
return Boolean(node.meta.form);
|
|
10188
|
+
default:
|
|
10189
|
+
throw new Error(`Invalid content category "${category}"`);
|
|
10190
|
+
}
|
|
10228
10191
|
}
|
|
10229
|
-
|
|
10230
|
-
|
|
10192
|
+
}
|
|
10193
|
+
function validateKeys(rule) {
|
|
10194
|
+
for (const key of Object.keys(rule)) {
|
|
10195
|
+
if (!allowedKeys.includes(key)) {
|
|
10196
|
+
const str = JSON.stringify(rule);
|
|
10197
|
+
throw new Error(`Permitted rule "${str}" contains unknown property "${key}"`);
|
|
10198
|
+
}
|
|
10231
10199
|
}
|
|
10232
10200
|
}
|
|
10233
|
-
|
|
10234
|
-
|
|
10235
|
-
|
|
10236
|
-
|
|
10237
|
-
|
|
10238
|
-
|
|
10239
|
-
|
|
10240
|
-
|
|
10241
|
-
|
|
10201
|
+
function parseQuantifier(quantifier) {
|
|
10202
|
+
switch (quantifier) {
|
|
10203
|
+
case "?":
|
|
10204
|
+
return 1;
|
|
10205
|
+
case "*":
|
|
10206
|
+
return null;
|
|
10207
|
+
default:
|
|
10208
|
+
throw new Error(`Invalid quantifier "${quantifier}" used`);
|
|
10209
|
+
}
|
|
10210
|
+
}
|
|
10211
|
+
|
|
10212
|
+
const $schema = "http://json-schema.org/draft-06/schema#";
|
|
10213
|
+
const $id = "https://html-validate.org/schemas/config.json";
|
|
10214
|
+
const type = "object";
|
|
10215
|
+
const additionalProperties = false;
|
|
10216
|
+
const properties = {
|
|
10217
|
+
$schema: {
|
|
10218
|
+
type: "string"
|
|
10219
|
+
},
|
|
10220
|
+
root: {
|
|
10221
|
+
type: "boolean",
|
|
10222
|
+
title: "Mark as root configuration",
|
|
10223
|
+
description: "If this is set to true no further configurations will be searched.",
|
|
10224
|
+
"default": false
|
|
10225
|
+
},
|
|
10226
|
+
"extends": {
|
|
10227
|
+
type: "array",
|
|
10228
|
+
items: {
|
|
10229
|
+
type: "string"
|
|
10230
|
+
},
|
|
10231
|
+
title: "Configurations to extend",
|
|
10232
|
+
description: "Array of shareable or builtin configurations to extend."
|
|
10233
|
+
},
|
|
10234
|
+
elements: {
|
|
10235
|
+
type: "array",
|
|
10236
|
+
items: {
|
|
10237
|
+
anyOf: [
|
|
10238
|
+
{
|
|
10239
|
+
type: "string"
|
|
10240
|
+
},
|
|
10241
|
+
{
|
|
10242
|
+
type: "object"
|
|
10243
|
+
}
|
|
10244
|
+
]
|
|
10245
|
+
},
|
|
10246
|
+
title: "Element metadata to load",
|
|
10247
|
+
description: "Array of modules, plugins or files to load element metadata from. Use <rootDir> to refer to the folder with the package.json file.",
|
|
10248
|
+
examples: [
|
|
10249
|
+
[
|
|
10250
|
+
"html-validate:recommended",
|
|
10251
|
+
"plugin:recommended",
|
|
10252
|
+
"module",
|
|
10253
|
+
"./local-file.json"
|
|
10254
|
+
]
|
|
10255
|
+
]
|
|
10256
|
+
},
|
|
10257
|
+
plugins: {
|
|
10258
|
+
type: "array",
|
|
10259
|
+
items: {
|
|
10260
|
+
anyOf: [
|
|
10261
|
+
{
|
|
10262
|
+
type: "string"
|
|
10263
|
+
},
|
|
10264
|
+
{
|
|
10265
|
+
type: "object"
|
|
10266
|
+
}
|
|
10267
|
+
]
|
|
10268
|
+
},
|
|
10269
|
+
title: "Plugins to load",
|
|
10270
|
+
description: "Array of plugins load. Use <rootDir> to refer to the folder with the package.json file.",
|
|
10271
|
+
examples: [
|
|
10272
|
+
[
|
|
10273
|
+
"my-plugin",
|
|
10274
|
+
"./local-plugin"
|
|
10275
|
+
]
|
|
10276
|
+
]
|
|
10277
|
+
},
|
|
10278
|
+
transform: {
|
|
10279
|
+
type: "object",
|
|
10280
|
+
additionalProperties: {
|
|
10281
|
+
type: "string"
|
|
10282
|
+
},
|
|
10283
|
+
title: "File transformations to use.",
|
|
10284
|
+
description: "Object where key is regular expression to match filename and value is name of transformer.",
|
|
10285
|
+
examples: [
|
|
10286
|
+
{
|
|
10287
|
+
"^.*\\.foo$": "my-transformer",
|
|
10288
|
+
"^.*\\.bar$": "my-plugin",
|
|
10289
|
+
"^.*\\.baz$": "my-plugin:named"
|
|
10290
|
+
}
|
|
10291
|
+
]
|
|
10292
|
+
},
|
|
10293
|
+
rules: {
|
|
10294
|
+
type: "object",
|
|
10295
|
+
patternProperties: {
|
|
10296
|
+
".*": {
|
|
10297
|
+
anyOf: [
|
|
10298
|
+
{
|
|
10299
|
+
"enum": [
|
|
10300
|
+
0,
|
|
10301
|
+
1,
|
|
10302
|
+
2,
|
|
10303
|
+
"off",
|
|
10304
|
+
"warn",
|
|
10305
|
+
"error"
|
|
10306
|
+
]
|
|
10307
|
+
},
|
|
10308
|
+
{
|
|
10309
|
+
type: "array",
|
|
10310
|
+
minItems: 1,
|
|
10311
|
+
maxItems: 1,
|
|
10312
|
+
items: [
|
|
10313
|
+
{
|
|
10314
|
+
"enum": [
|
|
10315
|
+
0,
|
|
10316
|
+
1,
|
|
10317
|
+
2,
|
|
10318
|
+
"off",
|
|
10319
|
+
"warn",
|
|
10320
|
+
"error"
|
|
10321
|
+
]
|
|
10322
|
+
}
|
|
10323
|
+
]
|
|
10324
|
+
},
|
|
10325
|
+
{
|
|
10326
|
+
type: "array",
|
|
10327
|
+
minItems: 2,
|
|
10328
|
+
maxItems: 2,
|
|
10329
|
+
items: [
|
|
10330
|
+
{
|
|
10331
|
+
"enum": [
|
|
10332
|
+
0,
|
|
10333
|
+
1,
|
|
10334
|
+
2,
|
|
10335
|
+
"off",
|
|
10336
|
+
"warn",
|
|
10337
|
+
"error"
|
|
10338
|
+
]
|
|
10339
|
+
},
|
|
10340
|
+
{
|
|
10341
|
+
}
|
|
10342
|
+
]
|
|
10343
|
+
}
|
|
10344
|
+
]
|
|
10345
|
+
}
|
|
10346
|
+
},
|
|
10347
|
+
title: "Rule configuration.",
|
|
10348
|
+
description: "Enable/disable rules, set severity. Some rules have additional configuration like style or patterns to use.",
|
|
10349
|
+
examples: [
|
|
10350
|
+
{
|
|
10351
|
+
foo: "error",
|
|
10352
|
+
bar: "off",
|
|
10353
|
+
baz: [
|
|
10354
|
+
"error",
|
|
10355
|
+
{
|
|
10356
|
+
style: "camelcase"
|
|
10357
|
+
}
|
|
10358
|
+
]
|
|
10359
|
+
}
|
|
10360
|
+
]
|
|
10361
|
+
}
|
|
10362
|
+
};
|
|
10363
|
+
var configurationSchema = {
|
|
10364
|
+
$schema: $schema,
|
|
10365
|
+
$id: $id,
|
|
10366
|
+
type: type,
|
|
10367
|
+
additionalProperties: additionalProperties,
|
|
10368
|
+
properties: properties
|
|
10242
10369
|
};
|
|
10243
10370
|
|
|
10244
|
-
const
|
|
10245
|
-
|
|
10246
|
-
"area-alt": AreaAlt,
|
|
10247
|
-
"aria-hidden-body": AriaHiddenBody,
|
|
10248
|
-
"aria-label-misuse": AriaLabelMisuse,
|
|
10249
|
-
"attr-case": AttrCase,
|
|
10250
|
-
"attr-delimiter": AttrDelimiter,
|
|
10251
|
-
"attr-pattern": AttrPattern,
|
|
10252
|
-
"attr-quotes": AttrQuotes,
|
|
10253
|
-
"attr-spacing": AttrSpacing,
|
|
10254
|
-
"attribute-allowed-values": AttributeAllowedValues,
|
|
10255
|
-
"attribute-boolean-style": AttributeBooleanStyle,
|
|
10256
|
-
"attribute-empty-style": AttributeEmptyStyle,
|
|
10257
|
-
"attribute-misuse": AttributeMisuse,
|
|
10258
|
-
"class-pattern": ClassPattern,
|
|
10259
|
-
"close-attr": CloseAttr,
|
|
10260
|
-
"close-order": CloseOrder,
|
|
10261
|
-
deprecated: Deprecated,
|
|
10262
|
-
"deprecated-rule": DeprecatedRule,
|
|
10263
|
-
"doctype-html": NoStyleTag$1,
|
|
10264
|
-
"doctype-style": DoctypeStyle,
|
|
10265
|
-
"element-case": ElementCase,
|
|
10266
|
-
"element-name": ElementName,
|
|
10267
|
-
"element-permitted-content": ElementPermittedContent,
|
|
10268
|
-
"element-permitted-occurrences": ElementPermittedOccurrences,
|
|
10269
|
-
"element-permitted-order": ElementPermittedOrder,
|
|
10270
|
-
"element-permitted-parent": ElementPermittedParent,
|
|
10271
|
-
"element-required-ancestor": ElementRequiredAncestor,
|
|
10272
|
-
"element-required-attributes": ElementRequiredAttributes,
|
|
10273
|
-
"element-required-content": ElementRequiredContent,
|
|
10274
|
-
"empty-heading": EmptyHeading,
|
|
10275
|
-
"empty-title": EmptyTitle,
|
|
10276
|
-
"form-dup-name": FormDupName,
|
|
10277
|
-
"heading-level": HeadingLevel,
|
|
10278
|
-
"hidden-focusable": HiddenFocusable,
|
|
10279
|
-
"id-pattern": IdPattern,
|
|
10280
|
-
"input-attributes": InputAttributes,
|
|
10281
|
-
"input-missing-label": InputMissingLabel,
|
|
10282
|
-
"long-title": LongTitle,
|
|
10283
|
-
"map-dup-name": MapDupName,
|
|
10284
|
-
"map-id-name": MapIdName,
|
|
10285
|
-
"meta-refresh": MetaRefresh,
|
|
10286
|
-
"missing-doctype": MissingDoctype,
|
|
10287
|
-
"multiple-labeled-controls": MultipleLabeledControls,
|
|
10288
|
-
"name-pattern": NamePattern,
|
|
10289
|
-
"no-abstract-role": NoAbstractRole,
|
|
10290
|
-
"no-autoplay": NoAutoplay,
|
|
10291
|
-
"no-conditional-comment": NoConditionalComment,
|
|
10292
|
-
"no-deprecated-attr": NoDeprecatedAttr,
|
|
10293
|
-
"no-dup-attr": NoDupAttr,
|
|
10294
|
-
"no-dup-class": NoDupClass,
|
|
10295
|
-
"no-dup-id": NoDupID,
|
|
10296
|
-
"no-implicit-button-type": NoImplicitButtonType,
|
|
10297
|
-
"no-implicit-input-type": NoImplicitInputType,
|
|
10298
|
-
"no-implicit-close": NoImplicitClose,
|
|
10299
|
-
"no-inline-style": NoInlineStyle,
|
|
10300
|
-
"no-missing-references": NoMissingReferences,
|
|
10301
|
-
"no-multiple-main": NoMultipleMain,
|
|
10302
|
-
"no-raw-characters": NoRawCharacters,
|
|
10303
|
-
"no-redundant-aria-label": NoRedundantAriaLabel,
|
|
10304
|
-
"no-redundant-for": NoRedundantFor,
|
|
10305
|
-
"no-redundant-role": NoRedundantRole,
|
|
10306
|
-
"no-self-closing": NoSelfClosing,
|
|
10307
|
-
"no-style-tag": NoStyleTag,
|
|
10308
|
-
"no-trailing-whitespace": NoTrailingWhitespace,
|
|
10309
|
-
"no-unknown-elements": NoUnknownElements,
|
|
10310
|
-
"no-unused-disable": NoUnusedDisable,
|
|
10311
|
-
"no-utf8-bom": NoUtf8Bom,
|
|
10312
|
-
"prefer-button": PreferButton,
|
|
10313
|
-
"prefer-native-element": PreferNativeElement,
|
|
10314
|
-
"prefer-tbody": PreferTbody,
|
|
10315
|
-
"require-csp-nonce": RequireCSPNonce,
|
|
10316
|
-
"require-sri": RequireSri,
|
|
10317
|
-
"script-element": ScriptElement,
|
|
10318
|
-
"script-type": ScriptType,
|
|
10319
|
-
"svg-focusable": SvgFocusable,
|
|
10320
|
-
"tel-non-breaking": TelNonBreaking,
|
|
10321
|
-
"text-content": TextContent,
|
|
10322
|
-
"unique-landmark": UniqueLandmark,
|
|
10323
|
-
"unrecognized-char-ref": UnknownCharReference,
|
|
10324
|
-
"valid-autocomplete": ValidAutocomplete,
|
|
10325
|
-
"valid-id": ValidID,
|
|
10326
|
-
"void-content": VoidContent,
|
|
10327
|
-
"void-style": VoidStyle,
|
|
10328
|
-
...bundledRules$1
|
|
10371
|
+
const TRANSFORMER_API = {
|
|
10372
|
+
VERSION: 1
|
|
10329
10373
|
};
|
|
10330
10374
|
|
|
10331
10375
|
var defaultConfig = {};
|
|
@@ -11453,7 +11497,7 @@ class Parser {
|
|
|
11453
11497
|
location
|
|
11454
11498
|
};
|
|
11455
11499
|
this.trigger("tag:end", event);
|
|
11456
|
-
if (
|
|
11500
|
+
if (!active.isRootElement()) {
|
|
11457
11501
|
this.trigger("element:ready", {
|
|
11458
11502
|
target: active,
|
|
11459
11503
|
location: active.location
|
|
@@ -11805,15 +11849,6 @@ class Parser {
|
|
|
11805
11849
|
}
|
|
11806
11850
|
}
|
|
11807
11851
|
|
|
11808
|
-
function isThenable(value) {
|
|
11809
|
-
return value && typeof value === "object" && "then" in value && typeof value.then === "function";
|
|
11810
|
-
}
|
|
11811
|
-
|
|
11812
|
-
const ruleIds = new Set(Object.keys(bundledRules));
|
|
11813
|
-
function ruleExists(ruleId) {
|
|
11814
|
-
return ruleIds.has(ruleId);
|
|
11815
|
-
}
|
|
11816
|
-
|
|
11817
11852
|
function freeze(src) {
|
|
11818
11853
|
return {
|
|
11819
11854
|
...src,
|
|
@@ -11984,7 +12019,7 @@ class Engine {
|
|
|
11984
12019
|
const directiveContext = {
|
|
11985
12020
|
rules,
|
|
11986
12021
|
reportUnused(rules2, unused, options, location2) {
|
|
11987
|
-
if (
|
|
12022
|
+
if (!rules2.has(noUnusedDisable.name)) {
|
|
11988
12023
|
noUnusedDisable.reportUnused(unused, options, location2);
|
|
11989
12024
|
}
|
|
11990
12025
|
}
|
|
@@ -12115,7 +12150,9 @@ class Engine {
|
|
|
12115
12150
|
return new this.ParserClass(this.config);
|
|
12116
12151
|
}
|
|
12117
12152
|
processDirective(event, parser, context) {
|
|
12118
|
-
const rules = event.data.split(",").map((name) => name.trim()).map((name) => context.rules[name]).filter((rule) =>
|
|
12153
|
+
const rules = event.data.split(",").map((name) => name.trim()).map((name) => context.rules[name]).filter((rule) => {
|
|
12154
|
+
return Boolean(rule);
|
|
12155
|
+
});
|
|
12119
12156
|
const location = event.optionsLocation ?? event.location;
|
|
12120
12157
|
switch (event.action) {
|
|
12121
12158
|
case "enable":
|
|
@@ -12139,7 +12176,7 @@ class Engine {
|
|
|
12139
12176
|
rule.setServerity(Severity.ERROR);
|
|
12140
12177
|
}
|
|
12141
12178
|
}
|
|
12142
|
-
parser.on("tag:start", (
|
|
12179
|
+
parser.on("tag:start", (_event, data) => {
|
|
12143
12180
|
data.target.enableRules(rules.map((rule) => rule.name));
|
|
12144
12181
|
});
|
|
12145
12182
|
}
|
|
@@ -12147,7 +12184,7 @@ class Engine {
|
|
|
12147
12184
|
for (const rule of rules) {
|
|
12148
12185
|
rule.setEnabled(false);
|
|
12149
12186
|
}
|
|
12150
|
-
parser.on("tag:start", (
|
|
12187
|
+
parser.on("tag:start", (_event, data) => {
|
|
12151
12188
|
data.target.disableRules(rules.map((rule) => rule.name));
|
|
12152
12189
|
});
|
|
12153
12190
|
}
|
|
@@ -12159,14 +12196,14 @@ class Engine {
|
|
|
12159
12196
|
for (const rule of rules) {
|
|
12160
12197
|
rule.block(blocker);
|
|
12161
12198
|
}
|
|
12162
|
-
const unregisterOpen = parser.on("tag:start", (
|
|
12199
|
+
const unregisterOpen = parser.on("tag:start", (_event, data) => {
|
|
12163
12200
|
var _a;
|
|
12164
12201
|
if (directiveBlock === null) {
|
|
12165
12202
|
directiveBlock = ((_a = data.target.parent) == null ? void 0 : _a.unique) ?? null;
|
|
12166
12203
|
}
|
|
12167
12204
|
data.target.blockRules(ruleIds, blocker);
|
|
12168
12205
|
});
|
|
12169
|
-
const unregisterClose = parser.on("tag:end", (
|
|
12206
|
+
const unregisterClose = parser.on("tag:end", (_event, data) => {
|
|
12170
12207
|
const lastNode = directiveBlock === null;
|
|
12171
12208
|
const parentClosed = directiveBlock === data.previous.unique;
|
|
12172
12209
|
if (lastNode || parentClosed) {
|
|
@@ -12177,7 +12214,7 @@ class Engine {
|
|
|
12177
12214
|
}
|
|
12178
12215
|
}
|
|
12179
12216
|
});
|
|
12180
|
-
parser.on("rule:error", (
|
|
12217
|
+
parser.on("rule:error", (_event, data) => {
|
|
12181
12218
|
if (data.blockers.includes(blocker)) {
|
|
12182
12219
|
unused.delete(data.ruleId);
|
|
12183
12220
|
}
|
|
@@ -12193,10 +12230,10 @@ class Engine {
|
|
|
12193
12230
|
for (const rule of rules) {
|
|
12194
12231
|
rule.block(blocker);
|
|
12195
12232
|
}
|
|
12196
|
-
const unregister = parser.on("tag:start", (
|
|
12233
|
+
const unregister = parser.on("tag:start", (_event, data) => {
|
|
12197
12234
|
data.target.blockRules(ruleIds, blocker);
|
|
12198
12235
|
});
|
|
12199
|
-
parser.on("rule:error", (
|
|
12236
|
+
parser.on("rule:error", (_event, data) => {
|
|
12200
12237
|
if (data.blockers.includes(blocker)) {
|
|
12201
12238
|
unused.delete(data.ruleId);
|
|
12202
12239
|
}
|
|
@@ -12760,7 +12797,7 @@ class HtmlValidate {
|
|
|
12760
12797
|
}
|
|
12761
12798
|
|
|
12762
12799
|
const name = "html-validate";
|
|
12763
|
-
const version = "8.
|
|
12800
|
+
const version = "8.21.0";
|
|
12764
12801
|
const bugs = "https://gitlab.com/html-validate/html-validate/issues/new";
|
|
12765
12802
|
|
|
12766
12803
|
function definePlugin(plugin) {
|
|
@@ -13682,4 +13719,5 @@ exports.ruleExists = ruleExists;
|
|
|
13682
13719
|
exports.sliceLocation = sliceLocation;
|
|
13683
13720
|
exports.staticResolver = staticResolver;
|
|
13684
13721
|
exports.version = version;
|
|
13722
|
+
exports.walk = walk;
|
|
13685
13723
|
//# sourceMappingURL=core.js.map
|