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/es/core.js
CHANGED
|
@@ -1269,14 +1269,12 @@ class MetaTable {
|
|
|
1269
1269
|
* @returns A shallow copy of metadata.
|
|
1270
1270
|
*/
|
|
1271
1271
|
getMetaFor(tagName) {
|
|
1272
|
-
|
|
1273
|
-
if (
|
|
1274
|
-
return { ...
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
|
-
return { ...this.elements["*"] };
|
|
1272
|
+
const meta = this.elements[tagName.toLowerCase()] ?? this.elements["*"];
|
|
1273
|
+
if (meta) {
|
|
1274
|
+
return { ...meta };
|
|
1275
|
+
} else {
|
|
1276
|
+
return null;
|
|
1278
1277
|
}
|
|
1279
|
-
return null;
|
|
1280
1278
|
}
|
|
1281
1279
|
/**
|
|
1282
1280
|
* Find all tags which has enabled given property.
|
|
@@ -1284,7 +1282,7 @@ class MetaTable {
|
|
|
1284
1282
|
* @public
|
|
1285
1283
|
*/
|
|
1286
1284
|
getTagsWithProperty(propName) {
|
|
1287
|
-
return
|
|
1285
|
+
return this.entries.filter(([, entry]) => entry[propName]).map(([tagName]) => tagName);
|
|
1288
1286
|
}
|
|
1289
1287
|
/**
|
|
1290
1288
|
* Find tag matching tagName or inheriting from it.
|
|
@@ -1292,10 +1290,10 @@ class MetaTable {
|
|
|
1292
1290
|
* @public
|
|
1293
1291
|
*/
|
|
1294
1292
|
getTagsDerivedFrom(tagName) {
|
|
1295
|
-
return
|
|
1293
|
+
return this.entries.filter(([key, entry]) => key === tagName || entry.inherit === tagName).map(([tagName2]) => tagName2);
|
|
1296
1294
|
}
|
|
1297
1295
|
addEntry(tagName, entry) {
|
|
1298
|
-
let parent = this.elements[tagName]
|
|
1296
|
+
let parent = this.elements[tagName];
|
|
1299
1297
|
if (entry.inherit) {
|
|
1300
1298
|
const name = entry.inherit;
|
|
1301
1299
|
parent = this.elements[name];
|
|
@@ -1306,7 +1304,7 @@ class MetaTable {
|
|
|
1306
1304
|
});
|
|
1307
1305
|
}
|
|
1308
1306
|
}
|
|
1309
|
-
const expanded = this.mergeElement(parent, { ...entry, tagName });
|
|
1307
|
+
const expanded = this.mergeElement(parent ?? {}, { ...entry, tagName });
|
|
1310
1308
|
expandRegex(expanded);
|
|
1311
1309
|
this.elements[tagName] = expanded;
|
|
1312
1310
|
}
|
|
@@ -1335,6 +1333,12 @@ class MetaTable {
|
|
|
1335
1333
|
getJSONSchema() {
|
|
1336
1334
|
return this.schema;
|
|
1337
1335
|
}
|
|
1336
|
+
/**
|
|
1337
|
+
* @internal
|
|
1338
|
+
*/
|
|
1339
|
+
get entries() {
|
|
1340
|
+
return Object.entries(this.elements);
|
|
1341
|
+
}
|
|
1338
1342
|
/**
|
|
1339
1343
|
* Finds the global element definition and merges each known element with the
|
|
1340
1344
|
* global, e.g. to assign global attributes.
|
|
@@ -1346,7 +1350,7 @@ class MetaTable {
|
|
|
1346
1350
|
delete this.elements["*"];
|
|
1347
1351
|
delete global.tagName;
|
|
1348
1352
|
delete global.void;
|
|
1349
|
-
for (const [tagName, entry] of
|
|
1353
|
+
for (const [tagName, entry] of this.entries) {
|
|
1350
1354
|
this.elements[tagName] = this.mergeElement(global, entry);
|
|
1351
1355
|
}
|
|
1352
1356
|
}
|
|
@@ -2134,7 +2138,7 @@ class AttrMatcher extends Matcher {
|
|
|
2134
2138
|
this.value = value;
|
|
2135
2139
|
}
|
|
2136
2140
|
match(node) {
|
|
2137
|
-
const attr = node.getAttribute(this.key, true)
|
|
2141
|
+
const attr = node.getAttribute(this.key, true);
|
|
2138
2142
|
return attr.some((cur) => {
|
|
2139
2143
|
switch (this.op) {
|
|
2140
2144
|
case void 0:
|
|
@@ -2580,10 +2584,13 @@ class HtmlElement extends DOMNode {
|
|
|
2580
2584
|
*/
|
|
2581
2585
|
setAttribute(key, value, keyLocation, valueLocation, originalAttribute) {
|
|
2582
2586
|
key = key.toLowerCase();
|
|
2583
|
-
|
|
2584
|
-
|
|
2587
|
+
const attr = new Attribute(key, value, keyLocation, valueLocation, originalAttribute);
|
|
2588
|
+
const list = this.attr[key];
|
|
2589
|
+
if (list) {
|
|
2590
|
+
list.push(attr);
|
|
2591
|
+
} else {
|
|
2592
|
+
this.attr[key] = [attr];
|
|
2585
2593
|
}
|
|
2586
|
-
this.attr[key].push(new Attribute(key, value, keyLocation, valueLocation, originalAttribute));
|
|
2587
2594
|
}
|
|
2588
2595
|
/**
|
|
2589
2596
|
* Get parsed tabindex for this element.
|
|
@@ -2640,7 +2647,7 @@ class HtmlElement extends DOMNode {
|
|
|
2640
2647
|
const matches = this.attr[key];
|
|
2641
2648
|
return all ? matches : matches[0];
|
|
2642
2649
|
} else {
|
|
2643
|
-
return null;
|
|
2650
|
+
return all ? [] : null;
|
|
2644
2651
|
}
|
|
2645
2652
|
}
|
|
2646
2653
|
/**
|
|
@@ -2746,20 +2753,6 @@ class HtmlElement extends DOMNode {
|
|
|
2746
2753
|
yield* pattern.match(this);
|
|
2747
2754
|
}
|
|
2748
2755
|
}
|
|
2749
|
-
/**
|
|
2750
|
-
* Visit all nodes from this node and down. Depth first.
|
|
2751
|
-
*
|
|
2752
|
-
* @internal
|
|
2753
|
-
*/
|
|
2754
|
-
visitDepthFirst(callback) {
|
|
2755
|
-
function visit(node) {
|
|
2756
|
-
node.childElements.forEach(visit);
|
|
2757
|
-
if (!node.isRootElement()) {
|
|
2758
|
-
callback(node);
|
|
2759
|
-
}
|
|
2760
|
-
}
|
|
2761
|
-
visit(this);
|
|
2762
|
-
}
|
|
2763
2756
|
/**
|
|
2764
2757
|
* Evaluates callbackk on all descendants, returning true if any are true.
|
|
2765
2758
|
*
|
|
@@ -2823,591 +2816,133 @@ function isClosed(endToken, meta) {
|
|
|
2823
2816
|
return closed;
|
|
2824
2817
|
}
|
|
2825
2818
|
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
pushActive(node) {
|
|
2833
|
-
this.active = node;
|
|
2819
|
+
function escape(value) {
|
|
2820
|
+
return JSON.stringify(value);
|
|
2821
|
+
}
|
|
2822
|
+
function format(value, quote = false) {
|
|
2823
|
+
if (value === null) {
|
|
2824
|
+
return "null";
|
|
2834
2825
|
}
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
return;
|
|
2838
|
-
}
|
|
2839
|
-
this.active = this.active.parent ?? this.root;
|
|
2826
|
+
if (typeof value === "number") {
|
|
2827
|
+
return value.toString();
|
|
2840
2828
|
}
|
|
2841
|
-
|
|
2842
|
-
return
|
|
2829
|
+
if (typeof value === "string") {
|
|
2830
|
+
return quote ? escape(value) : value;
|
|
2843
2831
|
}
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
resolveMeta(table) {
|
|
2848
|
-
this.visitDepthFirst((node) => {
|
|
2849
|
-
table.resolve(node);
|
|
2850
|
-
});
|
|
2832
|
+
if (Array.isArray(value)) {
|
|
2833
|
+
const content = value.map((it) => format(it, true)).join(", ");
|
|
2834
|
+
return `[ ${content} ]`;
|
|
2851
2835
|
}
|
|
2852
|
-
|
|
2853
|
-
|
|
2836
|
+
if (typeof value === "object") {
|
|
2837
|
+
const content = Object.entries(value).map(([key, nested]) => `${key}: ${format(nested, true)}`).join(", ");
|
|
2838
|
+
return `{ ${content} }`;
|
|
2854
2839
|
}
|
|
2855
|
-
|
|
2856
|
-
|
|
2840
|
+
return String(value);
|
|
2841
|
+
}
|
|
2842
|
+
function interpolate(text, data) {
|
|
2843
|
+
return text.replace(/{{\s*([^\s{}]+)\s*}}/g, (match, key) => {
|
|
2844
|
+
return typeof data[key] !== "undefined" ? format(data[key]) : match;
|
|
2845
|
+
});
|
|
2846
|
+
}
|
|
2847
|
+
|
|
2848
|
+
function isThenable(value) {
|
|
2849
|
+
return value && typeof value === "object" && "then" in value && typeof value.then === "function";
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
var Severity = /* @__PURE__ */ ((Severity2) => {
|
|
2853
|
+
Severity2[Severity2["DISABLED"] = 0] = "DISABLED";
|
|
2854
|
+
Severity2[Severity2["WARN"] = 1] = "WARN";
|
|
2855
|
+
Severity2[Severity2["ERROR"] = 2] = "ERROR";
|
|
2856
|
+
return Severity2;
|
|
2857
|
+
})(Severity || {});
|
|
2858
|
+
function parseSeverity(value) {
|
|
2859
|
+
switch (value) {
|
|
2860
|
+
case 0:
|
|
2861
|
+
case "off":
|
|
2862
|
+
return 0 /* DISABLED */;
|
|
2863
|
+
case 1:
|
|
2864
|
+
case "warn":
|
|
2865
|
+
return 1 /* WARN */;
|
|
2866
|
+
case 2:
|
|
2867
|
+
case "error":
|
|
2868
|
+
return 2 /* ERROR */;
|
|
2869
|
+
default:
|
|
2870
|
+
throw new Error(`Invalid severity "${String(value)}"`);
|
|
2857
2871
|
}
|
|
2858
|
-
|
|
2859
|
-
|
|
2872
|
+
}
|
|
2873
|
+
|
|
2874
|
+
const cacheKey = Symbol("aria-naming");
|
|
2875
|
+
const defaultValue = "allowed";
|
|
2876
|
+
const prohibitedRoles = [
|
|
2877
|
+
"caption",
|
|
2878
|
+
"code",
|
|
2879
|
+
"deletion",
|
|
2880
|
+
"emphasis",
|
|
2881
|
+
"generic",
|
|
2882
|
+
"insertion",
|
|
2883
|
+
"paragraph",
|
|
2884
|
+
"presentation",
|
|
2885
|
+
"strong",
|
|
2886
|
+
"subscript",
|
|
2887
|
+
"superscript"
|
|
2888
|
+
];
|
|
2889
|
+
function byRole(role) {
|
|
2890
|
+
return prohibitedRoles.includes(role) ? "prohibited" : "allowed";
|
|
2891
|
+
}
|
|
2892
|
+
function byMeta(element, meta) {
|
|
2893
|
+
return meta.aria.naming(element._adapter);
|
|
2894
|
+
}
|
|
2895
|
+
function ariaNaming(element) {
|
|
2896
|
+
var _a;
|
|
2897
|
+
const cached = element.cacheGet(cacheKey);
|
|
2898
|
+
if (cached) {
|
|
2899
|
+
return cached;
|
|
2860
2900
|
}
|
|
2861
|
-
|
|
2862
|
-
|
|
2901
|
+
const role = (_a = element.getAttribute("role")) == null ? void 0 : _a.value;
|
|
2902
|
+
if (role) {
|
|
2903
|
+
if (role instanceof DynamicValue) {
|
|
2904
|
+
return element.cacheSet(cacheKey, defaultValue);
|
|
2905
|
+
} else {
|
|
2906
|
+
return element.cacheSet(cacheKey, byRole(role));
|
|
2907
|
+
}
|
|
2863
2908
|
}
|
|
2864
|
-
|
|
2865
|
-
|
|
2909
|
+
const meta = element.meta;
|
|
2910
|
+
if (!meta) {
|
|
2911
|
+
return element.cacheSet(cacheKey, defaultValue);
|
|
2866
2912
|
}
|
|
2913
|
+
return element.cacheSet(cacheKey, byMeta(element, meta));
|
|
2867
2914
|
}
|
|
2868
2915
|
|
|
2869
|
-
const
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
}
|
|
2882
|
-
return rules.some((rule) => {
|
|
2883
|
-
return Validator.validatePermittedRule(node, rule);
|
|
2884
|
-
});
|
|
2916
|
+
const patternCache = /* @__PURE__ */ new Map();
|
|
2917
|
+
function compileStringPattern(pattern) {
|
|
2918
|
+
const regexp = pattern.replace(/[*]+/g, ".+");
|
|
2919
|
+
return new RegExp(`^${regexp}$`);
|
|
2920
|
+
}
|
|
2921
|
+
function compileRegExpPattern(pattern) {
|
|
2922
|
+
return new RegExp(`^${pattern}$`);
|
|
2923
|
+
}
|
|
2924
|
+
function compilePattern(pattern) {
|
|
2925
|
+
const cached = patternCache.get(pattern);
|
|
2926
|
+
if (cached) {
|
|
2927
|
+
return cached;
|
|
2885
2928
|
}
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
* @param rules - List of rules of the parent element.
|
|
2896
|
-
* @returns `true` if the parent element of the children passes the test.
|
|
2897
|
-
*/
|
|
2898
|
-
static validateOccurrences(children, rules, cb) {
|
|
2899
|
-
if (!rules) {
|
|
2929
|
+
const match = pattern.match(/^\/(.*)\/$/);
|
|
2930
|
+
const regexp = match ? compileRegExpPattern(match[1]) : compileStringPattern(pattern);
|
|
2931
|
+
patternCache.set(pattern, regexp);
|
|
2932
|
+
return regexp;
|
|
2933
|
+
}
|
|
2934
|
+
function keywordPatternMatcher(list, keyword) {
|
|
2935
|
+
for (const pattern of list) {
|
|
2936
|
+
const regexp = compilePattern(pattern);
|
|
2937
|
+
if (regexp.test(keyword)) {
|
|
2900
2938
|
return true;
|
|
2901
2939
|
}
|
|
2902
|
-
let valid = true;
|
|
2903
|
-
for (const rule of rules) {
|
|
2904
|
-
if (typeof rule !== "string") {
|
|
2905
|
-
return false;
|
|
2906
|
-
}
|
|
2907
|
-
const [, category, quantifier] = rule.match(/^(@?.*?)([?*]?)$/);
|
|
2908
|
-
const limit = category && quantifier && parseQuantifier(quantifier);
|
|
2909
|
-
if (limit) {
|
|
2910
|
-
const siblings = children.filter(
|
|
2911
|
-
(cur) => Validator.validatePermittedCategory(cur, rule, true)
|
|
2912
|
-
);
|
|
2913
|
-
if (siblings.length > limit) {
|
|
2914
|
-
for (const child of siblings.slice(limit)) {
|
|
2915
|
-
cb(child, category);
|
|
2916
|
-
}
|
|
2917
|
-
valid = false;
|
|
2918
|
-
}
|
|
2919
|
-
}
|
|
2920
|
-
}
|
|
2921
|
-
return valid;
|
|
2922
2940
|
}
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
* specified order.
|
|
2929
|
-
*
|
|
2930
|
-
* For instance, for a `<table>` element the `<caption>` element must come
|
|
2931
|
-
* before a `<thead>` which must come before `<tbody>`.
|
|
2932
|
-
*
|
|
2933
|
-
* @param children - Array of children to validate.
|
|
2934
|
-
*/
|
|
2935
|
-
static validateOrder(children, rules, cb) {
|
|
2936
|
-
if (!rules) {
|
|
2937
|
-
return true;
|
|
2938
|
-
}
|
|
2939
|
-
let i = 0;
|
|
2940
|
-
let prev = null;
|
|
2941
|
-
for (const node of children) {
|
|
2942
|
-
const old = i;
|
|
2943
|
-
while (rules[i] && !Validator.validatePermittedCategory(node, rules[i], true)) {
|
|
2944
|
-
i++;
|
|
2945
|
-
}
|
|
2946
|
-
if (i >= rules.length) {
|
|
2947
|
-
const orderSpecified = rules.find(
|
|
2948
|
-
(cur) => Validator.validatePermittedCategory(node, cur, true)
|
|
2949
|
-
);
|
|
2950
|
-
if (orderSpecified) {
|
|
2951
|
-
cb(node, prev);
|
|
2952
|
-
return false;
|
|
2953
|
-
}
|
|
2954
|
-
i = old;
|
|
2955
|
-
}
|
|
2956
|
-
prev = node;
|
|
2957
|
-
}
|
|
2958
|
-
return true;
|
|
2959
|
-
}
|
|
2960
|
-
/**
|
|
2961
|
-
* Validate element ancestors.
|
|
2962
|
-
*
|
|
2963
|
-
* Check if an element has the required set of elements. At least one of the
|
|
2964
|
-
* selectors must match.
|
|
2965
|
-
*/
|
|
2966
|
-
static validateAncestors(node, rules) {
|
|
2967
|
-
if (!rules || rules.length === 0) {
|
|
2968
|
-
return true;
|
|
2969
|
-
}
|
|
2970
|
-
return rules.some((rule) => node.closest(rule));
|
|
2971
|
-
}
|
|
2972
|
-
/**
|
|
2973
|
-
* Validate element required content.
|
|
2974
|
-
*
|
|
2975
|
-
* Check if an element has the required set of elements. At least one of the
|
|
2976
|
-
* selectors must match.
|
|
2977
|
-
*
|
|
2978
|
-
* Returns `[]` when valid or a list of required but missing tagnames or
|
|
2979
|
-
* categories.
|
|
2980
|
-
*/
|
|
2981
|
-
static validateRequiredContent(node, rules) {
|
|
2982
|
-
if (!rules || rules.length === 0) {
|
|
2983
|
-
return [];
|
|
2984
|
-
}
|
|
2985
|
-
return rules.filter((tagName) => {
|
|
2986
|
-
const haveMatchingChild = node.childElements.some(
|
|
2987
|
-
(child) => Validator.validatePermittedCategory(child, tagName, false)
|
|
2988
|
-
);
|
|
2989
|
-
return !haveMatchingChild;
|
|
2990
|
-
});
|
|
2991
|
-
}
|
|
2992
|
-
/**
|
|
2993
|
-
* Test if an attribute has an allowed value and/or format.
|
|
2994
|
-
*
|
|
2995
|
-
* @param attr - Attribute to test.
|
|
2996
|
-
* @param rules - Element attribute metadta.
|
|
2997
|
-
* @returns `true` if attribute passes all tests.
|
|
2998
|
-
*/
|
|
2999
|
-
static validateAttribute(attr, rules) {
|
|
3000
|
-
const rule = rules[attr.key];
|
|
3001
|
-
if (!rule) {
|
|
3002
|
-
return true;
|
|
3003
|
-
}
|
|
3004
|
-
const value = attr.value;
|
|
3005
|
-
if (value instanceof DynamicValue) {
|
|
3006
|
-
return true;
|
|
3007
|
-
}
|
|
3008
|
-
const empty = value === null || value === "";
|
|
3009
|
-
if (rule.boolean) {
|
|
3010
|
-
return empty || value === attr.key;
|
|
3011
|
-
}
|
|
3012
|
-
if (rule.omit && empty) {
|
|
3013
|
-
return true;
|
|
3014
|
-
}
|
|
3015
|
-
if (rule.list) {
|
|
3016
|
-
const tokens = new DOMTokenList(value, attr.valueLocation);
|
|
3017
|
-
return tokens.every((token) => {
|
|
3018
|
-
return this.validateAttributeValue(token, rule);
|
|
3019
|
-
});
|
|
3020
|
-
}
|
|
3021
|
-
return this.validateAttributeValue(value, rule);
|
|
3022
|
-
}
|
|
3023
|
-
static validateAttributeValue(value, rule) {
|
|
3024
|
-
if (!rule.enum) {
|
|
3025
|
-
return true;
|
|
3026
|
-
}
|
|
3027
|
-
if (value === null || value === void 0) {
|
|
3028
|
-
return false;
|
|
3029
|
-
}
|
|
3030
|
-
const caseInsensitiveValue = value.toLowerCase();
|
|
3031
|
-
return rule.enum.some((entry) => {
|
|
3032
|
-
if (entry instanceof RegExp) {
|
|
3033
|
-
return !!value.match(entry);
|
|
3034
|
-
} else {
|
|
3035
|
-
return caseInsensitiveValue === entry;
|
|
3036
|
-
}
|
|
3037
|
-
});
|
|
3038
|
-
}
|
|
3039
|
-
static validatePermittedRule(node, rule, isExclude = false) {
|
|
3040
|
-
if (typeof rule === "string") {
|
|
3041
|
-
return Validator.validatePermittedCategory(node, rule, !isExclude);
|
|
3042
|
-
} else if (Array.isArray(rule)) {
|
|
3043
|
-
return rule.every((inner) => {
|
|
3044
|
-
return Validator.validatePermittedRule(node, inner, isExclude);
|
|
3045
|
-
});
|
|
3046
|
-
} else {
|
|
3047
|
-
validateKeys(rule);
|
|
3048
|
-
if (rule.exclude) {
|
|
3049
|
-
if (Array.isArray(rule.exclude)) {
|
|
3050
|
-
return !rule.exclude.some((inner) => {
|
|
3051
|
-
return Validator.validatePermittedRule(node, inner, true);
|
|
3052
|
-
});
|
|
3053
|
-
} else {
|
|
3054
|
-
return !Validator.validatePermittedRule(node, rule.exclude, true);
|
|
3055
|
-
}
|
|
3056
|
-
} else {
|
|
3057
|
-
return true;
|
|
3058
|
-
}
|
|
3059
|
-
}
|
|
3060
|
-
}
|
|
3061
|
-
/**
|
|
3062
|
-
* Validate node against a content category.
|
|
3063
|
-
*
|
|
3064
|
-
* When matching parent nodes against permitted parents use the superset
|
|
3065
|
-
* parameter to also match for `@flow`. E.g. if a node expects a `@phrasing`
|
|
3066
|
-
* parent it should also allow `@flow` parent since `@phrasing` is a subset of
|
|
3067
|
-
* `@flow`.
|
|
3068
|
-
*
|
|
3069
|
-
* @param node - The node to test against
|
|
3070
|
-
* @param category - Name of category with `@` prefix or tag name.
|
|
3071
|
-
* @param defaultMatch - The default return value when node categories is not known.
|
|
3072
|
-
*/
|
|
3073
|
-
/* eslint-disable-next-line complexity -- rule does not like switch */
|
|
3074
|
-
static validatePermittedCategory(node, category, defaultMatch) {
|
|
3075
|
-
const [, rawCategory] = category.match(/^(@?.*?)([?*]?)$/);
|
|
3076
|
-
if (!rawCategory.startsWith("@")) {
|
|
3077
|
-
return node.tagName === rawCategory;
|
|
3078
|
-
}
|
|
3079
|
-
if (!node.meta) {
|
|
3080
|
-
return defaultMatch;
|
|
3081
|
-
}
|
|
3082
|
-
switch (rawCategory) {
|
|
3083
|
-
case "@meta":
|
|
3084
|
-
return node.meta.metadata;
|
|
3085
|
-
case "@flow":
|
|
3086
|
-
return node.meta.flow;
|
|
3087
|
-
case "@sectioning":
|
|
3088
|
-
return node.meta.sectioning;
|
|
3089
|
-
case "@heading":
|
|
3090
|
-
return node.meta.heading;
|
|
3091
|
-
case "@phrasing":
|
|
3092
|
-
return node.meta.phrasing;
|
|
3093
|
-
case "@embedded":
|
|
3094
|
-
return node.meta.embedded;
|
|
3095
|
-
case "@interactive":
|
|
3096
|
-
return node.meta.interactive;
|
|
3097
|
-
case "@script":
|
|
3098
|
-
return Boolean(node.meta.scriptSupporting);
|
|
3099
|
-
case "@form":
|
|
3100
|
-
return Boolean(node.meta.form);
|
|
3101
|
-
default:
|
|
3102
|
-
throw new Error(`Invalid content category "${category}"`);
|
|
3103
|
-
}
|
|
3104
|
-
}
|
|
3105
|
-
}
|
|
3106
|
-
function validateKeys(rule) {
|
|
3107
|
-
for (const key of Object.keys(rule)) {
|
|
3108
|
-
if (!allowedKeys.includes(key)) {
|
|
3109
|
-
const str = JSON.stringify(rule);
|
|
3110
|
-
throw new Error(`Permitted rule "${str}" contains unknown property "${key}"`);
|
|
3111
|
-
}
|
|
3112
|
-
}
|
|
3113
|
-
}
|
|
3114
|
-
function parseQuantifier(quantifier) {
|
|
3115
|
-
switch (quantifier) {
|
|
3116
|
-
case "?":
|
|
3117
|
-
return 1;
|
|
3118
|
-
case "*":
|
|
3119
|
-
return null;
|
|
3120
|
-
default:
|
|
3121
|
-
throw new Error(`Invalid quantifier "${quantifier}" used`);
|
|
3122
|
-
}
|
|
3123
|
-
}
|
|
3124
|
-
|
|
3125
|
-
const $schema = "http://json-schema.org/draft-06/schema#";
|
|
3126
|
-
const $id = "https://html-validate.org/schemas/config.json";
|
|
3127
|
-
const type = "object";
|
|
3128
|
-
const additionalProperties = false;
|
|
3129
|
-
const properties = {
|
|
3130
|
-
$schema: {
|
|
3131
|
-
type: "string"
|
|
3132
|
-
},
|
|
3133
|
-
root: {
|
|
3134
|
-
type: "boolean",
|
|
3135
|
-
title: "Mark as root configuration",
|
|
3136
|
-
description: "If this is set to true no further configurations will be searched.",
|
|
3137
|
-
"default": false
|
|
3138
|
-
},
|
|
3139
|
-
"extends": {
|
|
3140
|
-
type: "array",
|
|
3141
|
-
items: {
|
|
3142
|
-
type: "string"
|
|
3143
|
-
},
|
|
3144
|
-
title: "Configurations to extend",
|
|
3145
|
-
description: "Array of shareable or builtin configurations to extend."
|
|
3146
|
-
},
|
|
3147
|
-
elements: {
|
|
3148
|
-
type: "array",
|
|
3149
|
-
items: {
|
|
3150
|
-
anyOf: [
|
|
3151
|
-
{
|
|
3152
|
-
type: "string"
|
|
3153
|
-
},
|
|
3154
|
-
{
|
|
3155
|
-
type: "object"
|
|
3156
|
-
}
|
|
3157
|
-
]
|
|
3158
|
-
},
|
|
3159
|
-
title: "Element metadata to load",
|
|
3160
|
-
description: "Array of modules, plugins or files to load element metadata from. Use <rootDir> to refer to the folder with the package.json file.",
|
|
3161
|
-
examples: [
|
|
3162
|
-
[
|
|
3163
|
-
"html-validate:recommended",
|
|
3164
|
-
"plugin:recommended",
|
|
3165
|
-
"module",
|
|
3166
|
-
"./local-file.json"
|
|
3167
|
-
]
|
|
3168
|
-
]
|
|
3169
|
-
},
|
|
3170
|
-
plugins: {
|
|
3171
|
-
type: "array",
|
|
3172
|
-
items: {
|
|
3173
|
-
anyOf: [
|
|
3174
|
-
{
|
|
3175
|
-
type: "string"
|
|
3176
|
-
},
|
|
3177
|
-
{
|
|
3178
|
-
type: "object"
|
|
3179
|
-
}
|
|
3180
|
-
]
|
|
3181
|
-
},
|
|
3182
|
-
title: "Plugins to load",
|
|
3183
|
-
description: "Array of plugins load. Use <rootDir> to refer to the folder with the package.json file.",
|
|
3184
|
-
examples: [
|
|
3185
|
-
[
|
|
3186
|
-
"my-plugin",
|
|
3187
|
-
"./local-plugin"
|
|
3188
|
-
]
|
|
3189
|
-
]
|
|
3190
|
-
},
|
|
3191
|
-
transform: {
|
|
3192
|
-
type: "object",
|
|
3193
|
-
additionalProperties: {
|
|
3194
|
-
type: "string"
|
|
3195
|
-
},
|
|
3196
|
-
title: "File transformations to use.",
|
|
3197
|
-
description: "Object where key is regular expression to match filename and value is name of transformer.",
|
|
3198
|
-
examples: [
|
|
3199
|
-
{
|
|
3200
|
-
"^.*\\.foo$": "my-transformer",
|
|
3201
|
-
"^.*\\.bar$": "my-plugin",
|
|
3202
|
-
"^.*\\.baz$": "my-plugin:named"
|
|
3203
|
-
}
|
|
3204
|
-
]
|
|
3205
|
-
},
|
|
3206
|
-
rules: {
|
|
3207
|
-
type: "object",
|
|
3208
|
-
patternProperties: {
|
|
3209
|
-
".*": {
|
|
3210
|
-
anyOf: [
|
|
3211
|
-
{
|
|
3212
|
-
"enum": [
|
|
3213
|
-
0,
|
|
3214
|
-
1,
|
|
3215
|
-
2,
|
|
3216
|
-
"off",
|
|
3217
|
-
"warn",
|
|
3218
|
-
"error"
|
|
3219
|
-
]
|
|
3220
|
-
},
|
|
3221
|
-
{
|
|
3222
|
-
type: "array",
|
|
3223
|
-
minItems: 1,
|
|
3224
|
-
maxItems: 1,
|
|
3225
|
-
items: [
|
|
3226
|
-
{
|
|
3227
|
-
"enum": [
|
|
3228
|
-
0,
|
|
3229
|
-
1,
|
|
3230
|
-
2,
|
|
3231
|
-
"off",
|
|
3232
|
-
"warn",
|
|
3233
|
-
"error"
|
|
3234
|
-
]
|
|
3235
|
-
}
|
|
3236
|
-
]
|
|
3237
|
-
},
|
|
3238
|
-
{
|
|
3239
|
-
type: "array",
|
|
3240
|
-
minItems: 2,
|
|
3241
|
-
maxItems: 2,
|
|
3242
|
-
items: [
|
|
3243
|
-
{
|
|
3244
|
-
"enum": [
|
|
3245
|
-
0,
|
|
3246
|
-
1,
|
|
3247
|
-
2,
|
|
3248
|
-
"off",
|
|
3249
|
-
"warn",
|
|
3250
|
-
"error"
|
|
3251
|
-
]
|
|
3252
|
-
},
|
|
3253
|
-
{
|
|
3254
|
-
}
|
|
3255
|
-
]
|
|
3256
|
-
}
|
|
3257
|
-
]
|
|
3258
|
-
}
|
|
3259
|
-
},
|
|
3260
|
-
title: "Rule configuration.",
|
|
3261
|
-
description: "Enable/disable rules, set severity. Some rules have additional configuration like style or patterns to use.",
|
|
3262
|
-
examples: [
|
|
3263
|
-
{
|
|
3264
|
-
foo: "error",
|
|
3265
|
-
bar: "off",
|
|
3266
|
-
baz: [
|
|
3267
|
-
"error",
|
|
3268
|
-
{
|
|
3269
|
-
style: "camelcase"
|
|
3270
|
-
}
|
|
3271
|
-
]
|
|
3272
|
-
}
|
|
3273
|
-
]
|
|
3274
|
-
}
|
|
3275
|
-
};
|
|
3276
|
-
var configurationSchema = {
|
|
3277
|
-
$schema: $schema,
|
|
3278
|
-
$id: $id,
|
|
3279
|
-
type: type,
|
|
3280
|
-
additionalProperties: additionalProperties,
|
|
3281
|
-
properties: properties
|
|
3282
|
-
};
|
|
3283
|
-
|
|
3284
|
-
const TRANSFORMER_API = {
|
|
3285
|
-
VERSION: 1
|
|
3286
|
-
};
|
|
3287
|
-
|
|
3288
|
-
var Severity = /* @__PURE__ */ ((Severity2) => {
|
|
3289
|
-
Severity2[Severity2["DISABLED"] = 0] = "DISABLED";
|
|
3290
|
-
Severity2[Severity2["WARN"] = 1] = "WARN";
|
|
3291
|
-
Severity2[Severity2["ERROR"] = 2] = "ERROR";
|
|
3292
|
-
return Severity2;
|
|
3293
|
-
})(Severity || {});
|
|
3294
|
-
function parseSeverity(value) {
|
|
3295
|
-
switch (value) {
|
|
3296
|
-
case 0:
|
|
3297
|
-
case "off":
|
|
3298
|
-
return 0 /* DISABLED */;
|
|
3299
|
-
case 1:
|
|
3300
|
-
case "warn":
|
|
3301
|
-
return 1 /* WARN */;
|
|
3302
|
-
case 2:
|
|
3303
|
-
case "error":
|
|
3304
|
-
return 2 /* ERROR */;
|
|
3305
|
-
default:
|
|
3306
|
-
throw new Error(`Invalid severity "${String(value)}"`);
|
|
3307
|
-
}
|
|
3308
|
-
}
|
|
3309
|
-
|
|
3310
|
-
function escape(value) {
|
|
3311
|
-
return JSON.stringify(value);
|
|
3312
|
-
}
|
|
3313
|
-
function format(value, quote = false) {
|
|
3314
|
-
if (value === null) {
|
|
3315
|
-
return "null";
|
|
3316
|
-
}
|
|
3317
|
-
if (typeof value === "number") {
|
|
3318
|
-
return value.toString();
|
|
3319
|
-
}
|
|
3320
|
-
if (typeof value === "string") {
|
|
3321
|
-
return quote ? escape(value) : value;
|
|
3322
|
-
}
|
|
3323
|
-
if (Array.isArray(value)) {
|
|
3324
|
-
const content = value.map((it) => format(it, true)).join(", ");
|
|
3325
|
-
return `[ ${content} ]`;
|
|
3326
|
-
}
|
|
3327
|
-
if (typeof value === "object") {
|
|
3328
|
-
const content = Object.entries(value).map(([key, nested]) => `${key}: ${format(nested, true)}`).join(", ");
|
|
3329
|
-
return `{ ${content} }`;
|
|
3330
|
-
}
|
|
3331
|
-
return String(value);
|
|
3332
|
-
}
|
|
3333
|
-
function interpolate(text, data) {
|
|
3334
|
-
return text.replace(/{{\s*([^\s{}]+)\s*}}/g, (match, key) => {
|
|
3335
|
-
return typeof data[key] !== "undefined" ? format(data[key]) : match;
|
|
3336
|
-
});
|
|
3337
|
-
}
|
|
3338
|
-
|
|
3339
|
-
const cacheKey = Symbol("aria-naming");
|
|
3340
|
-
const defaultValue = "allowed";
|
|
3341
|
-
const prohibitedRoles = [
|
|
3342
|
-
"caption",
|
|
3343
|
-
"code",
|
|
3344
|
-
"deletion",
|
|
3345
|
-
"emphasis",
|
|
3346
|
-
"generic",
|
|
3347
|
-
"insertion",
|
|
3348
|
-
"paragraph",
|
|
3349
|
-
"presentation",
|
|
3350
|
-
"strong",
|
|
3351
|
-
"subscript",
|
|
3352
|
-
"superscript"
|
|
3353
|
-
];
|
|
3354
|
-
function byRole(role) {
|
|
3355
|
-
return prohibitedRoles.includes(role) ? "prohibited" : "allowed";
|
|
3356
|
-
}
|
|
3357
|
-
function byMeta(element, meta) {
|
|
3358
|
-
return meta.aria.naming(element._adapter);
|
|
3359
|
-
}
|
|
3360
|
-
function ariaNaming(element) {
|
|
3361
|
-
var _a;
|
|
3362
|
-
const cached = element.cacheGet(cacheKey);
|
|
3363
|
-
if (cached) {
|
|
3364
|
-
return cached;
|
|
3365
|
-
}
|
|
3366
|
-
const role = (_a = element.getAttribute("role")) == null ? void 0 : _a.value;
|
|
3367
|
-
if (role) {
|
|
3368
|
-
if (role instanceof DynamicValue) {
|
|
3369
|
-
return element.cacheSet(cacheKey, defaultValue);
|
|
3370
|
-
} else {
|
|
3371
|
-
return element.cacheSet(cacheKey, byRole(role));
|
|
3372
|
-
}
|
|
3373
|
-
}
|
|
3374
|
-
const meta = element.meta;
|
|
3375
|
-
if (!meta) {
|
|
3376
|
-
return element.cacheSet(cacheKey, defaultValue);
|
|
3377
|
-
}
|
|
3378
|
-
return element.cacheSet(cacheKey, byMeta(element, meta));
|
|
3379
|
-
}
|
|
3380
|
-
|
|
3381
|
-
const patternCache = /* @__PURE__ */ new Map();
|
|
3382
|
-
function compileStringPattern(pattern) {
|
|
3383
|
-
const regexp = pattern.replace(/[*]+/g, ".+");
|
|
3384
|
-
return new RegExp(`^${regexp}$`);
|
|
3385
|
-
}
|
|
3386
|
-
function compileRegExpPattern(pattern) {
|
|
3387
|
-
return new RegExp(`^${pattern}$`);
|
|
3388
|
-
}
|
|
3389
|
-
function compilePattern(pattern) {
|
|
3390
|
-
const cached = patternCache.get(pattern);
|
|
3391
|
-
if (cached) {
|
|
3392
|
-
return cached;
|
|
3393
|
-
}
|
|
3394
|
-
const match = pattern.match(/^\/(.*)\/$/);
|
|
3395
|
-
const regexp = match ? compileRegExpPattern(match[1]) : compileStringPattern(pattern);
|
|
3396
|
-
patternCache.set(pattern, regexp);
|
|
3397
|
-
return regexp;
|
|
3398
|
-
}
|
|
3399
|
-
function keywordPatternMatcher(list, keyword) {
|
|
3400
|
-
for (const pattern of list) {
|
|
3401
|
-
const regexp = compilePattern(pattern);
|
|
3402
|
-
if (regexp.test(keyword)) {
|
|
3403
|
-
return true;
|
|
3404
|
-
}
|
|
3405
|
-
}
|
|
3406
|
-
return false;
|
|
3407
|
-
}
|
|
3408
|
-
function isKeywordIgnored(options, keyword, matcher = (list, it) => list.includes(it)) {
|
|
3409
|
-
const { include, exclude } = options;
|
|
3410
|
-
if (include && !matcher(include, keyword)) {
|
|
2941
|
+
return false;
|
|
2942
|
+
}
|
|
2943
|
+
function isKeywordIgnored(options, keyword, matcher = (list, it) => list.includes(it)) {
|
|
2944
|
+
const { include, exclude } = options;
|
|
2945
|
+
if (include && !matcher(include, keyword)) {
|
|
3411
2946
|
return true;
|
|
3412
2947
|
}
|
|
3413
2948
|
if (exclude && matcher(exclude, keyword)) {
|
|
@@ -5012,7 +4547,7 @@ class AttributeAllowedValues extends Rule {
|
|
|
5012
4547
|
setup() {
|
|
5013
4548
|
this.on("dom:ready", (event) => {
|
|
5014
4549
|
const doc = event.document;
|
|
5015
|
-
|
|
4550
|
+
walk.depthFirst(doc, (node) => {
|
|
5016
4551
|
const meta = node.meta;
|
|
5017
4552
|
if (!(meta == null ? void 0 : meta.attributes))
|
|
5018
4553
|
return;
|
|
@@ -5043,11 +4578,7 @@ class AttributeAllowedValues extends Rule {
|
|
|
5043
4578
|
}
|
|
5044
4579
|
}
|
|
5045
4580
|
getLocation(attr) {
|
|
5046
|
-
|
|
5047
|
-
return attr.valueLocation;
|
|
5048
|
-
} else {
|
|
5049
|
-
return attr.keyLocation;
|
|
5050
|
-
}
|
|
4581
|
+
return attr.valueLocation ?? attr.keyLocation;
|
|
5051
4582
|
}
|
|
5052
4583
|
}
|
|
5053
4584
|
|
|
@@ -5076,7 +4607,7 @@ class AttributeBooleanStyle extends Rule {
|
|
|
5076
4607
|
setup() {
|
|
5077
4608
|
this.on("dom:ready", (event) => {
|
|
5078
4609
|
const doc = event.document;
|
|
5079
|
-
|
|
4610
|
+
walk.depthFirst(doc, (node) => {
|
|
5080
4611
|
const meta = node.meta;
|
|
5081
4612
|
if (!(meta == null ? void 0 : meta.attributes))
|
|
5082
4613
|
return;
|
|
@@ -5148,7 +4679,7 @@ class AttributeEmptyStyle extends Rule {
|
|
|
5148
4679
|
setup() {
|
|
5149
4680
|
this.on("dom:ready", (event) => {
|
|
5150
4681
|
const doc = event.document;
|
|
5151
|
-
|
|
4682
|
+
walk.depthFirst(doc, (node) => {
|
|
5152
4683
|
const meta = node.meta;
|
|
5153
4684
|
if (!(meta == null ? void 0 : meta.attributes))
|
|
5154
4685
|
return;
|
|
@@ -5800,7 +5331,7 @@ class ElementPermittedContent extends Rule {
|
|
|
5800
5331
|
setup() {
|
|
5801
5332
|
this.on("dom:ready", (event) => {
|
|
5802
5333
|
const doc = event.document;
|
|
5803
|
-
|
|
5334
|
+
walk.depthFirst(doc, (node) => {
|
|
5804
5335
|
const parent = node.parent;
|
|
5805
5336
|
if (!parent) {
|
|
5806
5337
|
return;
|
|
@@ -5879,7 +5410,7 @@ class ElementPermittedOccurrences extends Rule {
|
|
|
5879
5410
|
setup() {
|
|
5880
5411
|
this.on("dom:ready", (event) => {
|
|
5881
5412
|
const doc = event.document;
|
|
5882
|
-
|
|
5413
|
+
walk.depthFirst(doc, (node) => {
|
|
5883
5414
|
if (!node.meta) {
|
|
5884
5415
|
return;
|
|
5885
5416
|
}
|
|
@@ -5912,7 +5443,7 @@ class ElementPermittedOrder extends Rule {
|
|
|
5912
5443
|
setup() {
|
|
5913
5444
|
this.on("dom:ready", (event) => {
|
|
5914
5445
|
const doc = event.document;
|
|
5915
|
-
|
|
5446
|
+
walk.depthFirst(doc, (node) => {
|
|
5916
5447
|
if (!node.meta) {
|
|
5917
5448
|
return;
|
|
5918
5449
|
}
|
|
@@ -5982,7 +5513,7 @@ class ElementPermittedParent extends Rule {
|
|
|
5982
5513
|
setup() {
|
|
5983
5514
|
this.on("dom:ready", (event) => {
|
|
5984
5515
|
const doc = event.document;
|
|
5985
|
-
|
|
5516
|
+
walk.depthFirst(doc, (node) => {
|
|
5986
5517
|
var _a;
|
|
5987
5518
|
const parent = node.parent;
|
|
5988
5519
|
if (!parent) {
|
|
@@ -6030,7 +5561,7 @@ class ElementRequiredAncestor extends Rule {
|
|
|
6030
5561
|
setup() {
|
|
6031
5562
|
this.on("dom:ready", (event) => {
|
|
6032
5563
|
const doc = event.document;
|
|
6033
|
-
|
|
5564
|
+
walk.depthFirst(doc, (node) => {
|
|
6034
5565
|
const parent = node.parent;
|
|
6035
5566
|
if (!parent) {
|
|
6036
5567
|
return;
|
|
@@ -6114,7 +5645,7 @@ class ElementRequiredContent extends Rule {
|
|
|
6114
5645
|
setup() {
|
|
6115
5646
|
this.on("dom:ready", (event) => {
|
|
6116
5647
|
const doc = event.document;
|
|
6117
|
-
|
|
5648
|
+
walk.depthFirst(doc, (node) => {
|
|
6118
5649
|
if (!node.meta) {
|
|
6119
5650
|
return;
|
|
6120
5651
|
}
|
|
@@ -6634,7 +6165,7 @@ function isFocusableImpl(element) {
|
|
|
6634
6165
|
if (isDisabled(element, meta)) {
|
|
6635
6166
|
return false;
|
|
6636
6167
|
}
|
|
6637
|
-
return Boolean(meta
|
|
6168
|
+
return Boolean(meta.focusable);
|
|
6638
6169
|
}
|
|
6639
6170
|
function isFocusable(element) {
|
|
6640
6171
|
const cached = element.cacheGet(FOCUSABLE_CACHE);
|
|
@@ -8544,1778 +8075,2291 @@ class RequireSri extends Rule {
|
|
|
8544
8075
|
items: {
|
|
8545
8076
|
type: "string"
|
|
8546
8077
|
},
|
|
8547
|
-
type: "array"
|
|
8548
|
-
},
|
|
8549
|
-
{
|
|
8550
|
-
type: "null"
|
|
8551
|
-
}
|
|
8552
|
-
]
|
|
8553
|
-
},
|
|
8554
|
-
exclude: {
|
|
8555
|
-
anyOf: [
|
|
8556
|
-
{
|
|
8557
|
-
items: {
|
|
8078
|
+
type: "array"
|
|
8079
|
+
},
|
|
8080
|
+
{
|
|
8081
|
+
type: "null"
|
|
8082
|
+
}
|
|
8083
|
+
]
|
|
8084
|
+
},
|
|
8085
|
+
exclude: {
|
|
8086
|
+
anyOf: [
|
|
8087
|
+
{
|
|
8088
|
+
items: {
|
|
8089
|
+
type: "string"
|
|
8090
|
+
},
|
|
8091
|
+
type: "array"
|
|
8092
|
+
},
|
|
8093
|
+
{
|
|
8094
|
+
type: "null"
|
|
8095
|
+
}
|
|
8096
|
+
]
|
|
8097
|
+
}
|
|
8098
|
+
};
|
|
8099
|
+
}
|
|
8100
|
+
documentation() {
|
|
8101
|
+
return {
|
|
8102
|
+
description: `Subresource Integrity (SRI) \`integrity\` attribute is required to prevent tampering or manipulation from Content Delivery Networks (CDN), rouge proxies, malicious entities, etc.`,
|
|
8103
|
+
url: "https://html-validate.org/rules/require-sri.html"
|
|
8104
|
+
};
|
|
8105
|
+
}
|
|
8106
|
+
setup() {
|
|
8107
|
+
this.on("tag:end", (event) => {
|
|
8108
|
+
const node = event.previous;
|
|
8109
|
+
if (!(this.supportSri(node) && this.needSri(node))) {
|
|
8110
|
+
return;
|
|
8111
|
+
}
|
|
8112
|
+
if (node.hasAttribute("integrity")) {
|
|
8113
|
+
return;
|
|
8114
|
+
}
|
|
8115
|
+
this.report(
|
|
8116
|
+
node,
|
|
8117
|
+
`SRI "integrity" attribute is required on <${node.tagName}> element`,
|
|
8118
|
+
node.location
|
|
8119
|
+
);
|
|
8120
|
+
});
|
|
8121
|
+
}
|
|
8122
|
+
supportSri(node) {
|
|
8123
|
+
return Object.keys(supportSri).includes(node.tagName);
|
|
8124
|
+
}
|
|
8125
|
+
needSri(node) {
|
|
8126
|
+
const attr = this.elementSourceAttr(node);
|
|
8127
|
+
if (!attr) {
|
|
8128
|
+
return false;
|
|
8129
|
+
}
|
|
8130
|
+
if (attr.value === null || attr.value === "" || attr.isDynamic) {
|
|
8131
|
+
return false;
|
|
8132
|
+
}
|
|
8133
|
+
const url = attr.value.toString();
|
|
8134
|
+
if (this.target === "all" || crossorigin.test(url)) {
|
|
8135
|
+
return !this.isIgnored(url);
|
|
8136
|
+
}
|
|
8137
|
+
return false;
|
|
8138
|
+
}
|
|
8139
|
+
elementSourceAttr(node) {
|
|
8140
|
+
const key = supportSri[node.tagName];
|
|
8141
|
+
return node.getAttribute(key);
|
|
8142
|
+
}
|
|
8143
|
+
isIgnored(url) {
|
|
8144
|
+
return this.isKeywordIgnored(url, (list, it) => {
|
|
8145
|
+
return list.some((pattern) => it.includes(pattern));
|
|
8146
|
+
});
|
|
8147
|
+
}
|
|
8148
|
+
}
|
|
8149
|
+
|
|
8150
|
+
class ScriptElement extends Rule {
|
|
8151
|
+
documentation() {
|
|
8152
|
+
return {
|
|
8153
|
+
description: "The end tag for `<script>` is a hard requirement and must never be omitted even when using the `src` attribute.",
|
|
8154
|
+
url: "https://html-validate.org/rules/script-element.html"
|
|
8155
|
+
};
|
|
8156
|
+
}
|
|
8157
|
+
setup() {
|
|
8158
|
+
this.on("tag:end", (event) => {
|
|
8159
|
+
const node = event.target;
|
|
8160
|
+
if (!node || node.tagName !== "script") {
|
|
8161
|
+
return;
|
|
8162
|
+
}
|
|
8163
|
+
if (node.closed !== NodeClosed.EndTag) {
|
|
8164
|
+
this.report(node, `End tag for <${node.tagName}> must not be omitted`);
|
|
8165
|
+
}
|
|
8166
|
+
});
|
|
8167
|
+
}
|
|
8168
|
+
}
|
|
8169
|
+
|
|
8170
|
+
const javascript = [
|
|
8171
|
+
"",
|
|
8172
|
+
"application/ecmascript",
|
|
8173
|
+
"application/javascript",
|
|
8174
|
+
"text/ecmascript",
|
|
8175
|
+
"text/javascript"
|
|
8176
|
+
];
|
|
8177
|
+
class ScriptType extends Rule {
|
|
8178
|
+
documentation() {
|
|
8179
|
+
return {
|
|
8180
|
+
description: "While valid the HTML5 standard encourages authors to omit the type element for JavaScript resources.",
|
|
8181
|
+
url: "https://html-validate.org/rules/script-type.html"
|
|
8182
|
+
};
|
|
8183
|
+
}
|
|
8184
|
+
setup() {
|
|
8185
|
+
this.on("tag:end", (event) => {
|
|
8186
|
+
const node = event.previous;
|
|
8187
|
+
if (node.tagName !== "script") {
|
|
8188
|
+
return;
|
|
8189
|
+
}
|
|
8190
|
+
const attr = node.getAttribute("type");
|
|
8191
|
+
if (!attr || attr.isDynamic) {
|
|
8192
|
+
return;
|
|
8193
|
+
}
|
|
8194
|
+
const value = attr.value ? attr.value.toString() : "";
|
|
8195
|
+
if (!this.isJavascript(value)) {
|
|
8196
|
+
return;
|
|
8197
|
+
}
|
|
8198
|
+
this.report(
|
|
8199
|
+
node,
|
|
8200
|
+
'"type" attribute is unnecessary for javascript resources',
|
|
8201
|
+
attr.keyLocation
|
|
8202
|
+
);
|
|
8203
|
+
});
|
|
8204
|
+
}
|
|
8205
|
+
isJavascript(mime) {
|
|
8206
|
+
const type = mime.replace(/;.*/, "");
|
|
8207
|
+
return javascript.includes(type);
|
|
8208
|
+
}
|
|
8209
|
+
}
|
|
8210
|
+
|
|
8211
|
+
class SvgFocusable extends Rule {
|
|
8212
|
+
documentation() {
|
|
8213
|
+
return {
|
|
8214
|
+
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.`,
|
|
8215
|
+
url: "https://html-validate.org/rules/svg-focusable.html"
|
|
8216
|
+
};
|
|
8217
|
+
}
|
|
8218
|
+
setup() {
|
|
8219
|
+
this.on("element:ready", (event) => {
|
|
8220
|
+
if (event.target.is("svg")) {
|
|
8221
|
+
this.validate(event.target);
|
|
8222
|
+
}
|
|
8223
|
+
});
|
|
8224
|
+
}
|
|
8225
|
+
validate(svg) {
|
|
8226
|
+
if (svg.hasAttribute("focusable")) {
|
|
8227
|
+
return;
|
|
8228
|
+
}
|
|
8229
|
+
this.report(svg, `<${svg.tagName}> is missing required "focusable" attribute`);
|
|
8230
|
+
}
|
|
8231
|
+
}
|
|
8232
|
+
|
|
8233
|
+
const defaults$5 = {
|
|
8234
|
+
characters: [
|
|
8235
|
+
{ pattern: " ", replacement: " ", description: "non-breaking space" },
|
|
8236
|
+
{ pattern: "-", replacement: "‑", description: "non-breaking hyphen" }
|
|
8237
|
+
],
|
|
8238
|
+
ignoreClasses: [],
|
|
8239
|
+
ignoreStyle: true
|
|
8240
|
+
};
|
|
8241
|
+
function constructRegex(characters) {
|
|
8242
|
+
const disallowed = characters.map((it) => {
|
|
8243
|
+
return it.pattern;
|
|
8244
|
+
}).join("|");
|
|
8245
|
+
const pattern = `(${disallowed})`;
|
|
8246
|
+
return new RegExp(pattern, "g");
|
|
8247
|
+
}
|
|
8248
|
+
function getText(node) {
|
|
8249
|
+
const match = node.textContent.match(/^(\s*)(.*)$/);
|
|
8250
|
+
const [, leading, text] = match;
|
|
8251
|
+
return [leading.length, text.trimEnd()];
|
|
8252
|
+
}
|
|
8253
|
+
function matchAll(text, regexp) {
|
|
8254
|
+
const copy = new RegExp(regexp);
|
|
8255
|
+
const matches = [];
|
|
8256
|
+
let match;
|
|
8257
|
+
while (match = copy.exec(text)) {
|
|
8258
|
+
matches.push(match);
|
|
8259
|
+
}
|
|
8260
|
+
return matches;
|
|
8261
|
+
}
|
|
8262
|
+
class TelNonBreaking extends Rule {
|
|
8263
|
+
constructor(options) {
|
|
8264
|
+
super({ ...defaults$5, ...options });
|
|
8265
|
+
this.regex = constructRegex(this.options.characters);
|
|
8266
|
+
}
|
|
8267
|
+
static schema() {
|
|
8268
|
+
return {
|
|
8269
|
+
characters: {
|
|
8270
|
+
type: "array",
|
|
8271
|
+
items: {
|
|
8272
|
+
type: "object",
|
|
8273
|
+
additionalProperties: false,
|
|
8274
|
+
properties: {
|
|
8275
|
+
pattern: {
|
|
8276
|
+
type: "string"
|
|
8277
|
+
},
|
|
8278
|
+
replacement: {
|
|
8558
8279
|
type: "string"
|
|
8559
8280
|
},
|
|
8560
|
-
|
|
8561
|
-
|
|
8562
|
-
|
|
8563
|
-
type: "null"
|
|
8281
|
+
description: {
|
|
8282
|
+
type: "string"
|
|
8283
|
+
}
|
|
8564
8284
|
}
|
|
8565
|
-
|
|
8285
|
+
}
|
|
8286
|
+
},
|
|
8287
|
+
ignoreClasses: {
|
|
8288
|
+
type: "array",
|
|
8289
|
+
items: {
|
|
8290
|
+
type: "string"
|
|
8291
|
+
}
|
|
8292
|
+
},
|
|
8293
|
+
ignoreStyle: {
|
|
8294
|
+
type: "boolean"
|
|
8566
8295
|
}
|
|
8567
8296
|
};
|
|
8568
8297
|
}
|
|
8569
|
-
documentation() {
|
|
8298
|
+
documentation(context) {
|
|
8299
|
+
const { characters } = this.options;
|
|
8300
|
+
const replacements = characters.map((it) => {
|
|
8301
|
+
return ` - \`${it.pattern}\` - replace with \`${it.replacement}\` (${it.description}).`;
|
|
8302
|
+
});
|
|
8570
8303
|
return {
|
|
8571
|
-
description:
|
|
8572
|
-
|
|
8304
|
+
description: [
|
|
8305
|
+
`The \`${context.pattern}\` character should be replaced with \`${context.replacement}\` character (${context.description}) when used in a telephone number.`,
|
|
8306
|
+
"",
|
|
8307
|
+
"Unless non-breaking characters is used there could be a line break inserted at that character.",
|
|
8308
|
+
"Line breaks make is harder to read and understand the telephone number.",
|
|
8309
|
+
"",
|
|
8310
|
+
"The following characters should be avoided:",
|
|
8311
|
+
"",
|
|
8312
|
+
...replacements
|
|
8313
|
+
].join("\n"),
|
|
8314
|
+
url: "https://html-validate.org/rules/tel-non-breaking.html"
|
|
8573
8315
|
};
|
|
8574
8316
|
}
|
|
8575
8317
|
setup() {
|
|
8576
|
-
this.on("
|
|
8577
|
-
const
|
|
8578
|
-
if (
|
|
8579
|
-
return;
|
|
8580
|
-
}
|
|
8581
|
-
if (node.hasAttribute("integrity")) {
|
|
8318
|
+
this.on("element:ready", this.isRelevant, (event) => {
|
|
8319
|
+
const { target } = event;
|
|
8320
|
+
if (this.isIgnored(target)) {
|
|
8582
8321
|
return;
|
|
8583
8322
|
}
|
|
8584
|
-
this.
|
|
8585
|
-
node,
|
|
8586
|
-
`SRI "integrity" attribute is required on <${node.tagName}> element`,
|
|
8587
|
-
node.location
|
|
8588
|
-
);
|
|
8323
|
+
this.walk(target, target);
|
|
8589
8324
|
});
|
|
8590
8325
|
}
|
|
8591
|
-
|
|
8592
|
-
|
|
8593
|
-
|
|
8594
|
-
needSri(node) {
|
|
8595
|
-
const attr = this.elementSourceAttr(node);
|
|
8596
|
-
if (!attr) {
|
|
8326
|
+
isRelevant(event) {
|
|
8327
|
+
const { target } = event;
|
|
8328
|
+
if (!target.is("a")) {
|
|
8597
8329
|
return false;
|
|
8598
8330
|
}
|
|
8599
|
-
|
|
8331
|
+
const attr = target.getAttribute("href");
|
|
8332
|
+
if (!(attr == null ? void 0 : attr.valueMatches(/^tel:/, false))) {
|
|
8600
8333
|
return false;
|
|
8601
8334
|
}
|
|
8602
|
-
|
|
8603
|
-
|
|
8604
|
-
|
|
8335
|
+
return true;
|
|
8336
|
+
}
|
|
8337
|
+
isIgnoredClass(node) {
|
|
8338
|
+
const { ignoreClasses } = this.options;
|
|
8339
|
+
const { classList } = node;
|
|
8340
|
+
return ignoreClasses.some((it) => classList.contains(it));
|
|
8341
|
+
}
|
|
8342
|
+
isIgnoredStyle(node) {
|
|
8343
|
+
const { ignoreStyle } = this.options;
|
|
8344
|
+
const { style } = node;
|
|
8345
|
+
if (!ignoreStyle) {
|
|
8346
|
+
return false;
|
|
8347
|
+
}
|
|
8348
|
+
if (style["white-space"] === "nowrap" || style["white-space"] === "pre") {
|
|
8349
|
+
return true;
|
|
8605
8350
|
}
|
|
8606
8351
|
return false;
|
|
8607
8352
|
}
|
|
8608
|
-
|
|
8609
|
-
|
|
8610
|
-
|
|
8353
|
+
isIgnored(node) {
|
|
8354
|
+
return this.isIgnoredClass(node) || this.isIgnoredStyle(node);
|
|
8355
|
+
}
|
|
8356
|
+
walk(anchor, node) {
|
|
8357
|
+
for (const child of node.childNodes) {
|
|
8358
|
+
if (isTextNode(child)) {
|
|
8359
|
+
this.detectDisallowed(anchor, child);
|
|
8360
|
+
} else if (isElementNode(child)) {
|
|
8361
|
+
this.walk(anchor, child);
|
|
8362
|
+
}
|
|
8363
|
+
}
|
|
8364
|
+
}
|
|
8365
|
+
detectDisallowed(anchor, node) {
|
|
8366
|
+
const [offset, text] = getText(node);
|
|
8367
|
+
const matches = matchAll(text, this.regex);
|
|
8368
|
+
for (const match of matches) {
|
|
8369
|
+
const detected = match[0];
|
|
8370
|
+
const entry = this.options.characters.find((it) => it.pattern === detected);
|
|
8371
|
+
if (!entry) {
|
|
8372
|
+
throw new Error(`Failed to find entry for "${detected}" when searching text "${text}"`);
|
|
8373
|
+
}
|
|
8374
|
+
const message = `"${detected}" should be replaced with "${entry.replacement}" (${entry.description}) in telephone number`;
|
|
8375
|
+
const begin = offset + match.index;
|
|
8376
|
+
const end = begin + detected.length;
|
|
8377
|
+
const location = sliceLocation(node.location, begin, end);
|
|
8378
|
+
const context = entry;
|
|
8379
|
+
this.report(anchor, message, location, context);
|
|
8380
|
+
}
|
|
8381
|
+
}
|
|
8382
|
+
}
|
|
8383
|
+
|
|
8384
|
+
function hasNonEmptyAttribute(node, key) {
|
|
8385
|
+
const attr = node.getAttribute(key);
|
|
8386
|
+
return Boolean(attr == null ? void 0 : attr.valueMatches(/.+/, true));
|
|
8387
|
+
}
|
|
8388
|
+
function hasDefaultText(node) {
|
|
8389
|
+
if (!node.is("input")) {
|
|
8390
|
+
return false;
|
|
8391
|
+
}
|
|
8392
|
+
if (node.hasAttribute("value")) {
|
|
8393
|
+
return false;
|
|
8394
|
+
}
|
|
8395
|
+
const type = node.getAttribute("type");
|
|
8396
|
+
return Boolean(type == null ? void 0 : type.valueMatches(/submit|reset/, false));
|
|
8397
|
+
}
|
|
8398
|
+
function isNonEmptyText(node) {
|
|
8399
|
+
if (isTextNode(node)) {
|
|
8400
|
+
return node.isDynamic || node.textContent.trim() !== "";
|
|
8401
|
+
} else {
|
|
8402
|
+
return false;
|
|
8403
|
+
}
|
|
8404
|
+
}
|
|
8405
|
+
function haveAccessibleText(node) {
|
|
8406
|
+
if (!inAccessibilityTree(node)) {
|
|
8407
|
+
return false;
|
|
8408
|
+
}
|
|
8409
|
+
const haveText = node.childNodes.some((child) => isNonEmptyText(child));
|
|
8410
|
+
if (haveText) {
|
|
8411
|
+
return true;
|
|
8412
|
+
}
|
|
8413
|
+
if (hasNonEmptyAttribute(node, "aria-label")) {
|
|
8414
|
+
return true;
|
|
8415
|
+
}
|
|
8416
|
+
if (hasNonEmptyAttribute(node, "aria-labelledby")) {
|
|
8417
|
+
return true;
|
|
8418
|
+
}
|
|
8419
|
+
if (node.is("img") && hasNonEmptyAttribute(node, "alt")) {
|
|
8420
|
+
return true;
|
|
8421
|
+
}
|
|
8422
|
+
if (hasDefaultText(node)) {
|
|
8423
|
+
return true;
|
|
8424
|
+
}
|
|
8425
|
+
return node.childElements.some((child) => {
|
|
8426
|
+
return haveAccessibleText(child);
|
|
8427
|
+
});
|
|
8428
|
+
}
|
|
8429
|
+
class TextContent extends Rule {
|
|
8430
|
+
documentation(context) {
|
|
8431
|
+
const doc = {
|
|
8432
|
+
description: `The textual content for this element is not valid.`,
|
|
8433
|
+
url: "https://html-validate.org/rules/text-content.html"
|
|
8434
|
+
};
|
|
8435
|
+
switch (context.textContent) {
|
|
8436
|
+
case TextContent$1.NONE:
|
|
8437
|
+
doc.description = `The \`<${context.tagName}>\` element must not have textual content.`;
|
|
8438
|
+
break;
|
|
8439
|
+
case TextContent$1.REQUIRED:
|
|
8440
|
+
doc.description = `The \`<${context.tagName}>\` element must have textual content.`;
|
|
8441
|
+
break;
|
|
8442
|
+
case TextContent$1.ACCESSIBLE:
|
|
8443
|
+
doc.description = `The \`<${context.tagName}>\` element must have accessible text.`;
|
|
8444
|
+
break;
|
|
8445
|
+
}
|
|
8446
|
+
return doc;
|
|
8447
|
+
}
|
|
8448
|
+
static filter(event) {
|
|
8449
|
+
const { target } = event;
|
|
8450
|
+
if (!target.meta) {
|
|
8451
|
+
return false;
|
|
8452
|
+
}
|
|
8453
|
+
const { textContent } = target.meta;
|
|
8454
|
+
if (!textContent || textContent === TextContent$1.DEFAULT) {
|
|
8455
|
+
return false;
|
|
8456
|
+
}
|
|
8457
|
+
return true;
|
|
8458
|
+
}
|
|
8459
|
+
setup() {
|
|
8460
|
+
this.on("element:ready", TextContent.filter, (event) => {
|
|
8461
|
+
const target = event.target;
|
|
8462
|
+
const { textContent } = target.meta;
|
|
8463
|
+
switch (textContent) {
|
|
8464
|
+
case TextContent$1.NONE:
|
|
8465
|
+
this.validateNone(target);
|
|
8466
|
+
break;
|
|
8467
|
+
case TextContent$1.REQUIRED:
|
|
8468
|
+
this.validateRequired(target);
|
|
8469
|
+
break;
|
|
8470
|
+
case TextContent$1.ACCESSIBLE:
|
|
8471
|
+
this.validateAccessible(target);
|
|
8472
|
+
break;
|
|
8473
|
+
}
|
|
8474
|
+
});
|
|
8475
|
+
}
|
|
8476
|
+
/**
|
|
8477
|
+
* Validate element has empty text (inter-element whitespace is not considered text)
|
|
8478
|
+
*/
|
|
8479
|
+
validateNone(node) {
|
|
8480
|
+
if (classifyNodeText(node) === TextClassification.EMPTY_TEXT) {
|
|
8481
|
+
return;
|
|
8482
|
+
}
|
|
8483
|
+
this.reportError(node, node.meta, `${node.annotatedName} must not have text content`);
|
|
8484
|
+
}
|
|
8485
|
+
/**
|
|
8486
|
+
* Validate element has any text (inter-element whitespace is not considered text)
|
|
8487
|
+
*/
|
|
8488
|
+
validateRequired(node) {
|
|
8489
|
+
if (classifyNodeText(node) !== TextClassification.EMPTY_TEXT) {
|
|
8490
|
+
return;
|
|
8491
|
+
}
|
|
8492
|
+
this.reportError(node, node.meta, `${node.annotatedName} must have text content`);
|
|
8493
|
+
}
|
|
8494
|
+
/**
|
|
8495
|
+
* Validate element has accessible text (either regular text or text only
|
|
8496
|
+
* exposed in accessibility tree via aria-label or similar)
|
|
8497
|
+
*/
|
|
8498
|
+
validateAccessible(node) {
|
|
8499
|
+
if (!inAccessibilityTree(node)) {
|
|
8500
|
+
return;
|
|
8501
|
+
}
|
|
8502
|
+
if (haveAccessibleText(node)) {
|
|
8503
|
+
return;
|
|
8504
|
+
}
|
|
8505
|
+
this.reportError(node, node.meta, `${node.annotatedName} must have accessible text`);
|
|
8611
8506
|
}
|
|
8612
|
-
|
|
8613
|
-
|
|
8614
|
-
|
|
8507
|
+
reportError(node, meta, message) {
|
|
8508
|
+
this.report(node, message, null, {
|
|
8509
|
+
tagName: node.tagName,
|
|
8510
|
+
textContent: meta.textContent
|
|
8615
8511
|
});
|
|
8616
8512
|
}
|
|
8617
8513
|
}
|
|
8618
8514
|
|
|
8619
|
-
|
|
8620
|
-
|
|
8621
|
-
|
|
8622
|
-
|
|
8623
|
-
|
|
8624
|
-
|
|
8515
|
+
const roles = ["complementary", "contentinfo", "form", "banner", "main", "navigation", "region"];
|
|
8516
|
+
const selectors = [
|
|
8517
|
+
"aside",
|
|
8518
|
+
"footer",
|
|
8519
|
+
"form",
|
|
8520
|
+
"header",
|
|
8521
|
+
"main",
|
|
8522
|
+
"nav",
|
|
8523
|
+
"section",
|
|
8524
|
+
...roles.map((it) => `[role="${it}"]`)
|
|
8525
|
+
/* <search> does not (yet?) require a unique name */
|
|
8526
|
+
];
|
|
8527
|
+
function getTextFromReference(document, id) {
|
|
8528
|
+
if (!id || id instanceof DynamicValue) {
|
|
8529
|
+
return id;
|
|
8625
8530
|
}
|
|
8626
|
-
|
|
8627
|
-
|
|
8628
|
-
|
|
8629
|
-
|
|
8630
|
-
|
|
8631
|
-
|
|
8632
|
-
if (node.closed !== NodeClosed.EndTag) {
|
|
8633
|
-
this.report(node, `End tag for <${node.tagName}> must not be omitted`);
|
|
8634
|
-
}
|
|
8635
|
-
});
|
|
8531
|
+
const selector = `#${id}`;
|
|
8532
|
+
const ref = document.querySelector(selector);
|
|
8533
|
+
if (ref) {
|
|
8534
|
+
return ref.textContent;
|
|
8535
|
+
} else {
|
|
8536
|
+
return selector;
|
|
8636
8537
|
}
|
|
8637
8538
|
}
|
|
8638
|
-
|
|
8639
|
-
const
|
|
8640
|
-
|
|
8641
|
-
|
|
8642
|
-
|
|
8643
|
-
|
|
8644
|
-
|
|
8645
|
-
];
|
|
8646
|
-
|
|
8647
|
-
|
|
8539
|
+
function groupBy(values, callback) {
|
|
8540
|
+
const result = {};
|
|
8541
|
+
for (const value of values) {
|
|
8542
|
+
const key = callback(value);
|
|
8543
|
+
if (key in result) {
|
|
8544
|
+
result[key].push(value);
|
|
8545
|
+
} else {
|
|
8546
|
+
result[key] = [value];
|
|
8547
|
+
}
|
|
8548
|
+
}
|
|
8549
|
+
return result;
|
|
8550
|
+
}
|
|
8551
|
+
function getTextEntryFromElement(document, node) {
|
|
8552
|
+
const ariaLabel = node.getAttribute("aria-label");
|
|
8553
|
+
if (ariaLabel) {
|
|
8648
8554
|
return {
|
|
8649
|
-
|
|
8650
|
-
|
|
8555
|
+
node,
|
|
8556
|
+
text: ariaLabel.value,
|
|
8557
|
+
location: ariaLabel.keyLocation
|
|
8651
8558
|
};
|
|
8652
8559
|
}
|
|
8653
|
-
|
|
8654
|
-
|
|
8655
|
-
|
|
8656
|
-
|
|
8657
|
-
|
|
8658
|
-
|
|
8659
|
-
|
|
8660
|
-
|
|
8661
|
-
return;
|
|
8662
|
-
}
|
|
8663
|
-
const value = attr.value ? attr.value.toString() : "";
|
|
8664
|
-
if (!this.isJavascript(value)) {
|
|
8665
|
-
return;
|
|
8666
|
-
}
|
|
8667
|
-
this.report(
|
|
8668
|
-
node,
|
|
8669
|
-
'"type" attribute is unnecessary for javascript resources',
|
|
8670
|
-
attr.keyLocation
|
|
8671
|
-
);
|
|
8672
|
-
});
|
|
8560
|
+
const ariaLabelledby = node.getAttribute("aria-labelledby");
|
|
8561
|
+
if (ariaLabelledby) {
|
|
8562
|
+
const text = getTextFromReference(document, ariaLabelledby.value);
|
|
8563
|
+
return {
|
|
8564
|
+
node,
|
|
8565
|
+
text,
|
|
8566
|
+
location: ariaLabelledby.keyLocation
|
|
8567
|
+
};
|
|
8673
8568
|
}
|
|
8674
|
-
|
|
8675
|
-
|
|
8676
|
-
|
|
8569
|
+
return {
|
|
8570
|
+
node,
|
|
8571
|
+
text: null,
|
|
8572
|
+
location: node.location
|
|
8573
|
+
};
|
|
8574
|
+
}
|
|
8575
|
+
function isExcluded(entry) {
|
|
8576
|
+
const { node, text } = entry;
|
|
8577
|
+
if (text === null) {
|
|
8578
|
+
return !(node.is("form") || node.is("section"));
|
|
8677
8579
|
}
|
|
8580
|
+
return true;
|
|
8678
8581
|
}
|
|
8679
|
-
|
|
8680
|
-
class SvgFocusable extends Rule {
|
|
8582
|
+
class UniqueLandmark extends Rule {
|
|
8681
8583
|
documentation() {
|
|
8682
8584
|
return {
|
|
8683
|
-
description:
|
|
8684
|
-
|
|
8585
|
+
description: [
|
|
8586
|
+
"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.",
|
|
8587
|
+
"For instance, if the document has two `<nav>` elements each of them need an accessible name to be distinguished from each other.",
|
|
8588
|
+
"",
|
|
8589
|
+
"The following elements / roles are considered landmarks:",
|
|
8590
|
+
"",
|
|
8591
|
+
' - `aside` or `[role="complementary"]`',
|
|
8592
|
+
' - `footer` or `[role="contentinfo"]`',
|
|
8593
|
+
' - `form` or `[role="form"]`',
|
|
8594
|
+
' - `header` or `[role="banner"]`',
|
|
8595
|
+
' - `main` or `[role="main"]`',
|
|
8596
|
+
' - `nav` or `[role="navigation"]`',
|
|
8597
|
+
' - `section` or `[role="region"]`',
|
|
8598
|
+
"",
|
|
8599
|
+
"To fix this either:",
|
|
8600
|
+
"",
|
|
8601
|
+
" - Add `aria-label`.",
|
|
8602
|
+
" - Add `aria-labelledby`.",
|
|
8603
|
+
" - Remove one of the landmarks."
|
|
8604
|
+
].join("\n"),
|
|
8605
|
+
url: "https://html-validate.org/rules/unique-landmark.html"
|
|
8685
8606
|
};
|
|
8686
8607
|
}
|
|
8687
8608
|
setup() {
|
|
8688
|
-
this.on("
|
|
8689
|
-
|
|
8690
|
-
|
|
8609
|
+
this.on("dom:ready", (event) => {
|
|
8610
|
+
const { document } = event;
|
|
8611
|
+
const elements = document.querySelectorAll(selectors.join(",")).filter((it) => typeof it.role === "string" && roles.includes(it.role));
|
|
8612
|
+
const grouped = groupBy(elements, (it) => it.role);
|
|
8613
|
+
for (const nodes of Object.values(grouped)) {
|
|
8614
|
+
if (nodes.length <= 1) {
|
|
8615
|
+
continue;
|
|
8616
|
+
}
|
|
8617
|
+
const entries = nodes.map((it) => getTextEntryFromElement(document, it));
|
|
8618
|
+
const filteredEntries = entries.filter(isExcluded);
|
|
8619
|
+
for (const entry of filteredEntries) {
|
|
8620
|
+
if (entry.text instanceof DynamicValue) {
|
|
8621
|
+
continue;
|
|
8622
|
+
}
|
|
8623
|
+
const dup = entries.filter((it) => it.text === entry.text).length > 1;
|
|
8624
|
+
if (!entry.text || dup) {
|
|
8625
|
+
const message = `Landmarks must have a non-empty and unique accessible name (aria-label or aria-labelledby)`;
|
|
8626
|
+
const location = entry.location;
|
|
8627
|
+
this.report({
|
|
8628
|
+
node: entry.node,
|
|
8629
|
+
message,
|
|
8630
|
+
location
|
|
8631
|
+
});
|
|
8632
|
+
}
|
|
8633
|
+
}
|
|
8691
8634
|
}
|
|
8692
8635
|
});
|
|
8693
8636
|
}
|
|
8694
|
-
validate(svg) {
|
|
8695
|
-
if (svg.hasAttribute("focusable")) {
|
|
8696
|
-
return;
|
|
8697
|
-
}
|
|
8698
|
-
this.report(svg, `<${svg.tagName}> is missing required "focusable" attribute`);
|
|
8699
|
-
}
|
|
8700
8637
|
}
|
|
8701
8638
|
|
|
8702
|
-
const defaults$
|
|
8703
|
-
|
|
8704
|
-
|
|
8705
|
-
{ pattern: "-", replacement: "‑", description: "non-breaking hyphen" }
|
|
8706
|
-
],
|
|
8707
|
-
ignoreClasses: [],
|
|
8708
|
-
ignoreStyle: true
|
|
8639
|
+
const defaults$4 = {
|
|
8640
|
+
ignoreCase: false,
|
|
8641
|
+
requireSemicolon: true
|
|
8709
8642
|
};
|
|
8710
|
-
|
|
8711
|
-
|
|
8712
|
-
|
|
8713
|
-
|
|
8714
|
-
const pattern = `(${disallowed})`;
|
|
8715
|
-
return new RegExp(pattern, "g");
|
|
8716
|
-
}
|
|
8717
|
-
function getText(node) {
|
|
8718
|
-
const match = node.textContent.match(/^(\s*)(.*)$/);
|
|
8719
|
-
const [, leading, text] = match;
|
|
8720
|
-
return [leading.length, text.trimEnd()];
|
|
8643
|
+
const regexp$1 = /&(?:[a-z0-9]+|#x?[0-9a-f]+)(;|[^a-z0-9]|$)/gi;
|
|
8644
|
+
const lowercaseEntities = entities$1.map((it) => it.toLowerCase());
|
|
8645
|
+
function isNumerical(entity) {
|
|
8646
|
+
return entity.startsWith("&#");
|
|
8721
8647
|
}
|
|
8722
|
-
function
|
|
8723
|
-
const
|
|
8724
|
-
|
|
8725
|
-
let match;
|
|
8726
|
-
while (match = copy.exec(text)) {
|
|
8727
|
-
matches.push(match);
|
|
8728
|
-
}
|
|
8729
|
-
return matches;
|
|
8648
|
+
function getLocation(location, entity, match) {
|
|
8649
|
+
const index = match.index ?? 0;
|
|
8650
|
+
return sliceLocation(location, index, index + entity.length);
|
|
8730
8651
|
}
|
|
8731
|
-
|
|
8732
|
-
|
|
8733
|
-
|
|
8734
|
-
|
|
8652
|
+
function getDescription(context, options) {
|
|
8653
|
+
const url = "https://html.spec.whatwg.org/multipage/named-characters.html";
|
|
8654
|
+
let message;
|
|
8655
|
+
if (context.terminated) {
|
|
8656
|
+
message = `Unrecognized character reference \`${context.entity}\`.`;
|
|
8657
|
+
} else {
|
|
8658
|
+
message = `Character reference \`${context.entity}\` must be terminated by a semicolon.`;
|
|
8735
8659
|
}
|
|
8736
|
-
|
|
8737
|
-
|
|
8738
|
-
|
|
8739
|
-
|
|
8740
|
-
|
|
8741
|
-
|
|
8742
|
-
|
|
8743
|
-
|
|
8744
|
-
|
|
8745
|
-
|
|
8746
|
-
|
|
8747
|
-
|
|
8748
|
-
|
|
8749
|
-
|
|
8750
|
-
|
|
8751
|
-
|
|
8752
|
-
|
|
8753
|
-
|
|
8754
|
-
|
|
8755
|
-
},
|
|
8756
|
-
ignoreClasses: {
|
|
8757
|
-
type: "array",
|
|
8758
|
-
items: {
|
|
8759
|
-
type: "string"
|
|
8760
|
-
}
|
|
8660
|
+
return [
|
|
8661
|
+
message,
|
|
8662
|
+
`HTML5 defines a set of [valid character references](${url}) but this is not a valid one.`,
|
|
8663
|
+
"",
|
|
8664
|
+
"Ensure that:",
|
|
8665
|
+
"",
|
|
8666
|
+
"1. The character is one of the listed names.",
|
|
8667
|
+
...options.ignoreCase ? [] : ["1. The case is correct (names are case sensitive)."],
|
|
8668
|
+
...options.requireSemicolon ? ["1. The name is terminated with a `;`."] : []
|
|
8669
|
+
].join("\n");
|
|
8670
|
+
}
|
|
8671
|
+
class UnknownCharReference extends Rule {
|
|
8672
|
+
constructor(options) {
|
|
8673
|
+
super({ ...defaults$4, ...options });
|
|
8674
|
+
}
|
|
8675
|
+
static schema() {
|
|
8676
|
+
return {
|
|
8677
|
+
ignoreCase: {
|
|
8678
|
+
type: "boolean"
|
|
8761
8679
|
},
|
|
8762
|
-
|
|
8680
|
+
requireSemicolon: {
|
|
8763
8681
|
type: "boolean"
|
|
8764
8682
|
}
|
|
8765
8683
|
};
|
|
8766
8684
|
}
|
|
8767
8685
|
documentation(context) {
|
|
8768
|
-
const { characters } = this.options;
|
|
8769
|
-
const replacements = characters.map((it) => {
|
|
8770
|
-
return ` - \`${it.pattern}\` - replace with \`${it.replacement}\` (${it.description}).`;
|
|
8771
|
-
});
|
|
8772
8686
|
return {
|
|
8773
|
-
description:
|
|
8774
|
-
|
|
8775
|
-
"",
|
|
8776
|
-
"Unless non-breaking characters is used there could be a line break inserted at that character.",
|
|
8777
|
-
"Line breaks make is harder to read and understand the telephone number.",
|
|
8778
|
-
"",
|
|
8779
|
-
"The following characters should be avoided:",
|
|
8780
|
-
"",
|
|
8781
|
-
...replacements
|
|
8782
|
-
].join("\n"),
|
|
8783
|
-
url: "https://html-validate.org/rules/tel-non-breaking.html"
|
|
8687
|
+
description: getDescription(context, this.options),
|
|
8688
|
+
url: "https://html-validate.org/rules/unrecognized-char-ref.html"
|
|
8784
8689
|
};
|
|
8785
8690
|
}
|
|
8786
8691
|
setup() {
|
|
8787
|
-
this.on("element:ready",
|
|
8788
|
-
const
|
|
8789
|
-
|
|
8692
|
+
this.on("element:ready", (event) => {
|
|
8693
|
+
const node = event.target;
|
|
8694
|
+
for (const child of node.childNodes) {
|
|
8695
|
+
if (child.nodeType !== NodeType.TEXT_NODE) {
|
|
8696
|
+
continue;
|
|
8697
|
+
}
|
|
8698
|
+
this.findCharacterReferences(node, child.textContent, child.location, {
|
|
8699
|
+
isAttribute: false
|
|
8700
|
+
});
|
|
8701
|
+
}
|
|
8702
|
+
});
|
|
8703
|
+
this.on("attr", (event) => {
|
|
8704
|
+
if (!event.value) {
|
|
8790
8705
|
return;
|
|
8791
8706
|
}
|
|
8792
|
-
this.
|
|
8707
|
+
this.findCharacterReferences(event.target, event.value.toString(), event.valueLocation, {
|
|
8708
|
+
isAttribute: true
|
|
8709
|
+
});
|
|
8793
8710
|
});
|
|
8794
8711
|
}
|
|
8795
|
-
|
|
8796
|
-
|
|
8797
|
-
|
|
8798
|
-
|
|
8712
|
+
get entities() {
|
|
8713
|
+
if (this.options.ignoreCase) {
|
|
8714
|
+
return lowercaseEntities;
|
|
8715
|
+
} else {
|
|
8716
|
+
return entities$1;
|
|
8799
8717
|
}
|
|
8800
|
-
|
|
8801
|
-
|
|
8802
|
-
|
|
8718
|
+
}
|
|
8719
|
+
findCharacterReferences(node, text, location, { isAttribute }) {
|
|
8720
|
+
const isQuerystring = isAttribute && text.includes("?");
|
|
8721
|
+
for (const match of this.getMatches(text)) {
|
|
8722
|
+
this.validateCharacterReference(node, location, match, { isQuerystring });
|
|
8803
8723
|
}
|
|
8804
|
-
return true;
|
|
8805
8724
|
}
|
|
8806
|
-
|
|
8807
|
-
const {
|
|
8808
|
-
const {
|
|
8809
|
-
|
|
8725
|
+
validateCharacterReference(node, location, foobar, { isQuerystring }) {
|
|
8726
|
+
const { requireSemicolon } = this.options;
|
|
8727
|
+
const { match, entity, raw, terminated } = foobar;
|
|
8728
|
+
if (isNumerical(entity)) {
|
|
8729
|
+
return;
|
|
8730
|
+
}
|
|
8731
|
+
if (isQuerystring && !terminated) {
|
|
8732
|
+
return;
|
|
8733
|
+
}
|
|
8734
|
+
const found = this.entities.includes(entity);
|
|
8735
|
+
if (found && (terminated || !requireSemicolon)) {
|
|
8736
|
+
return;
|
|
8737
|
+
}
|
|
8738
|
+
if (found && !terminated) {
|
|
8739
|
+
const entityLocation2 = getLocation(location, entity, match);
|
|
8740
|
+
const message2 = `Character reference "{{ entity }}" must be terminated by a semicolon`;
|
|
8741
|
+
const context2 = {
|
|
8742
|
+
entity: raw,
|
|
8743
|
+
terminated: false
|
|
8744
|
+
};
|
|
8745
|
+
this.report(node, message2, entityLocation2, context2);
|
|
8746
|
+
return;
|
|
8747
|
+
}
|
|
8748
|
+
const entityLocation = getLocation(location, entity, match);
|
|
8749
|
+
const message = `Unrecognized character reference "{{ entity }}"`;
|
|
8750
|
+
const context = {
|
|
8751
|
+
entity: raw,
|
|
8752
|
+
terminated: true
|
|
8753
|
+
};
|
|
8754
|
+
this.report(node, message, entityLocation, context);
|
|
8755
|
+
}
|
|
8756
|
+
*getMatches(text) {
|
|
8757
|
+
let match;
|
|
8758
|
+
do {
|
|
8759
|
+
match = regexp$1.exec(text);
|
|
8760
|
+
if (match) {
|
|
8761
|
+
const terminator = match[1];
|
|
8762
|
+
const terminated = terminator === ";";
|
|
8763
|
+
const needSlice = terminator !== ";" && terminator.length > 0;
|
|
8764
|
+
const entity = needSlice ? match[0].slice(0, -1) : match[0];
|
|
8765
|
+
if (this.options.ignoreCase) {
|
|
8766
|
+
yield { match, entity: entity.toLowerCase(), raw: entity, terminated };
|
|
8767
|
+
} else {
|
|
8768
|
+
yield { match, entity, raw: entity, terminated };
|
|
8769
|
+
}
|
|
8770
|
+
}
|
|
8771
|
+
} while (match);
|
|
8772
|
+
}
|
|
8773
|
+
}
|
|
8774
|
+
|
|
8775
|
+
const expectedOrder = ["section", "hint", "contact", "field1", "field2", "webauthn"];
|
|
8776
|
+
const fieldNames1 = [
|
|
8777
|
+
"name",
|
|
8778
|
+
"honorific-prefix",
|
|
8779
|
+
"given-name",
|
|
8780
|
+
"additional-name",
|
|
8781
|
+
"family-name",
|
|
8782
|
+
"honorific-suffix",
|
|
8783
|
+
"nickname",
|
|
8784
|
+
"username",
|
|
8785
|
+
"new-password",
|
|
8786
|
+
"current-password",
|
|
8787
|
+
"one-time-code",
|
|
8788
|
+
"organization-title",
|
|
8789
|
+
"organization",
|
|
8790
|
+
"street-address",
|
|
8791
|
+
"address-line1",
|
|
8792
|
+
"address-line2",
|
|
8793
|
+
"address-line3",
|
|
8794
|
+
"address-level4",
|
|
8795
|
+
"address-level3",
|
|
8796
|
+
"address-level2",
|
|
8797
|
+
"address-level1",
|
|
8798
|
+
"country",
|
|
8799
|
+
"country-name",
|
|
8800
|
+
"postal-code",
|
|
8801
|
+
"cc-name",
|
|
8802
|
+
"cc-given-name",
|
|
8803
|
+
"cc-additional-name",
|
|
8804
|
+
"cc-family-name",
|
|
8805
|
+
"cc-number",
|
|
8806
|
+
"cc-exp",
|
|
8807
|
+
"cc-exp-month",
|
|
8808
|
+
"cc-exp-year",
|
|
8809
|
+
"cc-csc",
|
|
8810
|
+
"cc-type",
|
|
8811
|
+
"transaction-currency",
|
|
8812
|
+
"transaction-amount",
|
|
8813
|
+
"language",
|
|
8814
|
+
"bday",
|
|
8815
|
+
"bday-day",
|
|
8816
|
+
"bday-month",
|
|
8817
|
+
"bday-year",
|
|
8818
|
+
"sex",
|
|
8819
|
+
"url",
|
|
8820
|
+
"photo"
|
|
8821
|
+
];
|
|
8822
|
+
const fieldNames2 = [
|
|
8823
|
+
"tel",
|
|
8824
|
+
"tel-country-code",
|
|
8825
|
+
"tel-national",
|
|
8826
|
+
"tel-area-code",
|
|
8827
|
+
"tel-local",
|
|
8828
|
+
"tel-local-prefix",
|
|
8829
|
+
"tel-local-suffix",
|
|
8830
|
+
"tel-extension",
|
|
8831
|
+
"email",
|
|
8832
|
+
"impp"
|
|
8833
|
+
];
|
|
8834
|
+
const fieldNameGroup = {
|
|
8835
|
+
name: "text",
|
|
8836
|
+
"honorific-prefix": "text",
|
|
8837
|
+
"given-name": "text",
|
|
8838
|
+
"additional-name": "text",
|
|
8839
|
+
"family-name": "text",
|
|
8840
|
+
"honorific-suffix": "text",
|
|
8841
|
+
nickname: "text",
|
|
8842
|
+
username: "username",
|
|
8843
|
+
"new-password": "password",
|
|
8844
|
+
"current-password": "password",
|
|
8845
|
+
"one-time-code": "password",
|
|
8846
|
+
"organization-title": "text",
|
|
8847
|
+
organization: "text",
|
|
8848
|
+
"street-address": "multiline",
|
|
8849
|
+
"address-line1": "text",
|
|
8850
|
+
"address-line2": "text",
|
|
8851
|
+
"address-line3": "text",
|
|
8852
|
+
"address-level4": "text",
|
|
8853
|
+
"address-level3": "text",
|
|
8854
|
+
"address-level2": "text",
|
|
8855
|
+
"address-level1": "text",
|
|
8856
|
+
country: "text",
|
|
8857
|
+
"country-name": "text",
|
|
8858
|
+
"postal-code": "text",
|
|
8859
|
+
"cc-name": "text",
|
|
8860
|
+
"cc-given-name": "text",
|
|
8861
|
+
"cc-additional-name": "text",
|
|
8862
|
+
"cc-family-name": "text",
|
|
8863
|
+
"cc-number": "text",
|
|
8864
|
+
"cc-exp": "month",
|
|
8865
|
+
"cc-exp-month": "numeric",
|
|
8866
|
+
"cc-exp-year": "numeric",
|
|
8867
|
+
"cc-csc": "text",
|
|
8868
|
+
"cc-type": "text",
|
|
8869
|
+
"transaction-currency": "text",
|
|
8870
|
+
"transaction-amount": "numeric",
|
|
8871
|
+
language: "text",
|
|
8872
|
+
bday: "date",
|
|
8873
|
+
"bday-day": "numeric",
|
|
8874
|
+
"bday-month": "numeric",
|
|
8875
|
+
"bday-year": "numeric",
|
|
8876
|
+
sex: "text",
|
|
8877
|
+
url: "url",
|
|
8878
|
+
photo: "url",
|
|
8879
|
+
tel: "tel",
|
|
8880
|
+
"tel-country-code": "text",
|
|
8881
|
+
"tel-national": "text",
|
|
8882
|
+
"tel-area-code": "text",
|
|
8883
|
+
"tel-local": "text",
|
|
8884
|
+
"tel-local-prefix": "text",
|
|
8885
|
+
"tel-local-suffix": "text",
|
|
8886
|
+
"tel-extension": "text",
|
|
8887
|
+
email: "username",
|
|
8888
|
+
impp: "url"
|
|
8889
|
+
};
|
|
8890
|
+
const disallowedInputTypes = ["checkbox", "radio", "file", "submit", "image", "reset", "button"];
|
|
8891
|
+
function matchSection(token) {
|
|
8892
|
+
return token.startsWith("section-");
|
|
8893
|
+
}
|
|
8894
|
+
function matchHint(token) {
|
|
8895
|
+
return token === "shipping" || token === "billing";
|
|
8896
|
+
}
|
|
8897
|
+
function matchFieldNames1(token) {
|
|
8898
|
+
return fieldNames1.includes(token);
|
|
8899
|
+
}
|
|
8900
|
+
function matchContact(token) {
|
|
8901
|
+
const haystack = ["home", "work", "mobile", "fax", "pager"];
|
|
8902
|
+
return haystack.includes(token);
|
|
8903
|
+
}
|
|
8904
|
+
function matchFieldNames2(token) {
|
|
8905
|
+
return fieldNames2.includes(token);
|
|
8906
|
+
}
|
|
8907
|
+
function matchWebauthn(token) {
|
|
8908
|
+
return token === "webauthn";
|
|
8909
|
+
}
|
|
8910
|
+
function matchToken(token) {
|
|
8911
|
+
if (matchSection(token)) {
|
|
8912
|
+
return "section";
|
|
8913
|
+
}
|
|
8914
|
+
if (matchHint(token)) {
|
|
8915
|
+
return "hint";
|
|
8810
8916
|
}
|
|
8811
|
-
|
|
8812
|
-
|
|
8813
|
-
const { style } = node;
|
|
8814
|
-
if (!ignoreStyle) {
|
|
8815
|
-
return false;
|
|
8816
|
-
}
|
|
8817
|
-
if (style["white-space"] === "nowrap" || style["white-space"] === "pre") {
|
|
8818
|
-
return true;
|
|
8819
|
-
}
|
|
8820
|
-
return false;
|
|
8917
|
+
if (matchFieldNames1(token)) {
|
|
8918
|
+
return "field1";
|
|
8821
8919
|
}
|
|
8822
|
-
|
|
8823
|
-
return
|
|
8920
|
+
if (matchFieldNames2(token)) {
|
|
8921
|
+
return "field2";
|
|
8824
8922
|
}
|
|
8825
|
-
|
|
8826
|
-
|
|
8827
|
-
if (isTextNode(child)) {
|
|
8828
|
-
this.detectDisallowed(anchor, child);
|
|
8829
|
-
} else if (isElementNode(child)) {
|
|
8830
|
-
this.walk(anchor, child);
|
|
8831
|
-
}
|
|
8832
|
-
}
|
|
8923
|
+
if (matchContact(token)) {
|
|
8924
|
+
return "contact";
|
|
8833
8925
|
}
|
|
8834
|
-
|
|
8835
|
-
|
|
8836
|
-
const matches = matchAll(text, this.regex);
|
|
8837
|
-
for (const match of matches) {
|
|
8838
|
-
const detected = match[0];
|
|
8839
|
-
const entry = this.options.characters.find((it) => it.pattern === detected);
|
|
8840
|
-
if (!entry) {
|
|
8841
|
-
throw new Error(`Failed to find entry for "${detected}" when searching text "${text}"`);
|
|
8842
|
-
}
|
|
8843
|
-
const message = `"${detected}" should be replaced with "${entry.replacement}" (${entry.description}) in telephone number`;
|
|
8844
|
-
const begin = offset + match.index;
|
|
8845
|
-
const end = begin + detected.length;
|
|
8846
|
-
const location = sliceLocation(node.location, begin, end);
|
|
8847
|
-
const context = entry;
|
|
8848
|
-
this.report(anchor, message, location, context);
|
|
8849
|
-
}
|
|
8926
|
+
if (matchWebauthn(token)) {
|
|
8927
|
+
return "webauthn";
|
|
8850
8928
|
}
|
|
8929
|
+
return null;
|
|
8851
8930
|
}
|
|
8852
|
-
|
|
8853
|
-
|
|
8854
|
-
|
|
8855
|
-
|
|
8931
|
+
function getControlGroups(type) {
|
|
8932
|
+
const allGroups = [
|
|
8933
|
+
"text",
|
|
8934
|
+
"multiline",
|
|
8935
|
+
"password",
|
|
8936
|
+
"url",
|
|
8937
|
+
"username",
|
|
8938
|
+
"tel",
|
|
8939
|
+
"numeric",
|
|
8940
|
+
"month",
|
|
8941
|
+
"date"
|
|
8942
|
+
];
|
|
8943
|
+
const mapping = {
|
|
8944
|
+
hidden: allGroups,
|
|
8945
|
+
text: allGroups.filter((it) => it !== "multiline"),
|
|
8946
|
+
search: allGroups.filter((it) => it !== "multiline"),
|
|
8947
|
+
password: ["password"],
|
|
8948
|
+
url: ["url"],
|
|
8949
|
+
email: ["username"],
|
|
8950
|
+
tel: ["tel"],
|
|
8951
|
+
number: ["numeric"],
|
|
8952
|
+
month: ["month"],
|
|
8953
|
+
date: ["date"]
|
|
8954
|
+
};
|
|
8955
|
+
return mapping[type] ?? [];
|
|
8856
8956
|
}
|
|
8857
|
-
function
|
|
8957
|
+
function isDisallowedType(node, type) {
|
|
8858
8958
|
if (!node.is("input")) {
|
|
8859
8959
|
return false;
|
|
8860
8960
|
}
|
|
8861
|
-
|
|
8862
|
-
return false;
|
|
8863
|
-
}
|
|
8864
|
-
const type = node.getAttribute("type");
|
|
8865
|
-
return Boolean(type == null ? void 0 : type.valueMatches(/submit|reset/, false));
|
|
8961
|
+
return disallowedInputTypes.includes(type);
|
|
8866
8962
|
}
|
|
8867
|
-
function
|
|
8868
|
-
|
|
8869
|
-
|
|
8870
|
-
|
|
8871
|
-
|
|
8963
|
+
function getTerminalMessage(context) {
|
|
8964
|
+
switch (context.msg) {
|
|
8965
|
+
case 0 /* InvalidAttribute */:
|
|
8966
|
+
return "autocomplete attribute cannot be used on {{ what }}";
|
|
8967
|
+
case 1 /* InvalidValue */:
|
|
8968
|
+
return '"{{ value }}" cannot be used on {{ what }}';
|
|
8969
|
+
case 2 /* InvalidOrder */:
|
|
8970
|
+
return '"{{ second }}" must appear before "{{ first }}"';
|
|
8971
|
+
case 3 /* InvalidToken */:
|
|
8972
|
+
return '"{{ token }}" is not a valid autocomplete token or field name';
|
|
8973
|
+
case 4 /* InvalidCombination */:
|
|
8974
|
+
return '"{{ second }}" cannot be combined with "{{ first }}"';
|
|
8975
|
+
case 5 /* MissingField */:
|
|
8976
|
+
return "autocomplete attribute is missing field name";
|
|
8872
8977
|
}
|
|
8873
8978
|
}
|
|
8874
|
-
function
|
|
8875
|
-
|
|
8876
|
-
|
|
8877
|
-
|
|
8878
|
-
|
|
8879
|
-
|
|
8880
|
-
|
|
8881
|
-
|
|
8882
|
-
|
|
8883
|
-
|
|
8884
|
-
|
|
8885
|
-
|
|
8886
|
-
|
|
8887
|
-
|
|
8888
|
-
|
|
8889
|
-
|
|
8890
|
-
|
|
8891
|
-
|
|
8892
|
-
|
|
8979
|
+
function getMarkdownMessage(context) {
|
|
8980
|
+
switch (context.msg) {
|
|
8981
|
+
case 0 /* InvalidAttribute */:
|
|
8982
|
+
return [
|
|
8983
|
+
`\`autocomplete\` attribute cannot be used on \`${context.what}\``,
|
|
8984
|
+
"",
|
|
8985
|
+
"The following input types cannot use the `autocomplete` attribute:",
|
|
8986
|
+
"",
|
|
8987
|
+
...disallowedInputTypes.map((it) => `- \`${it}\``)
|
|
8988
|
+
].join("\n");
|
|
8989
|
+
case 1 /* InvalidValue */: {
|
|
8990
|
+
const message = `\`"${context.value}"\` cannot be used on \`${context.what}\``;
|
|
8991
|
+
if (context.type === "form") {
|
|
8992
|
+
return [
|
|
8993
|
+
message,
|
|
8994
|
+
"",
|
|
8995
|
+
'The `<form>` element can only use the values `"on"` and `"off"`.'
|
|
8996
|
+
].join("\n");
|
|
8997
|
+
}
|
|
8998
|
+
if (context.type === "hidden") {
|
|
8999
|
+
return [
|
|
9000
|
+
message,
|
|
9001
|
+
"",
|
|
9002
|
+
'`<input type="hidden">` cannot use the values `"on"` and `"off"`.'
|
|
9003
|
+
].join("\n");
|
|
9004
|
+
}
|
|
9005
|
+
const controlGroups = getControlGroups(context.type);
|
|
9006
|
+
const currentGroup = fieldNameGroup[context.value];
|
|
9007
|
+
return [
|
|
9008
|
+
message,
|
|
9009
|
+
"",
|
|
9010
|
+
`\`${context.what}\` allows autocomplete fields from the following group${controlGroups.length > 1 ? "s" : ""}:`,
|
|
9011
|
+
"",
|
|
9012
|
+
...controlGroups.map((it) => `- ${it}`),
|
|
9013
|
+
"",
|
|
9014
|
+
`The field \`"${context.value}"\` belongs to the group /${currentGroup}/ which cannot be used with this input type.`
|
|
9015
|
+
].join("\n");
|
|
9016
|
+
}
|
|
9017
|
+
case 2 /* InvalidOrder */:
|
|
9018
|
+
return [
|
|
9019
|
+
`\`"${context.second}"\` must appear before \`"${context.first}"\``,
|
|
9020
|
+
"",
|
|
9021
|
+
"The autocomplete tokens must appear in the following order:",
|
|
9022
|
+
"",
|
|
9023
|
+
"- Optional section name (`section-` prefix).",
|
|
9024
|
+
"- Optional `shipping` or `billing` token.",
|
|
9025
|
+
"- Optional `home`, `work`, `mobile`, `fax` or `pager` token (for fields supporting it).",
|
|
9026
|
+
"- Field name",
|
|
9027
|
+
"- Optional `webauthn` token."
|
|
9028
|
+
].join("\n");
|
|
9029
|
+
case 3 /* InvalidToken */:
|
|
9030
|
+
return `\`"${context.token}"\` is not a valid autocomplete token or field name`;
|
|
9031
|
+
case 4 /* InvalidCombination */:
|
|
9032
|
+
return `\`"${context.second}"\` cannot be combined with \`"${context.first}"\``;
|
|
9033
|
+
case 5 /* MissingField */:
|
|
9034
|
+
return "Autocomplete attribute is missing field name";
|
|
8893
9035
|
}
|
|
8894
|
-
return node.childElements.some((child) => {
|
|
8895
|
-
return haveAccessibleText(child);
|
|
8896
|
-
});
|
|
8897
9036
|
}
|
|
8898
|
-
class
|
|
9037
|
+
class ValidAutocomplete extends Rule {
|
|
8899
9038
|
documentation(context) {
|
|
8900
|
-
|
|
8901
|
-
description:
|
|
8902
|
-
url: "https://html-validate.org/rules/
|
|
9039
|
+
return {
|
|
9040
|
+
description: getMarkdownMessage(context),
|
|
9041
|
+
url: "https://html-validate.org/rules/valid-autocomplete.html"
|
|
8903
9042
|
};
|
|
8904
|
-
switch (context.textContent) {
|
|
8905
|
-
case TextContent$1.NONE:
|
|
8906
|
-
doc.description = `The \`<${context.tagName}>\` element must not have textual content.`;
|
|
8907
|
-
break;
|
|
8908
|
-
case TextContent$1.REQUIRED:
|
|
8909
|
-
doc.description = `The \`<${context.tagName}>\` element must have textual content.`;
|
|
8910
|
-
break;
|
|
8911
|
-
case TextContent$1.ACCESSIBLE:
|
|
8912
|
-
doc.description = `The \`<${context.tagName}>\` element must have accessible text.`;
|
|
8913
|
-
break;
|
|
8914
|
-
}
|
|
8915
|
-
return doc;
|
|
8916
|
-
}
|
|
8917
|
-
static filter(event) {
|
|
8918
|
-
const { target } = event;
|
|
8919
|
-
if (!target.meta) {
|
|
8920
|
-
return false;
|
|
8921
|
-
}
|
|
8922
|
-
const { textContent } = target.meta;
|
|
8923
|
-
if (!textContent || textContent === TextContent$1.DEFAULT) {
|
|
8924
|
-
return false;
|
|
8925
|
-
}
|
|
8926
|
-
return true;
|
|
8927
9043
|
}
|
|
8928
9044
|
setup() {
|
|
8929
|
-
this.on("
|
|
8930
|
-
const
|
|
8931
|
-
const
|
|
8932
|
-
|
|
8933
|
-
|
|
8934
|
-
|
|
8935
|
-
|
|
8936
|
-
|
|
8937
|
-
|
|
8938
|
-
|
|
8939
|
-
|
|
8940
|
-
|
|
8941
|
-
|
|
9045
|
+
this.on("dom:ready", (event) => {
|
|
9046
|
+
const { document } = event;
|
|
9047
|
+
const elements = document.querySelectorAll("[autocomplete]");
|
|
9048
|
+
for (const element of elements) {
|
|
9049
|
+
const autocomplete = element.getAttribute("autocomplete");
|
|
9050
|
+
if (autocomplete.value === null || autocomplete.value instanceof DynamicValue) {
|
|
9051
|
+
continue;
|
|
9052
|
+
}
|
|
9053
|
+
const location = autocomplete.valueLocation;
|
|
9054
|
+
const value = autocomplete.value.toLowerCase();
|
|
9055
|
+
const tokens = new DOMTokenList(value, location);
|
|
9056
|
+
if (tokens.length === 0) {
|
|
9057
|
+
continue;
|
|
9058
|
+
}
|
|
9059
|
+
this.validate(element, value, tokens, autocomplete.keyLocation, location);
|
|
8942
9060
|
}
|
|
8943
9061
|
});
|
|
8944
9062
|
}
|
|
8945
|
-
|
|
8946
|
-
|
|
8947
|
-
|
|
8948
|
-
|
|
8949
|
-
|
|
8950
|
-
|
|
9063
|
+
validate(node, value, tokens, keyLocation, valueLocation) {
|
|
9064
|
+
switch (node.tagName) {
|
|
9065
|
+
case "form":
|
|
9066
|
+
this.validateFormAutocomplete(node, value, valueLocation);
|
|
9067
|
+
break;
|
|
9068
|
+
case "input":
|
|
9069
|
+
case "textarea":
|
|
9070
|
+
case "select":
|
|
9071
|
+
this.validateControlAutocomplete(node, tokens, keyLocation);
|
|
9072
|
+
break;
|
|
8951
9073
|
}
|
|
8952
|
-
this.reportError(node, node.meta, `${node.annotatedName} must not have text content`);
|
|
8953
9074
|
}
|
|
8954
|
-
|
|
8955
|
-
|
|
8956
|
-
|
|
8957
|
-
|
|
8958
|
-
|
|
9075
|
+
validateControlAutocomplete(node, tokens, keyLocation) {
|
|
9076
|
+
const type = node.getAttributeValue("type") ?? "text";
|
|
9077
|
+
const mantle = type !== "hidden" ? "expectation" : "anchor";
|
|
9078
|
+
if (isDisallowedType(node, type)) {
|
|
9079
|
+
const context = {
|
|
9080
|
+
msg: 0 /* InvalidAttribute */,
|
|
9081
|
+
what: `<input type="${type}">`
|
|
9082
|
+
};
|
|
9083
|
+
this.report({
|
|
9084
|
+
node,
|
|
9085
|
+
message: getTerminalMessage(context),
|
|
9086
|
+
location: keyLocation,
|
|
9087
|
+
context
|
|
9088
|
+
});
|
|
8959
9089
|
return;
|
|
8960
9090
|
}
|
|
8961
|
-
|
|
8962
|
-
|
|
8963
|
-
/**
|
|
8964
|
-
* Validate element has accessible text (either regular text or text only
|
|
8965
|
-
* exposed in accessibility tree via aria-label or similar)
|
|
8966
|
-
*/
|
|
8967
|
-
validateAccessible(node) {
|
|
8968
|
-
if (!inAccessibilityTree(node)) {
|
|
9091
|
+
if (tokens.includes("on") || tokens.includes("off")) {
|
|
9092
|
+
this.validateOnOff(node, mantle, tokens);
|
|
8969
9093
|
return;
|
|
8970
9094
|
}
|
|
8971
|
-
|
|
9095
|
+
this.validateTokens(node, tokens, keyLocation);
|
|
9096
|
+
}
|
|
9097
|
+
validateFormAutocomplete(node, value, location) {
|
|
9098
|
+
const trimmed = value.trim();
|
|
9099
|
+
if (["on", "off"].includes(trimmed)) {
|
|
8972
9100
|
return;
|
|
8973
9101
|
}
|
|
8974
|
-
|
|
8975
|
-
|
|
8976
|
-
|
|
8977
|
-
|
|
8978
|
-
|
|
8979
|
-
|
|
9102
|
+
const context = {
|
|
9103
|
+
msg: 1 /* InvalidValue */,
|
|
9104
|
+
type: "form",
|
|
9105
|
+
value: trimmed,
|
|
9106
|
+
what: "<form>"
|
|
9107
|
+
};
|
|
9108
|
+
this.report({
|
|
9109
|
+
node,
|
|
9110
|
+
message: getTerminalMessage(context),
|
|
9111
|
+
location,
|
|
9112
|
+
context
|
|
8980
9113
|
});
|
|
8981
9114
|
}
|
|
8982
|
-
|
|
8983
|
-
|
|
8984
|
-
const
|
|
8985
|
-
const
|
|
8986
|
-
|
|
8987
|
-
|
|
8988
|
-
|
|
8989
|
-
|
|
8990
|
-
|
|
8991
|
-
|
|
8992
|
-
|
|
8993
|
-
|
|
8994
|
-
|
|
8995
|
-
|
|
8996
|
-
|
|
8997
|
-
|
|
8998
|
-
|
|
8999
|
-
|
|
9000
|
-
|
|
9001
|
-
|
|
9002
|
-
|
|
9003
|
-
|
|
9004
|
-
|
|
9005
|
-
|
|
9006
|
-
|
|
9007
|
-
|
|
9008
|
-
|
|
9009
|
-
|
|
9010
|
-
|
|
9011
|
-
|
|
9012
|
-
|
|
9013
|
-
|
|
9014
|
-
|
|
9015
|
-
|
|
9115
|
+
validateOnOff(node, mantle, tokens) {
|
|
9116
|
+
const index = tokens.findIndex((it) => it === "on" || it === "off");
|
|
9117
|
+
const value = tokens.item(index);
|
|
9118
|
+
const location = tokens.location(index);
|
|
9119
|
+
if (tokens.length > 1) {
|
|
9120
|
+
const context = {
|
|
9121
|
+
msg: 4 /* InvalidCombination */,
|
|
9122
|
+
/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
|
|
9123
|
+
first: tokens.item(index > 0 ? 0 : 1),
|
|
9124
|
+
second: value
|
|
9125
|
+
};
|
|
9126
|
+
this.report({
|
|
9127
|
+
node,
|
|
9128
|
+
message: getTerminalMessage(context),
|
|
9129
|
+
location,
|
|
9130
|
+
context
|
|
9131
|
+
});
|
|
9132
|
+
}
|
|
9133
|
+
switch (mantle) {
|
|
9134
|
+
case "expectation":
|
|
9135
|
+
return;
|
|
9136
|
+
case "anchor": {
|
|
9137
|
+
const context = {
|
|
9138
|
+
msg: 1 /* InvalidValue */,
|
|
9139
|
+
type: "hidden",
|
|
9140
|
+
value,
|
|
9141
|
+
what: `<input type="hidden">`
|
|
9142
|
+
};
|
|
9143
|
+
this.report({
|
|
9144
|
+
node,
|
|
9145
|
+
message: getTerminalMessage(context),
|
|
9146
|
+
location: tokens.location(0),
|
|
9147
|
+
context
|
|
9148
|
+
});
|
|
9149
|
+
}
|
|
9016
9150
|
}
|
|
9017
9151
|
}
|
|
9018
|
-
|
|
9019
|
-
|
|
9020
|
-
|
|
9021
|
-
|
|
9022
|
-
|
|
9023
|
-
|
|
9024
|
-
|
|
9025
|
-
|
|
9026
|
-
|
|
9027
|
-
|
|
9028
|
-
|
|
9029
|
-
|
|
9030
|
-
|
|
9031
|
-
|
|
9032
|
-
|
|
9033
|
-
|
|
9034
|
-
|
|
9035
|
-
|
|
9036
|
-
|
|
9152
|
+
validateTokens(node, tokens, keyLocation) {
|
|
9153
|
+
const order = [];
|
|
9154
|
+
for (const { item, location } of tokens.iterator()) {
|
|
9155
|
+
const tokenType = matchToken(item);
|
|
9156
|
+
if (tokenType) {
|
|
9157
|
+
order.push(tokenType);
|
|
9158
|
+
} else {
|
|
9159
|
+
const context = {
|
|
9160
|
+
msg: 3 /* InvalidToken */,
|
|
9161
|
+
token: item
|
|
9162
|
+
};
|
|
9163
|
+
this.report({
|
|
9164
|
+
node,
|
|
9165
|
+
message: getTerminalMessage(context),
|
|
9166
|
+
location,
|
|
9167
|
+
context
|
|
9168
|
+
});
|
|
9169
|
+
return;
|
|
9170
|
+
}
|
|
9171
|
+
}
|
|
9172
|
+
const fieldTokens = order.map((it) => it === "field1" || it === "field2");
|
|
9173
|
+
this.validateFieldPresence(node, tokens, fieldTokens, keyLocation);
|
|
9174
|
+
this.validateContact(node, tokens, order);
|
|
9175
|
+
this.validateOrder(node, tokens, order);
|
|
9176
|
+
this.validateControlGroup(node, tokens, fieldTokens);
|
|
9037
9177
|
}
|
|
9038
|
-
|
|
9039
|
-
|
|
9040
|
-
|
|
9041
|
-
|
|
9042
|
-
|
|
9043
|
-
|
|
9044
|
-
|
|
9045
|
-
|
|
9046
|
-
|
|
9047
|
-
|
|
9178
|
+
/**
|
|
9179
|
+
* Ensure that exactly one field name is present from the two field lists.
|
|
9180
|
+
*/
|
|
9181
|
+
validateFieldPresence(node, tokens, fieldTokens, keyLocation) {
|
|
9182
|
+
const numFields = fieldTokens.filter(Boolean).length;
|
|
9183
|
+
if (numFields === 0) {
|
|
9184
|
+
const context = {
|
|
9185
|
+
msg: 5 /* MissingField */
|
|
9186
|
+
};
|
|
9187
|
+
this.report({
|
|
9188
|
+
node,
|
|
9189
|
+
message: getTerminalMessage(context),
|
|
9190
|
+
location: keyLocation,
|
|
9191
|
+
context
|
|
9192
|
+
});
|
|
9193
|
+
} else if (numFields > 1) {
|
|
9194
|
+
const a = fieldTokens.indexOf(true);
|
|
9195
|
+
const b = fieldTokens.lastIndexOf(true);
|
|
9196
|
+
const context = {
|
|
9197
|
+
msg: 4 /* InvalidCombination */,
|
|
9198
|
+
/* eslint-disable @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
|
|
9199
|
+
first: tokens.item(a),
|
|
9200
|
+
second: tokens.item(b)
|
|
9201
|
+
/* eslint-enable @typescript-eslint/no-non-null-assertion */
|
|
9202
|
+
};
|
|
9203
|
+
this.report({
|
|
9204
|
+
node,
|
|
9205
|
+
message: getTerminalMessage(context),
|
|
9206
|
+
location: tokens.location(b),
|
|
9207
|
+
context
|
|
9208
|
+
});
|
|
9209
|
+
}
|
|
9048
9210
|
}
|
|
9049
|
-
|
|
9050
|
-
|
|
9051
|
-
|
|
9052
|
-
|
|
9053
|
-
|
|
9054
|
-
|
|
9055
|
-
|
|
9056
|
-
|
|
9057
|
-
|
|
9058
|
-
|
|
9059
|
-
|
|
9060
|
-
|
|
9061
|
-
|
|
9062
|
-
|
|
9063
|
-
|
|
9064
|
-
|
|
9065
|
-
|
|
9066
|
-
|
|
9067
|
-
|
|
9068
|
-
|
|
9069
|
-
|
|
9070
|
-
" - Add `aria-label`.",
|
|
9071
|
-
" - Add `aria-labelledby`.",
|
|
9072
|
-
" - Remove one of the landmarks."
|
|
9073
|
-
].join("\n"),
|
|
9074
|
-
url: "https://html-validate.org/rules/unique-landmark.html"
|
|
9075
|
-
};
|
|
9211
|
+
/**
|
|
9212
|
+
* Ensure contact token is only used with field names from the second list.
|
|
9213
|
+
*/
|
|
9214
|
+
validateContact(node, tokens, order) {
|
|
9215
|
+
if (order.includes("contact") && order.includes("field1")) {
|
|
9216
|
+
const a = order.indexOf("field1");
|
|
9217
|
+
const b = order.indexOf("contact");
|
|
9218
|
+
const context = {
|
|
9219
|
+
msg: 4 /* InvalidCombination */,
|
|
9220
|
+
/* eslint-disable @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
|
|
9221
|
+
first: tokens.item(a),
|
|
9222
|
+
second: tokens.item(b)
|
|
9223
|
+
/* eslint-enable @typescript-eslint/no-non-null-assertion */
|
|
9224
|
+
};
|
|
9225
|
+
this.report({
|
|
9226
|
+
node,
|
|
9227
|
+
message: getTerminalMessage(context),
|
|
9228
|
+
location: tokens.location(b),
|
|
9229
|
+
context
|
|
9230
|
+
});
|
|
9231
|
+
}
|
|
9076
9232
|
}
|
|
9077
|
-
|
|
9078
|
-
|
|
9079
|
-
|
|
9080
|
-
|
|
9081
|
-
|
|
9082
|
-
|
|
9083
|
-
|
|
9084
|
-
|
|
9085
|
-
|
|
9086
|
-
|
|
9087
|
-
|
|
9088
|
-
|
|
9089
|
-
|
|
9090
|
-
|
|
9091
|
-
|
|
9092
|
-
|
|
9093
|
-
|
|
9094
|
-
const message = `Landmarks must have a non-empty and unique accessible name (aria-label or aria-labelledby)`;
|
|
9095
|
-
const location = entry.location;
|
|
9096
|
-
this.report({
|
|
9097
|
-
node: entry.node,
|
|
9098
|
-
message,
|
|
9099
|
-
location
|
|
9100
|
-
});
|
|
9101
|
-
}
|
|
9102
|
-
}
|
|
9233
|
+
validateOrder(node, tokens, order) {
|
|
9234
|
+
const indicies = order.map((it) => expectedOrder.indexOf(it));
|
|
9235
|
+
for (let i = 0; i < indicies.length - 1; i++) {
|
|
9236
|
+
if (indicies[0] > indicies[i + 1]) {
|
|
9237
|
+
const context = {
|
|
9238
|
+
msg: 2 /* InvalidOrder */,
|
|
9239
|
+
/* eslint-disable @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
|
|
9240
|
+
first: tokens.item(i),
|
|
9241
|
+
second: tokens.item(i + 1)
|
|
9242
|
+
/* eslint-enable @typescript-eslint/no-non-null-assertion */
|
|
9243
|
+
};
|
|
9244
|
+
this.report({
|
|
9245
|
+
node,
|
|
9246
|
+
message: getTerminalMessage(context),
|
|
9247
|
+
location: tokens.location(i + 1),
|
|
9248
|
+
context
|
|
9249
|
+
});
|
|
9103
9250
|
}
|
|
9104
|
-
}
|
|
9251
|
+
}
|
|
9252
|
+
}
|
|
9253
|
+
validateControlGroup(node, tokens, fieldTokens) {
|
|
9254
|
+
const numFields = fieldTokens.filter(Boolean).length;
|
|
9255
|
+
if (numFields === 0) {
|
|
9256
|
+
return;
|
|
9257
|
+
}
|
|
9258
|
+
if (!node.is("input")) {
|
|
9259
|
+
return;
|
|
9260
|
+
}
|
|
9261
|
+
const attr = node.getAttribute("type");
|
|
9262
|
+
const type = (attr == null ? void 0 : attr.value) ?? "text";
|
|
9263
|
+
if (type instanceof DynamicValue) {
|
|
9264
|
+
return;
|
|
9265
|
+
}
|
|
9266
|
+
const controlGroups = getControlGroups(type);
|
|
9267
|
+
const fieldIndex = fieldTokens.indexOf(true);
|
|
9268
|
+
const fieldToken = tokens.item(fieldIndex);
|
|
9269
|
+
const fieldGroup = fieldNameGroup[fieldToken];
|
|
9270
|
+
if (!controlGroups.includes(fieldGroup)) {
|
|
9271
|
+
const context = {
|
|
9272
|
+
msg: 1 /* InvalidValue */,
|
|
9273
|
+
type,
|
|
9274
|
+
value: fieldToken,
|
|
9275
|
+
what: `<input type="${type}">`
|
|
9276
|
+
};
|
|
9277
|
+
this.report({
|
|
9278
|
+
node,
|
|
9279
|
+
message: getTerminalMessage(context),
|
|
9280
|
+
location: tokens.location(fieldIndex),
|
|
9281
|
+
context
|
|
9282
|
+
});
|
|
9283
|
+
}
|
|
9105
9284
|
}
|
|
9106
9285
|
}
|
|
9107
9286
|
|
|
9108
|
-
const defaults$
|
|
9109
|
-
|
|
9110
|
-
requireSemicolon: true
|
|
9287
|
+
const defaults$3 = {
|
|
9288
|
+
relaxed: false
|
|
9111
9289
|
};
|
|
9112
|
-
|
|
9113
|
-
const lowercaseEntities = entities$1.map((it) => it.toLowerCase());
|
|
9114
|
-
function isNumerical(entity) {
|
|
9115
|
-
return entity.startsWith("&#");
|
|
9116
|
-
}
|
|
9117
|
-
function getLocation(location, entity, match) {
|
|
9118
|
-
const index = match.index ?? 0;
|
|
9119
|
-
return sliceLocation(location, index, index + entity.length);
|
|
9120
|
-
}
|
|
9121
|
-
function getDescription(context, options) {
|
|
9122
|
-
const url = "https://html.spec.whatwg.org/multipage/named-characters.html";
|
|
9123
|
-
let message;
|
|
9124
|
-
if (context.terminated) {
|
|
9125
|
-
message = `Unrecognized character reference \`${context.entity}\`.`;
|
|
9126
|
-
} else {
|
|
9127
|
-
message = `Character reference \`${context.entity}\` must be terminated by a semicolon.`;
|
|
9128
|
-
}
|
|
9129
|
-
return [
|
|
9130
|
-
message,
|
|
9131
|
-
`HTML5 defines a set of [valid character references](${url}) but this is not a valid one.`,
|
|
9132
|
-
"",
|
|
9133
|
-
"Ensure that:",
|
|
9134
|
-
"",
|
|
9135
|
-
"1. The character is one of the listed names.",
|
|
9136
|
-
...options.ignoreCase ? [] : ["1. The case is correct (names are case sensitive)."],
|
|
9137
|
-
...options.requireSemicolon ? ["1. The name is terminated with a `;`."] : []
|
|
9138
|
-
].join("\n");
|
|
9139
|
-
}
|
|
9140
|
-
class UnknownCharReference extends Rule {
|
|
9290
|
+
class ValidID extends Rule {
|
|
9141
9291
|
constructor(options) {
|
|
9142
|
-
super({ ...defaults$
|
|
9292
|
+
super({ ...defaults$3, ...options });
|
|
9143
9293
|
}
|
|
9144
9294
|
static schema() {
|
|
9145
9295
|
return {
|
|
9146
|
-
|
|
9147
|
-
type: "boolean"
|
|
9148
|
-
},
|
|
9149
|
-
requireSemicolon: {
|
|
9296
|
+
relaxed: {
|
|
9150
9297
|
type: "boolean"
|
|
9151
9298
|
}
|
|
9152
9299
|
};
|
|
9153
9300
|
}
|
|
9154
9301
|
documentation(context) {
|
|
9302
|
+
const { relaxed } = this.options;
|
|
9303
|
+
const message = this.messages[context].replace("id", "ID").replace(/^(.)/, (m) => m.toUpperCase());
|
|
9304
|
+
const relaxedDescription = relaxed ? [] : [
|
|
9305
|
+
" - ID must begin with a letter",
|
|
9306
|
+
" - ID must only contain letters, digits, `-` and `_`"
|
|
9307
|
+
];
|
|
9155
9308
|
return {
|
|
9156
|
-
description:
|
|
9157
|
-
|
|
9309
|
+
description: [
|
|
9310
|
+
`${message}.`,
|
|
9311
|
+
"",
|
|
9312
|
+
"Under the current configuration the following rules are applied:",
|
|
9313
|
+
"",
|
|
9314
|
+
" - ID must not be empty",
|
|
9315
|
+
" - ID must not contain any whitespace characters",
|
|
9316
|
+
...relaxedDescription
|
|
9317
|
+
].join("\n"),
|
|
9318
|
+
url: "https://html-validate.org/rules/valid-id.html"
|
|
9158
9319
|
};
|
|
9159
9320
|
}
|
|
9160
9321
|
setup() {
|
|
9161
|
-
this.on("
|
|
9162
|
-
const
|
|
9163
|
-
|
|
9164
|
-
|
|
9165
|
-
continue;
|
|
9166
|
-
}
|
|
9167
|
-
this.findCharacterReferences(node, child.textContent, child.location, {
|
|
9168
|
-
isAttribute: false
|
|
9169
|
-
});
|
|
9322
|
+
this.on("attr", this.isRelevant, (event) => {
|
|
9323
|
+
const { value } = event;
|
|
9324
|
+
if (value === null || value instanceof DynamicValue) {
|
|
9325
|
+
return;
|
|
9170
9326
|
}
|
|
9171
|
-
|
|
9172
|
-
|
|
9173
|
-
|
|
9327
|
+
if (value === "") {
|
|
9328
|
+
const context = 1 /* EMPTY */;
|
|
9329
|
+
this.report(event.target, this.messages[context], event.location, context);
|
|
9174
9330
|
return;
|
|
9175
9331
|
}
|
|
9176
|
-
|
|
9177
|
-
|
|
9178
|
-
|
|
9332
|
+
if (value.match(/\s/)) {
|
|
9333
|
+
const context = 2 /* WHITESPACE */;
|
|
9334
|
+
this.report(event.target, this.messages[context], event.valueLocation, context);
|
|
9335
|
+
return;
|
|
9336
|
+
}
|
|
9337
|
+
const { relaxed } = this.options;
|
|
9338
|
+
if (relaxed) {
|
|
9339
|
+
return;
|
|
9340
|
+
}
|
|
9341
|
+
if (value.match(/^[^\p{L}]/u)) {
|
|
9342
|
+
const context = 3 /* LEADING_CHARACTER */;
|
|
9343
|
+
this.report(event.target, this.messages[context], event.valueLocation, context);
|
|
9344
|
+
return;
|
|
9345
|
+
}
|
|
9346
|
+
if (value.match(/[^\p{L}\p{N}_-]/u)) {
|
|
9347
|
+
const context = 4 /* DISALLOWED_CHARACTER */;
|
|
9348
|
+
this.report(event.target, this.messages[context], event.valueLocation, context);
|
|
9349
|
+
}
|
|
9179
9350
|
});
|
|
9180
9351
|
}
|
|
9181
|
-
get
|
|
9182
|
-
|
|
9183
|
-
|
|
9184
|
-
|
|
9185
|
-
|
|
9186
|
-
|
|
9187
|
-
}
|
|
9188
|
-
findCharacterReferences(node, text, location, { isAttribute }) {
|
|
9189
|
-
const isQuerystring = isAttribute && text.includes("?");
|
|
9190
|
-
for (const match of this.getMatches(text)) {
|
|
9191
|
-
this.validateCharacterReference(node, location, match, { isQuerystring });
|
|
9192
|
-
}
|
|
9193
|
-
}
|
|
9194
|
-
validateCharacterReference(node, location, foobar, { isQuerystring }) {
|
|
9195
|
-
const { requireSemicolon } = this.options;
|
|
9196
|
-
const { match, entity, raw, terminated } = foobar;
|
|
9197
|
-
if (isNumerical(entity)) {
|
|
9198
|
-
return;
|
|
9199
|
-
}
|
|
9200
|
-
if (isQuerystring && !terminated) {
|
|
9201
|
-
return;
|
|
9202
|
-
}
|
|
9203
|
-
const found = this.entities.includes(entity);
|
|
9204
|
-
if (found && (terminated || !requireSemicolon)) {
|
|
9205
|
-
return;
|
|
9206
|
-
}
|
|
9207
|
-
if (found && !terminated) {
|
|
9208
|
-
const entityLocation2 = getLocation(location, entity, match);
|
|
9209
|
-
const message2 = `Character reference "{{ entity }}" must be terminated by a semicolon`;
|
|
9210
|
-
const context2 = {
|
|
9211
|
-
entity: raw,
|
|
9212
|
-
terminated: false
|
|
9213
|
-
};
|
|
9214
|
-
this.report(node, message2, entityLocation2, context2);
|
|
9215
|
-
return;
|
|
9216
|
-
}
|
|
9217
|
-
const entityLocation = getLocation(location, entity, match);
|
|
9218
|
-
const message = `Unrecognized character reference "{{ entity }}"`;
|
|
9219
|
-
const context = {
|
|
9220
|
-
entity: raw,
|
|
9221
|
-
terminated: true
|
|
9352
|
+
get messages() {
|
|
9353
|
+
return {
|
|
9354
|
+
[1 /* EMPTY */]: "element id must not be empty",
|
|
9355
|
+
[2 /* WHITESPACE */]: "element id must not contain whitespace",
|
|
9356
|
+
[3 /* LEADING_CHARACTER */]: "element id must begin with a letter",
|
|
9357
|
+
[4 /* DISALLOWED_CHARACTER */]: "element id must only contain letters, digits, dash and underscore characters"
|
|
9222
9358
|
};
|
|
9223
|
-
this.report(node, message, entityLocation, context);
|
|
9224
9359
|
}
|
|
9225
|
-
|
|
9226
|
-
|
|
9227
|
-
|
|
9228
|
-
|
|
9229
|
-
|
|
9230
|
-
|
|
9231
|
-
|
|
9232
|
-
|
|
9233
|
-
|
|
9234
|
-
|
|
9235
|
-
|
|
9236
|
-
|
|
9237
|
-
|
|
9238
|
-
|
|
9360
|
+
isRelevant(event) {
|
|
9361
|
+
return event.key === "id";
|
|
9362
|
+
}
|
|
9363
|
+
}
|
|
9364
|
+
|
|
9365
|
+
class VoidContent extends Rule {
|
|
9366
|
+
documentation(tagName) {
|
|
9367
|
+
const doc = {
|
|
9368
|
+
description: "HTML void elements cannot have any content and must not have content or end tag.",
|
|
9369
|
+
url: "https://html-validate.org/rules/void-content.html"
|
|
9370
|
+
};
|
|
9371
|
+
if (tagName) {
|
|
9372
|
+
doc.description = `<${tagName}> is a void element and must not have content or end tag.`;
|
|
9373
|
+
}
|
|
9374
|
+
return doc;
|
|
9375
|
+
}
|
|
9376
|
+
setup() {
|
|
9377
|
+
this.on("tag:end", (event) => {
|
|
9378
|
+
const node = event.target;
|
|
9379
|
+
if (!node) {
|
|
9380
|
+
return;
|
|
9239
9381
|
}
|
|
9240
|
-
|
|
9382
|
+
if (!node.voidElement) {
|
|
9383
|
+
return;
|
|
9384
|
+
}
|
|
9385
|
+
if (node.closed === NodeClosed.EndTag) {
|
|
9386
|
+
this.report(
|
|
9387
|
+
null,
|
|
9388
|
+
`End tag for <${node.tagName}> must be omitted`,
|
|
9389
|
+
node.location,
|
|
9390
|
+
node.tagName
|
|
9391
|
+
);
|
|
9392
|
+
}
|
|
9393
|
+
});
|
|
9241
9394
|
}
|
|
9242
9395
|
}
|
|
9243
9396
|
|
|
9244
|
-
const
|
|
9245
|
-
|
|
9246
|
-
"name",
|
|
9247
|
-
"honorific-prefix",
|
|
9248
|
-
"given-name",
|
|
9249
|
-
"additional-name",
|
|
9250
|
-
"family-name",
|
|
9251
|
-
"honorific-suffix",
|
|
9252
|
-
"nickname",
|
|
9253
|
-
"username",
|
|
9254
|
-
"new-password",
|
|
9255
|
-
"current-password",
|
|
9256
|
-
"one-time-code",
|
|
9257
|
-
"organization-title",
|
|
9258
|
-
"organization",
|
|
9259
|
-
"street-address",
|
|
9260
|
-
"address-line1",
|
|
9261
|
-
"address-line2",
|
|
9262
|
-
"address-line3",
|
|
9263
|
-
"address-level4",
|
|
9264
|
-
"address-level3",
|
|
9265
|
-
"address-level2",
|
|
9266
|
-
"address-level1",
|
|
9267
|
-
"country",
|
|
9268
|
-
"country-name",
|
|
9269
|
-
"postal-code",
|
|
9270
|
-
"cc-name",
|
|
9271
|
-
"cc-given-name",
|
|
9272
|
-
"cc-additional-name",
|
|
9273
|
-
"cc-family-name",
|
|
9274
|
-
"cc-number",
|
|
9275
|
-
"cc-exp",
|
|
9276
|
-
"cc-exp-month",
|
|
9277
|
-
"cc-exp-year",
|
|
9278
|
-
"cc-csc",
|
|
9279
|
-
"cc-type",
|
|
9280
|
-
"transaction-currency",
|
|
9281
|
-
"transaction-amount",
|
|
9282
|
-
"language",
|
|
9283
|
-
"bday",
|
|
9284
|
-
"bday-day",
|
|
9285
|
-
"bday-month",
|
|
9286
|
-
"bday-year",
|
|
9287
|
-
"sex",
|
|
9288
|
-
"url",
|
|
9289
|
-
"photo"
|
|
9290
|
-
];
|
|
9291
|
-
const fieldNames2 = [
|
|
9292
|
-
"tel",
|
|
9293
|
-
"tel-country-code",
|
|
9294
|
-
"tel-national",
|
|
9295
|
-
"tel-area-code",
|
|
9296
|
-
"tel-local",
|
|
9297
|
-
"tel-local-prefix",
|
|
9298
|
-
"tel-local-suffix",
|
|
9299
|
-
"tel-extension",
|
|
9300
|
-
"email",
|
|
9301
|
-
"impp"
|
|
9302
|
-
];
|
|
9303
|
-
const fieldNameGroup = {
|
|
9304
|
-
name: "text",
|
|
9305
|
-
"honorific-prefix": "text",
|
|
9306
|
-
"given-name": "text",
|
|
9307
|
-
"additional-name": "text",
|
|
9308
|
-
"family-name": "text",
|
|
9309
|
-
"honorific-suffix": "text",
|
|
9310
|
-
nickname: "text",
|
|
9311
|
-
username: "username",
|
|
9312
|
-
"new-password": "password",
|
|
9313
|
-
"current-password": "password",
|
|
9314
|
-
"one-time-code": "password",
|
|
9315
|
-
"organization-title": "text",
|
|
9316
|
-
organization: "text",
|
|
9317
|
-
"street-address": "multiline",
|
|
9318
|
-
"address-line1": "text",
|
|
9319
|
-
"address-line2": "text",
|
|
9320
|
-
"address-line3": "text",
|
|
9321
|
-
"address-level4": "text",
|
|
9322
|
-
"address-level3": "text",
|
|
9323
|
-
"address-level2": "text",
|
|
9324
|
-
"address-level1": "text",
|
|
9325
|
-
country: "text",
|
|
9326
|
-
"country-name": "text",
|
|
9327
|
-
"postal-code": "text",
|
|
9328
|
-
"cc-name": "text",
|
|
9329
|
-
"cc-given-name": "text",
|
|
9330
|
-
"cc-additional-name": "text",
|
|
9331
|
-
"cc-family-name": "text",
|
|
9332
|
-
"cc-number": "text",
|
|
9333
|
-
"cc-exp": "month",
|
|
9334
|
-
"cc-exp-month": "numeric",
|
|
9335
|
-
"cc-exp-year": "numeric",
|
|
9336
|
-
"cc-csc": "text",
|
|
9337
|
-
"cc-type": "text",
|
|
9338
|
-
"transaction-currency": "text",
|
|
9339
|
-
"transaction-amount": "numeric",
|
|
9340
|
-
language: "text",
|
|
9341
|
-
bday: "date",
|
|
9342
|
-
"bday-day": "numeric",
|
|
9343
|
-
"bday-month": "numeric",
|
|
9344
|
-
"bday-year": "numeric",
|
|
9345
|
-
sex: "text",
|
|
9346
|
-
url: "url",
|
|
9347
|
-
photo: "url",
|
|
9348
|
-
tel: "tel",
|
|
9349
|
-
"tel-country-code": "text",
|
|
9350
|
-
"tel-national": "text",
|
|
9351
|
-
"tel-area-code": "text",
|
|
9352
|
-
"tel-local": "text",
|
|
9353
|
-
"tel-local-prefix": "text",
|
|
9354
|
-
"tel-local-suffix": "text",
|
|
9355
|
-
"tel-extension": "text",
|
|
9356
|
-
email: "username",
|
|
9357
|
-
impp: "url"
|
|
9397
|
+
const defaults$2 = {
|
|
9398
|
+
style: "omit"
|
|
9358
9399
|
};
|
|
9359
|
-
|
|
9360
|
-
|
|
9361
|
-
|
|
9362
|
-
|
|
9363
|
-
function matchHint(token) {
|
|
9364
|
-
return token === "shipping" || token === "billing";
|
|
9365
|
-
}
|
|
9366
|
-
function matchFieldNames1(token) {
|
|
9367
|
-
return fieldNames1.includes(token);
|
|
9368
|
-
}
|
|
9369
|
-
function matchContact(token) {
|
|
9370
|
-
const haystack = ["home", "work", "mobile", "fax", "pager"];
|
|
9371
|
-
return haystack.includes(token);
|
|
9372
|
-
}
|
|
9373
|
-
function matchFieldNames2(token) {
|
|
9374
|
-
return fieldNames2.includes(token);
|
|
9375
|
-
}
|
|
9376
|
-
function matchWebauthn(token) {
|
|
9377
|
-
return token === "webauthn";
|
|
9378
|
-
}
|
|
9379
|
-
function matchToken(token) {
|
|
9380
|
-
if (matchSection(token)) {
|
|
9381
|
-
return "section";
|
|
9400
|
+
class VoidStyle extends Rule {
|
|
9401
|
+
constructor(options) {
|
|
9402
|
+
super({ ...defaults$2, ...options });
|
|
9403
|
+
this.style = parseStyle(this.options.style);
|
|
9382
9404
|
}
|
|
9383
|
-
|
|
9384
|
-
return
|
|
9405
|
+
static schema() {
|
|
9406
|
+
return {
|
|
9407
|
+
style: {
|
|
9408
|
+
enum: ["omit", "selfclose", "selfclosing"],
|
|
9409
|
+
type: "string"
|
|
9410
|
+
}
|
|
9411
|
+
};
|
|
9385
9412
|
}
|
|
9386
|
-
|
|
9387
|
-
|
|
9413
|
+
documentation(context) {
|
|
9414
|
+
const [desc, end] = styleDescription(context.style);
|
|
9415
|
+
return {
|
|
9416
|
+
description: `The current configuration requires void elements to ${desc}, use <${context.tagName}${end}> instead.`,
|
|
9417
|
+
url: "https://html-validate.org/rules/void-style.html"
|
|
9418
|
+
};
|
|
9388
9419
|
}
|
|
9389
|
-
|
|
9390
|
-
|
|
9420
|
+
setup() {
|
|
9421
|
+
this.on("tag:end", (event) => {
|
|
9422
|
+
const active = event.previous;
|
|
9423
|
+
if (active.meta) {
|
|
9424
|
+
this.validateActive(active);
|
|
9425
|
+
}
|
|
9426
|
+
});
|
|
9391
9427
|
}
|
|
9392
|
-
|
|
9393
|
-
|
|
9428
|
+
validateActive(node) {
|
|
9429
|
+
if (!node.voidElement) {
|
|
9430
|
+
return;
|
|
9431
|
+
}
|
|
9432
|
+
if (this.shouldBeOmitted(node)) {
|
|
9433
|
+
this.reportError(
|
|
9434
|
+
node,
|
|
9435
|
+
`Expected omitted end tag <${node.tagName}> instead of self-closing element <${node.tagName}/>`
|
|
9436
|
+
);
|
|
9437
|
+
}
|
|
9438
|
+
if (this.shouldBeSelfClosed(node)) {
|
|
9439
|
+
this.reportError(
|
|
9440
|
+
node,
|
|
9441
|
+
`Expected self-closing element <${node.tagName}/> instead of omitted end-tag <${node.tagName}>`
|
|
9442
|
+
);
|
|
9443
|
+
}
|
|
9394
9444
|
}
|
|
9395
|
-
|
|
9396
|
-
|
|
9445
|
+
reportError(node, message) {
|
|
9446
|
+
const context = {
|
|
9447
|
+
style: this.style,
|
|
9448
|
+
tagName: node.tagName
|
|
9449
|
+
};
|
|
9450
|
+
super.report(node, message, null, context);
|
|
9451
|
+
}
|
|
9452
|
+
shouldBeOmitted(node) {
|
|
9453
|
+
return this.style === 1 /* AlwaysOmit */ && node.closed === NodeClosed.VoidSelfClosed;
|
|
9454
|
+
}
|
|
9455
|
+
shouldBeSelfClosed(node) {
|
|
9456
|
+
return this.style === 2 /* AlwaysSelfclose */ && node.closed === NodeClosed.VoidOmitted;
|
|
9397
9457
|
}
|
|
9398
|
-
return null;
|
|
9399
9458
|
}
|
|
9400
|
-
function
|
|
9401
|
-
|
|
9402
|
-
"
|
|
9403
|
-
|
|
9404
|
-
"
|
|
9405
|
-
"
|
|
9406
|
-
|
|
9407
|
-
|
|
9408
|
-
|
|
9409
|
-
"month",
|
|
9410
|
-
"date"
|
|
9411
|
-
];
|
|
9412
|
-
const mapping = {
|
|
9413
|
-
hidden: allGroups,
|
|
9414
|
-
text: allGroups.filter((it) => it !== "multiline"),
|
|
9415
|
-
search: allGroups.filter((it) => it !== "multiline"),
|
|
9416
|
-
password: ["password"],
|
|
9417
|
-
url: ["url"],
|
|
9418
|
-
email: ["username"],
|
|
9419
|
-
tel: ["tel"],
|
|
9420
|
-
number: ["numeric"],
|
|
9421
|
-
month: ["month"],
|
|
9422
|
-
date: ["date"]
|
|
9423
|
-
};
|
|
9424
|
-
const groups = mapping[type];
|
|
9425
|
-
if (groups) {
|
|
9426
|
-
return groups;
|
|
9459
|
+
function parseStyle(name) {
|
|
9460
|
+
switch (name) {
|
|
9461
|
+
case "omit":
|
|
9462
|
+
return 1 /* AlwaysOmit */;
|
|
9463
|
+
case "selfclose":
|
|
9464
|
+
case "selfclosing":
|
|
9465
|
+
return 2 /* AlwaysSelfclose */;
|
|
9466
|
+
default:
|
|
9467
|
+
throw new Error(`Invalid style "${name}" for "void-style" rule`);
|
|
9427
9468
|
}
|
|
9428
|
-
return [];
|
|
9429
9469
|
}
|
|
9430
|
-
function
|
|
9431
|
-
|
|
9432
|
-
|
|
9470
|
+
function styleDescription(style) {
|
|
9471
|
+
switch (style) {
|
|
9472
|
+
case 1 /* AlwaysOmit */:
|
|
9473
|
+
return ["omit end tag", ""];
|
|
9474
|
+
case 2 /* AlwaysSelfclose */:
|
|
9475
|
+
return ["be self-closed", "/"];
|
|
9476
|
+
default:
|
|
9477
|
+
throw new Error(`Unknown style`);
|
|
9433
9478
|
}
|
|
9434
|
-
return disallowedInputTypes.includes(type);
|
|
9435
9479
|
}
|
|
9436
|
-
|
|
9437
|
-
|
|
9438
|
-
|
|
9439
|
-
|
|
9440
|
-
|
|
9441
|
-
|
|
9442
|
-
|
|
9443
|
-
return '"{{ second }}" must appear before "{{ first }}"';
|
|
9444
|
-
case 3 /* InvalidToken */:
|
|
9445
|
-
return '"{{ token }}" is not a valid autocomplete token or field name';
|
|
9446
|
-
case 4 /* InvalidCombination */:
|
|
9447
|
-
return '"{{ second }}" cannot be combined with "{{ first }}"';
|
|
9448
|
-
case 5 /* MissingField */:
|
|
9449
|
-
return "autocomplete attribute is missing field name";
|
|
9480
|
+
|
|
9481
|
+
class H30 extends Rule {
|
|
9482
|
+
documentation() {
|
|
9483
|
+
return {
|
|
9484
|
+
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.",
|
|
9485
|
+
url: "https://html-validate.org/rules/wcag/h30.html"
|
|
9486
|
+
};
|
|
9450
9487
|
}
|
|
9451
|
-
|
|
9452
|
-
|
|
9453
|
-
|
|
9454
|
-
|
|
9455
|
-
|
|
9456
|
-
|
|
9457
|
-
|
|
9458
|
-
|
|
9459
|
-
|
|
9460
|
-
|
|
9461
|
-
|
|
9462
|
-
|
|
9463
|
-
|
|
9464
|
-
|
|
9465
|
-
|
|
9466
|
-
|
|
9467
|
-
|
|
9468
|
-
|
|
9469
|
-
|
|
9470
|
-
|
|
9471
|
-
|
|
9472
|
-
|
|
9473
|
-
|
|
9474
|
-
"",
|
|
9475
|
-
'`<input type="hidden">` cannot use the values `"on"` and `"off"`.'
|
|
9476
|
-
].join("\n");
|
|
9488
|
+
setup() {
|
|
9489
|
+
this.on("dom:ready", (event) => {
|
|
9490
|
+
const links = event.document.getElementsByTagName("a");
|
|
9491
|
+
for (const link of links) {
|
|
9492
|
+
if (!link.hasAttribute("href")) {
|
|
9493
|
+
continue;
|
|
9494
|
+
}
|
|
9495
|
+
if (!inAccessibilityTree(link)) {
|
|
9496
|
+
continue;
|
|
9497
|
+
}
|
|
9498
|
+
const textClassification = classifyNodeText(link, { ignoreHiddenRoot: true });
|
|
9499
|
+
if (textClassification !== TextClassification.EMPTY_TEXT) {
|
|
9500
|
+
continue;
|
|
9501
|
+
}
|
|
9502
|
+
const images = link.querySelectorAll("img");
|
|
9503
|
+
if (images.some((image) => hasAltText(image))) {
|
|
9504
|
+
continue;
|
|
9505
|
+
}
|
|
9506
|
+
const labels = link.querySelectorAll("[aria-label]");
|
|
9507
|
+
if (hasAriaLabel(link) || labels.some((cur) => hasAriaLabel(cur))) {
|
|
9508
|
+
continue;
|
|
9509
|
+
}
|
|
9510
|
+
this.report(link, "Anchor link must have a text describing its purpose");
|
|
9477
9511
|
}
|
|
9478
|
-
|
|
9479
|
-
const currentGroup = fieldNameGroup[context.value];
|
|
9480
|
-
return [
|
|
9481
|
-
message,
|
|
9482
|
-
"",
|
|
9483
|
-
`\`${context.what}\` allows autocomplete fields from the following group${controlGroups.length > 1 ? "s" : ""}:`,
|
|
9484
|
-
"",
|
|
9485
|
-
...controlGroups.map((it) => `- ${it}`),
|
|
9486
|
-
"",
|
|
9487
|
-
`The field \`"${context.value}"\` belongs to the group /${currentGroup}/ which cannot be used with this input type.`
|
|
9488
|
-
].join("\n");
|
|
9489
|
-
}
|
|
9490
|
-
case 2 /* InvalidOrder */:
|
|
9491
|
-
return [
|
|
9492
|
-
`\`"${context.second}"\` must appear before \`"${context.first}"\``,
|
|
9493
|
-
"",
|
|
9494
|
-
"The autocomplete tokens must appear in the following order:",
|
|
9495
|
-
"",
|
|
9496
|
-
"- Optional section name (`section-` prefix).",
|
|
9497
|
-
"- Optional `shipping` or `billing` token.",
|
|
9498
|
-
"- Optional `home`, `work`, `mobile`, `fax` or `pager` token (for fields supporting it).",
|
|
9499
|
-
"- Field name",
|
|
9500
|
-
"- Optional `webauthn` token."
|
|
9501
|
-
].join("\n");
|
|
9502
|
-
case 3 /* InvalidToken */:
|
|
9503
|
-
return `\`"${context.token}"\` is not a valid autocomplete token or field name`;
|
|
9504
|
-
case 4 /* InvalidCombination */:
|
|
9505
|
-
return `\`"${context.second}"\` cannot be combined with \`"${context.first}"\``;
|
|
9506
|
-
case 5 /* MissingField */:
|
|
9507
|
-
return "Autocomplete attribute is missing field name";
|
|
9512
|
+
});
|
|
9508
9513
|
}
|
|
9509
9514
|
}
|
|
9510
|
-
|
|
9511
|
-
|
|
9515
|
+
|
|
9516
|
+
class H32 extends Rule {
|
|
9517
|
+
documentation() {
|
|
9512
9518
|
return {
|
|
9513
|
-
description:
|
|
9514
|
-
url: "https://html-validate.org/rules/
|
|
9519
|
+
description: "WCAG 2.1 requires each `<form>` element to have at least one submit button.",
|
|
9520
|
+
url: "https://html-validate.org/rules/wcag/h32.html"
|
|
9515
9521
|
};
|
|
9516
9522
|
}
|
|
9517
9523
|
setup() {
|
|
9524
|
+
const formTags = this.getTagsWithProperty("form");
|
|
9525
|
+
const formSelector = formTags.join(",");
|
|
9518
9526
|
this.on("dom:ready", (event) => {
|
|
9519
9527
|
const { document } = event;
|
|
9520
|
-
const
|
|
9521
|
-
for (const
|
|
9522
|
-
|
|
9523
|
-
if (autocomplete.value === null || autocomplete.value instanceof DynamicValue) {
|
|
9528
|
+
const forms = document.querySelectorAll(formSelector);
|
|
9529
|
+
for (const form of forms) {
|
|
9530
|
+
if (hasNestedSubmit(form)) {
|
|
9524
9531
|
continue;
|
|
9525
9532
|
}
|
|
9526
|
-
|
|
9527
|
-
const value = autocomplete.value.toLowerCase();
|
|
9528
|
-
const tokens = new DOMTokenList(value, location);
|
|
9529
|
-
if (tokens.length === 0) {
|
|
9533
|
+
if (hasAssociatedSubmit(document, form)) {
|
|
9530
9534
|
continue;
|
|
9531
9535
|
}
|
|
9532
|
-
this.
|
|
9536
|
+
this.report(form, `<${form.tagName}> element must have a submit button`);
|
|
9533
9537
|
}
|
|
9534
9538
|
});
|
|
9535
9539
|
}
|
|
9536
|
-
|
|
9537
|
-
|
|
9538
|
-
|
|
9539
|
-
|
|
9540
|
-
|
|
9541
|
-
|
|
9542
|
-
|
|
9543
|
-
|
|
9544
|
-
|
|
9545
|
-
|
|
9546
|
-
|
|
9547
|
-
|
|
9548
|
-
|
|
9549
|
-
|
|
9550
|
-
|
|
9551
|
-
|
|
9552
|
-
|
|
9553
|
-
msg: 0 /* InvalidAttribute */,
|
|
9554
|
-
what: `<input type="${type}">`
|
|
9555
|
-
};
|
|
9556
|
-
this.report({
|
|
9557
|
-
node,
|
|
9558
|
-
message: getTerminalMessage(context),
|
|
9559
|
-
location: keyLocation,
|
|
9560
|
-
context
|
|
9561
|
-
});
|
|
9562
|
-
return;
|
|
9563
|
-
}
|
|
9564
|
-
if (tokens.includes("on") || tokens.includes("off")) {
|
|
9565
|
-
this.validateOnOff(node, mantle, tokens);
|
|
9566
|
-
return;
|
|
9567
|
-
}
|
|
9568
|
-
this.validateTokens(node, tokens, keyLocation);
|
|
9540
|
+
}
|
|
9541
|
+
function isSubmit(node) {
|
|
9542
|
+
const type = node.getAttribute("type");
|
|
9543
|
+
return Boolean(!type || type.valueMatches(/submit|image/));
|
|
9544
|
+
}
|
|
9545
|
+
function isAssociated(id, node) {
|
|
9546
|
+
const form = node.getAttribute("form");
|
|
9547
|
+
return Boolean(form == null ? void 0 : form.valueMatches(id, true));
|
|
9548
|
+
}
|
|
9549
|
+
function hasNestedSubmit(form) {
|
|
9550
|
+
const matches = form.querySelectorAll("button,input").filter(isSubmit).filter((node) => !node.hasAttribute("form"));
|
|
9551
|
+
return matches.length > 0;
|
|
9552
|
+
}
|
|
9553
|
+
function hasAssociatedSubmit(document, form) {
|
|
9554
|
+
const { id } = form;
|
|
9555
|
+
if (!id) {
|
|
9556
|
+
return false;
|
|
9569
9557
|
}
|
|
9570
|
-
|
|
9571
|
-
|
|
9572
|
-
|
|
9573
|
-
|
|
9574
|
-
|
|
9575
|
-
|
|
9576
|
-
|
|
9577
|
-
|
|
9578
|
-
|
|
9579
|
-
|
|
9558
|
+
const matches = document.querySelectorAll("button[form],input[form]").filter(isSubmit).filter((node) => isAssociated(id, node));
|
|
9559
|
+
return matches.length > 0;
|
|
9560
|
+
}
|
|
9561
|
+
|
|
9562
|
+
class H36 extends Rule {
|
|
9563
|
+
documentation() {
|
|
9564
|
+
return {
|
|
9565
|
+
description: [
|
|
9566
|
+
"WCAG 2.1 requires all images used as submit buttons to have a non-empty textual description using the `alt` attribute.",
|
|
9567
|
+
'The alt text cannot be empty (`alt=""`).'
|
|
9568
|
+
].join("\n"),
|
|
9569
|
+
url: "https://html-validate.org/rules/wcag/h36.html"
|
|
9580
9570
|
};
|
|
9581
|
-
this.report({
|
|
9582
|
-
node,
|
|
9583
|
-
message: getTerminalMessage(context),
|
|
9584
|
-
location,
|
|
9585
|
-
context
|
|
9586
|
-
});
|
|
9587
9571
|
}
|
|
9588
|
-
|
|
9589
|
-
|
|
9590
|
-
|
|
9591
|
-
|
|
9592
|
-
|
|
9593
|
-
|
|
9594
|
-
|
|
9595
|
-
/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
|
|
9596
|
-
first: tokens.item(index > 0 ? 0 : 1),
|
|
9597
|
-
second: value
|
|
9598
|
-
};
|
|
9599
|
-
this.report({
|
|
9600
|
-
node,
|
|
9601
|
-
message: getTerminalMessage(context),
|
|
9602
|
-
location,
|
|
9603
|
-
context
|
|
9604
|
-
});
|
|
9605
|
-
}
|
|
9606
|
-
switch (mantle) {
|
|
9607
|
-
case "expectation":
|
|
9608
|
-
return;
|
|
9609
|
-
case "anchor": {
|
|
9610
|
-
const context = {
|
|
9611
|
-
msg: 1 /* InvalidValue */,
|
|
9612
|
-
type: "hidden",
|
|
9613
|
-
value,
|
|
9614
|
-
what: `<input type="hidden">`
|
|
9615
|
-
};
|
|
9616
|
-
this.report({
|
|
9617
|
-
node,
|
|
9618
|
-
message: getTerminalMessage(context),
|
|
9619
|
-
location: tokens.location(0),
|
|
9620
|
-
context
|
|
9621
|
-
});
|
|
9572
|
+
setup() {
|
|
9573
|
+
this.on("tag:end", (event) => {
|
|
9574
|
+
const node = event.previous;
|
|
9575
|
+
if (node.tagName !== "input")
|
|
9576
|
+
return;
|
|
9577
|
+
if (node.getAttributeValue("type") !== "image") {
|
|
9578
|
+
return;
|
|
9622
9579
|
}
|
|
9623
|
-
|
|
9624
|
-
}
|
|
9625
|
-
validateTokens(node, tokens, keyLocation) {
|
|
9626
|
-
const order = [];
|
|
9627
|
-
for (const { item, location } of tokens.iterator()) {
|
|
9628
|
-
const tokenType = matchToken(item);
|
|
9629
|
-
if (tokenType) {
|
|
9630
|
-
order.push(tokenType);
|
|
9631
|
-
} else {
|
|
9632
|
-
const context = {
|
|
9633
|
-
msg: 3 /* InvalidToken */,
|
|
9634
|
-
token: item
|
|
9635
|
-
};
|
|
9636
|
-
this.report({
|
|
9637
|
-
node,
|
|
9638
|
-
message: getTerminalMessage(context),
|
|
9639
|
-
location,
|
|
9640
|
-
context
|
|
9641
|
-
});
|
|
9580
|
+
if (!inAccessibilityTree(node)) {
|
|
9642
9581
|
return;
|
|
9643
9582
|
}
|
|
9644
|
-
|
|
9645
|
-
|
|
9646
|
-
|
|
9647
|
-
this.validateContact(node, tokens, order);
|
|
9648
|
-
this.validateOrder(node, tokens, order);
|
|
9649
|
-
this.validateControlGroup(node, tokens, fieldTokens);
|
|
9650
|
-
}
|
|
9651
|
-
/**
|
|
9652
|
-
* Ensure that exactly one field name is present from the two field lists.
|
|
9653
|
-
*/
|
|
9654
|
-
validateFieldPresence(node, tokens, fieldTokens, keyLocation) {
|
|
9655
|
-
const numFields = fieldTokens.filter(Boolean).length;
|
|
9656
|
-
if (numFields === 0) {
|
|
9657
|
-
const context = {
|
|
9658
|
-
msg: 5 /* MissingField */
|
|
9659
|
-
};
|
|
9660
|
-
this.report({
|
|
9661
|
-
node,
|
|
9662
|
-
message: getTerminalMessage(context),
|
|
9663
|
-
location: keyLocation,
|
|
9664
|
-
context
|
|
9665
|
-
});
|
|
9666
|
-
} else if (numFields > 1) {
|
|
9667
|
-
const a = fieldTokens.indexOf(true);
|
|
9668
|
-
const b = fieldTokens.lastIndexOf(true);
|
|
9669
|
-
const context = {
|
|
9670
|
-
msg: 4 /* InvalidCombination */,
|
|
9671
|
-
/* eslint-disable @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
|
|
9672
|
-
first: tokens.item(a),
|
|
9673
|
-
second: tokens.item(b)
|
|
9674
|
-
/* eslint-enable @typescript-eslint/no-non-null-assertion */
|
|
9675
|
-
};
|
|
9676
|
-
this.report({
|
|
9677
|
-
node,
|
|
9678
|
-
message: getTerminalMessage(context),
|
|
9679
|
-
location: tokens.location(b),
|
|
9680
|
-
context
|
|
9681
|
-
});
|
|
9682
|
-
}
|
|
9683
|
-
}
|
|
9684
|
-
/**
|
|
9685
|
-
* Ensure contact token is only used with field names from the second list.
|
|
9686
|
-
*/
|
|
9687
|
-
validateContact(node, tokens, order) {
|
|
9688
|
-
if (order.includes("contact") && order.includes("field1")) {
|
|
9689
|
-
const a = order.indexOf("field1");
|
|
9690
|
-
const b = order.indexOf("contact");
|
|
9691
|
-
const context = {
|
|
9692
|
-
msg: 4 /* InvalidCombination */,
|
|
9693
|
-
/* eslint-disable @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
|
|
9694
|
-
first: tokens.item(a),
|
|
9695
|
-
second: tokens.item(b)
|
|
9696
|
-
/* eslint-enable @typescript-eslint/no-non-null-assertion */
|
|
9697
|
-
};
|
|
9698
|
-
this.report({
|
|
9699
|
-
node,
|
|
9700
|
-
message: getTerminalMessage(context),
|
|
9701
|
-
location: tokens.location(b),
|
|
9702
|
-
context
|
|
9703
|
-
});
|
|
9704
|
-
}
|
|
9705
|
-
}
|
|
9706
|
-
validateOrder(node, tokens, order) {
|
|
9707
|
-
const indicies = order.map((it) => expectedOrder.indexOf(it));
|
|
9708
|
-
for (let i = 0; i < indicies.length - 1; i++) {
|
|
9709
|
-
if (indicies[0] > indicies[i + 1]) {
|
|
9710
|
-
const context = {
|
|
9711
|
-
msg: 2 /* InvalidOrder */,
|
|
9712
|
-
/* eslint-disable @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
|
|
9713
|
-
first: tokens.item(i),
|
|
9714
|
-
second: tokens.item(i + 1)
|
|
9715
|
-
/* eslint-enable @typescript-eslint/no-non-null-assertion */
|
|
9716
|
-
};
|
|
9583
|
+
if (!hasAltText(node)) {
|
|
9584
|
+
const message = "image used as submit button must have non-empty alt text";
|
|
9585
|
+
const alt = node.getAttribute("alt");
|
|
9717
9586
|
this.report({
|
|
9718
9587
|
node,
|
|
9719
|
-
message
|
|
9720
|
-
location:
|
|
9721
|
-
context
|
|
9588
|
+
message,
|
|
9589
|
+
location: alt ? alt.keyLocation : node.location
|
|
9722
9590
|
});
|
|
9723
9591
|
}
|
|
9724
|
-
}
|
|
9725
|
-
}
|
|
9726
|
-
validateControlGroup(node, tokens, fieldTokens) {
|
|
9727
|
-
const numFields = fieldTokens.filter(Boolean).length;
|
|
9728
|
-
if (numFields === 0) {
|
|
9729
|
-
return;
|
|
9730
|
-
}
|
|
9731
|
-
if (!node.is("input")) {
|
|
9732
|
-
return;
|
|
9733
|
-
}
|
|
9734
|
-
const attr = node.getAttribute("type");
|
|
9735
|
-
const type = (attr == null ? void 0 : attr.value) ?? "text";
|
|
9736
|
-
if (type instanceof DynamicValue) {
|
|
9737
|
-
return;
|
|
9738
|
-
}
|
|
9739
|
-
const controlGroups = getControlGroups(type);
|
|
9740
|
-
const fieldIndex = fieldTokens.indexOf(true);
|
|
9741
|
-
const fieldToken = tokens.item(fieldIndex);
|
|
9742
|
-
const fieldGroup = fieldNameGroup[fieldToken];
|
|
9743
|
-
if (!controlGroups.includes(fieldGroup)) {
|
|
9744
|
-
const context = {
|
|
9745
|
-
msg: 1 /* InvalidValue */,
|
|
9746
|
-
type,
|
|
9747
|
-
value: fieldToken,
|
|
9748
|
-
what: `<input type="${type}">`
|
|
9749
|
-
};
|
|
9750
|
-
this.report({
|
|
9751
|
-
node,
|
|
9752
|
-
message: getTerminalMessage(context),
|
|
9753
|
-
location: tokens.location(fieldIndex),
|
|
9754
|
-
context
|
|
9755
|
-
});
|
|
9756
|
-
}
|
|
9592
|
+
});
|
|
9757
9593
|
}
|
|
9758
9594
|
}
|
|
9759
9595
|
|
|
9760
|
-
const defaults$
|
|
9761
|
-
|
|
9596
|
+
const defaults$1 = {
|
|
9597
|
+
allowEmpty: true,
|
|
9598
|
+
alias: []
|
|
9762
9599
|
};
|
|
9763
|
-
class
|
|
9600
|
+
class H37 extends Rule {
|
|
9764
9601
|
constructor(options) {
|
|
9765
|
-
super({ ...defaults$
|
|
9602
|
+
super({ ...defaults$1, ...options });
|
|
9603
|
+
if (!Array.isArray(this.options.alias)) {
|
|
9604
|
+
this.options.alias = [this.options.alias];
|
|
9605
|
+
}
|
|
9766
9606
|
}
|
|
9767
9607
|
static schema() {
|
|
9768
9608
|
return {
|
|
9769
|
-
|
|
9609
|
+
alias: {
|
|
9610
|
+
anyOf: [
|
|
9611
|
+
{
|
|
9612
|
+
items: {
|
|
9613
|
+
type: "string"
|
|
9614
|
+
},
|
|
9615
|
+
type: "array"
|
|
9616
|
+
},
|
|
9617
|
+
{
|
|
9618
|
+
type: "string"
|
|
9619
|
+
}
|
|
9620
|
+
]
|
|
9621
|
+
},
|
|
9622
|
+
allowEmpty: {
|
|
9770
9623
|
type: "boolean"
|
|
9771
9624
|
}
|
|
9772
9625
|
};
|
|
9773
9626
|
}
|
|
9774
|
-
documentation(
|
|
9775
|
-
const { relaxed } = this.options;
|
|
9776
|
-
const message = this.messages[context].replace("id", "ID").replace(/^(.)/, (m) => m.toUpperCase());
|
|
9777
|
-
const relaxedDescription = relaxed ? [] : [
|
|
9778
|
-
" - ID must begin with a letter",
|
|
9779
|
-
" - ID must only contain letters, digits, `-` and `_`"
|
|
9780
|
-
];
|
|
9627
|
+
documentation() {
|
|
9781
9628
|
return {
|
|
9782
|
-
description:
|
|
9783
|
-
|
|
9784
|
-
"",
|
|
9785
|
-
"Under the current configuration the following rules are applied:",
|
|
9786
|
-
"",
|
|
9787
|
-
" - ID must not be empty",
|
|
9788
|
-
" - ID must not contain any whitespace characters",
|
|
9789
|
-
...relaxedDescription
|
|
9790
|
-
].join("\n"),
|
|
9791
|
-
url: "https://html-validate.org/rules/valid-id.html"
|
|
9629
|
+
description: "Both HTML5 and WCAG 2.0 requires images to have a alternative text for each image.",
|
|
9630
|
+
url: "https://html-validate.org/rules/wcag/h37.html"
|
|
9792
9631
|
};
|
|
9793
9632
|
}
|
|
9794
9633
|
setup() {
|
|
9795
|
-
this.on("
|
|
9796
|
-
const {
|
|
9797
|
-
|
|
9798
|
-
|
|
9799
|
-
|
|
9800
|
-
if (value === "") {
|
|
9801
|
-
const context = 1 /* EMPTY */;
|
|
9802
|
-
this.report(event.target, this.messages[context], event.location, context);
|
|
9803
|
-
return;
|
|
9804
|
-
}
|
|
9805
|
-
if (value.match(/\s/)) {
|
|
9806
|
-
const context = 2 /* WHITESPACE */;
|
|
9807
|
-
this.report(event.target, this.messages[context], event.valueLocation, context);
|
|
9808
|
-
return;
|
|
9809
|
-
}
|
|
9810
|
-
const { relaxed } = this.options;
|
|
9811
|
-
if (relaxed) {
|
|
9812
|
-
return;
|
|
9813
|
-
}
|
|
9814
|
-
if (value.match(/^[^\p{L}]/u)) {
|
|
9815
|
-
const context = 3 /* LEADING_CHARACTER */;
|
|
9816
|
-
this.report(event.target, this.messages[context], event.valueLocation, context);
|
|
9817
|
-
return;
|
|
9818
|
-
}
|
|
9819
|
-
if (value.match(/[^\p{L}\p{N}_-]/u)) {
|
|
9820
|
-
const context = 4 /* DISALLOWED_CHARACTER */;
|
|
9821
|
-
this.report(event.target, this.messages[context], event.valueLocation, context);
|
|
9634
|
+
this.on("dom:ready", (event) => {
|
|
9635
|
+
const { document } = event;
|
|
9636
|
+
const nodes = document.querySelectorAll("img");
|
|
9637
|
+
for (const node of nodes) {
|
|
9638
|
+
this.validateNode(node);
|
|
9822
9639
|
}
|
|
9823
9640
|
});
|
|
9824
9641
|
}
|
|
9825
|
-
|
|
9826
|
-
|
|
9827
|
-
|
|
9828
|
-
|
|
9829
|
-
|
|
9830
|
-
|
|
9831
|
-
}
|
|
9832
|
-
|
|
9833
|
-
|
|
9834
|
-
|
|
9642
|
+
validateNode(node) {
|
|
9643
|
+
if (!inAccessibilityTree(node)) {
|
|
9644
|
+
return;
|
|
9645
|
+
}
|
|
9646
|
+
if (Boolean(node.getAttributeValue("alt")) || Boolean(node.hasAttribute("alt") && this.options.allowEmpty)) {
|
|
9647
|
+
return;
|
|
9648
|
+
}
|
|
9649
|
+
for (const attr of this.options.alias) {
|
|
9650
|
+
if (node.getAttribute(attr)) {
|
|
9651
|
+
return;
|
|
9652
|
+
}
|
|
9653
|
+
}
|
|
9654
|
+
const tag = node.annotatedName;
|
|
9655
|
+
if (node.hasAttribute("alt")) {
|
|
9656
|
+
const attr = node.getAttribute("alt");
|
|
9657
|
+
this.report(node, `${tag} cannot have empty "alt" attribute`, attr.keyLocation);
|
|
9658
|
+
} else {
|
|
9659
|
+
this.report(node, `${tag} is missing required "alt" attribute`, node.location);
|
|
9660
|
+
}
|
|
9835
9661
|
}
|
|
9836
9662
|
}
|
|
9837
9663
|
|
|
9838
|
-
|
|
9839
|
-
|
|
9840
|
-
|
|
9841
|
-
|
|
9842
|
-
|
|
9664
|
+
var _a;
|
|
9665
|
+
const { enum: validScopes } = (_a = html5.th.attributes) == null ? void 0 : _a.scope;
|
|
9666
|
+
const joinedScopes = naturalJoin(validScopes);
|
|
9667
|
+
class H63 extends Rule {
|
|
9668
|
+
documentation() {
|
|
9669
|
+
return {
|
|
9670
|
+
description: "H63: Using the scope attribute to associate header cells and data cells in data tables",
|
|
9671
|
+
url: "https://html-validate.org/rules/wcag/h63.html"
|
|
9843
9672
|
};
|
|
9844
|
-
if (tagName) {
|
|
9845
|
-
doc.description = `<${tagName}> is a void element and must not have content or end tag.`;
|
|
9846
|
-
}
|
|
9847
|
-
return doc;
|
|
9848
9673
|
}
|
|
9849
9674
|
setup() {
|
|
9850
|
-
this.on("tag:
|
|
9675
|
+
this.on("tag:ready", (event) => {
|
|
9851
9676
|
const node = event.target;
|
|
9852
|
-
if (
|
|
9677
|
+
if (node.tagName !== "th") {
|
|
9853
9678
|
return;
|
|
9854
9679
|
}
|
|
9855
|
-
|
|
9680
|
+
const scope = node.getAttribute("scope");
|
|
9681
|
+
const value = scope == null ? void 0 : scope.value;
|
|
9682
|
+
if (value instanceof DynamicValue) {
|
|
9856
9683
|
return;
|
|
9857
9684
|
}
|
|
9858
|
-
if (
|
|
9859
|
-
|
|
9860
|
-
null,
|
|
9861
|
-
`End tag for <${node.tagName}> must be omitted`,
|
|
9862
|
-
node.location,
|
|
9863
|
-
node.tagName
|
|
9864
|
-
);
|
|
9685
|
+
if (value && validScopes.includes(value)) {
|
|
9686
|
+
return;
|
|
9865
9687
|
}
|
|
9688
|
+
const message = `<th> element must have a valid scope attribute: ${joinedScopes}`;
|
|
9689
|
+
const location = (scope == null ? void 0 : scope.valueLocation) ?? (scope == null ? void 0 : scope.keyLocation) ?? node.location;
|
|
9690
|
+
this.report(node, message, location);
|
|
9866
9691
|
});
|
|
9867
9692
|
}
|
|
9868
9693
|
}
|
|
9869
9694
|
|
|
9870
|
-
|
|
9871
|
-
|
|
9872
|
-
};
|
|
9873
|
-
class VoidStyle extends Rule {
|
|
9874
|
-
constructor(options) {
|
|
9875
|
-
super({ ...defaults$2, ...options });
|
|
9876
|
-
this.style = parseStyle(this.options.style);
|
|
9877
|
-
}
|
|
9878
|
-
static schema() {
|
|
9879
|
-
return {
|
|
9880
|
-
style: {
|
|
9881
|
-
enum: ["omit", "selfclose", "selfclosing"],
|
|
9882
|
-
type: "string"
|
|
9883
|
-
}
|
|
9884
|
-
};
|
|
9885
|
-
}
|
|
9886
|
-
documentation(context) {
|
|
9887
|
-
const [desc, end] = styleDescription(context.style);
|
|
9695
|
+
class H67 extends Rule {
|
|
9696
|
+
documentation() {
|
|
9888
9697
|
return {
|
|
9889
|
-
description:
|
|
9890
|
-
url: "https://html-validate.org/rules/
|
|
9698
|
+
description: "A decorative image cannot have a title attribute. Either remove `title` or add a descriptive `alt` text.",
|
|
9699
|
+
url: "https://html-validate.org/rules/wcag/h67.html"
|
|
9891
9700
|
};
|
|
9892
9701
|
}
|
|
9893
9702
|
setup() {
|
|
9894
9703
|
this.on("tag:end", (event) => {
|
|
9895
|
-
const
|
|
9896
|
-
if (
|
|
9897
|
-
|
|
9704
|
+
const node = event.target;
|
|
9705
|
+
if (!node || node.tagName !== "img") {
|
|
9706
|
+
return;
|
|
9707
|
+
}
|
|
9708
|
+
const title = node.getAttribute("title");
|
|
9709
|
+
if (!title || title.value === "") {
|
|
9710
|
+
return;
|
|
9711
|
+
}
|
|
9712
|
+
const alt = node.getAttributeValue("alt");
|
|
9713
|
+
if (alt && alt !== "") {
|
|
9714
|
+
return;
|
|
9898
9715
|
}
|
|
9716
|
+
this.report(node, "<img> with empty alt text cannot have title attribute", title.keyLocation);
|
|
9899
9717
|
});
|
|
9900
9718
|
}
|
|
9901
|
-
validateActive(node) {
|
|
9902
|
-
if (!node.voidElement) {
|
|
9903
|
-
return;
|
|
9904
|
-
}
|
|
9905
|
-
if (this.shouldBeOmitted(node)) {
|
|
9906
|
-
this.reportError(
|
|
9907
|
-
node,
|
|
9908
|
-
`Expected omitted end tag <${node.tagName}> instead of self-closing element <${node.tagName}/>`
|
|
9909
|
-
);
|
|
9910
|
-
}
|
|
9911
|
-
if (this.shouldBeSelfClosed(node)) {
|
|
9912
|
-
this.reportError(
|
|
9913
|
-
node,
|
|
9914
|
-
`Expected self-closing element <${node.tagName}/> instead of omitted end-tag <${node.tagName}>`
|
|
9915
|
-
);
|
|
9916
|
-
}
|
|
9917
|
-
}
|
|
9918
|
-
reportError(node, message) {
|
|
9919
|
-
const context = {
|
|
9920
|
-
style: this.style,
|
|
9921
|
-
tagName: node.tagName
|
|
9922
|
-
};
|
|
9923
|
-
super.report(node, message, null, context);
|
|
9924
|
-
}
|
|
9925
|
-
shouldBeOmitted(node) {
|
|
9926
|
-
return this.style === 1 /* AlwaysOmit */ && node.closed === NodeClosed.VoidSelfClosed;
|
|
9927
|
-
}
|
|
9928
|
-
shouldBeSelfClosed(node) {
|
|
9929
|
-
return this.style === 2 /* AlwaysSelfclose */ && node.closed === NodeClosed.VoidOmitted;
|
|
9930
|
-
}
|
|
9931
|
-
}
|
|
9932
|
-
function parseStyle(name) {
|
|
9933
|
-
switch (name) {
|
|
9934
|
-
case "omit":
|
|
9935
|
-
return 1 /* AlwaysOmit */;
|
|
9936
|
-
case "selfclose":
|
|
9937
|
-
case "selfclosing":
|
|
9938
|
-
return 2 /* AlwaysSelfclose */;
|
|
9939
|
-
default:
|
|
9940
|
-
throw new Error(`Invalid style "${name}" for "void-style" rule`);
|
|
9941
|
-
}
|
|
9942
|
-
}
|
|
9943
|
-
function styleDescription(style) {
|
|
9944
|
-
switch (style) {
|
|
9945
|
-
case 1 /* AlwaysOmit */:
|
|
9946
|
-
return ["omit end tag", ""];
|
|
9947
|
-
case 2 /* AlwaysSelfclose */:
|
|
9948
|
-
return ["be self-closed", "/"];
|
|
9949
|
-
default:
|
|
9950
|
-
throw new Error(`Unknown style`);
|
|
9951
|
-
}
|
|
9952
9719
|
}
|
|
9953
9720
|
|
|
9954
|
-
class
|
|
9721
|
+
class H71 extends Rule {
|
|
9955
9722
|
documentation() {
|
|
9956
9723
|
return {
|
|
9957
|
-
description: "
|
|
9958
|
-
url: "https://html-validate.org/rules/wcag/
|
|
9724
|
+
description: "H71: Providing a description for groups of form controls using fieldset and legend elements",
|
|
9725
|
+
url: "https://html-validate.org/rules/wcag/h71.html"
|
|
9959
9726
|
};
|
|
9960
9727
|
}
|
|
9961
9728
|
setup() {
|
|
9962
9729
|
this.on("dom:ready", (event) => {
|
|
9963
|
-
const
|
|
9964
|
-
|
|
9965
|
-
|
|
9966
|
-
|
|
9967
|
-
}
|
|
9968
|
-
if (!inAccessibilityTree(link)) {
|
|
9969
|
-
continue;
|
|
9970
|
-
}
|
|
9971
|
-
const textClassification = classifyNodeText(link, { ignoreHiddenRoot: true });
|
|
9972
|
-
if (textClassification !== TextClassification.EMPTY_TEXT) {
|
|
9973
|
-
continue;
|
|
9974
|
-
}
|
|
9975
|
-
const images = link.querySelectorAll("img");
|
|
9976
|
-
if (images.some((image) => hasAltText(image))) {
|
|
9977
|
-
continue;
|
|
9978
|
-
}
|
|
9979
|
-
const labels = link.querySelectorAll("[aria-label]");
|
|
9980
|
-
if (hasAriaLabel(link) || labels.some((cur) => hasAriaLabel(cur))) {
|
|
9981
|
-
continue;
|
|
9982
|
-
}
|
|
9983
|
-
this.report(link, "Anchor link must have a text describing its purpose");
|
|
9730
|
+
const { document } = event;
|
|
9731
|
+
const fieldsets = document.querySelectorAll(this.selector);
|
|
9732
|
+
for (const fieldset of fieldsets) {
|
|
9733
|
+
this.validate(fieldset);
|
|
9984
9734
|
}
|
|
9985
9735
|
});
|
|
9986
9736
|
}
|
|
9737
|
+
validate(fieldset) {
|
|
9738
|
+
const legend = fieldset.querySelectorAll("> legend");
|
|
9739
|
+
if (legend.length === 0) {
|
|
9740
|
+
this.reportNode(fieldset);
|
|
9741
|
+
}
|
|
9742
|
+
}
|
|
9743
|
+
reportNode(node) {
|
|
9744
|
+
super.report(node, `${node.annotatedName} must have a <legend> as the first child`);
|
|
9745
|
+
}
|
|
9746
|
+
get selector() {
|
|
9747
|
+
return this.getTagsDerivedFrom("fieldset").join(",");
|
|
9748
|
+
}
|
|
9749
|
+
}
|
|
9750
|
+
|
|
9751
|
+
const bundledRules$1 = {
|
|
9752
|
+
"wcag/h30": H30,
|
|
9753
|
+
"wcag/h32": H32,
|
|
9754
|
+
"wcag/h36": H36,
|
|
9755
|
+
"wcag/h37": H37,
|
|
9756
|
+
"wcag/h63": H63,
|
|
9757
|
+
"wcag/h67": H67,
|
|
9758
|
+
"wcag/h71": H71
|
|
9759
|
+
};
|
|
9760
|
+
|
|
9761
|
+
const bundledRules = {
|
|
9762
|
+
"allowed-links": AllowedLinks,
|
|
9763
|
+
"area-alt": AreaAlt,
|
|
9764
|
+
"aria-hidden-body": AriaHiddenBody,
|
|
9765
|
+
"aria-label-misuse": AriaLabelMisuse,
|
|
9766
|
+
"attr-case": AttrCase,
|
|
9767
|
+
"attr-delimiter": AttrDelimiter,
|
|
9768
|
+
"attr-pattern": AttrPattern,
|
|
9769
|
+
"attr-quotes": AttrQuotes,
|
|
9770
|
+
"attr-spacing": AttrSpacing,
|
|
9771
|
+
"attribute-allowed-values": AttributeAllowedValues,
|
|
9772
|
+
"attribute-boolean-style": AttributeBooleanStyle,
|
|
9773
|
+
"attribute-empty-style": AttributeEmptyStyle,
|
|
9774
|
+
"attribute-misuse": AttributeMisuse,
|
|
9775
|
+
"class-pattern": ClassPattern,
|
|
9776
|
+
"close-attr": CloseAttr,
|
|
9777
|
+
"close-order": CloseOrder,
|
|
9778
|
+
deprecated: Deprecated,
|
|
9779
|
+
"deprecated-rule": DeprecatedRule,
|
|
9780
|
+
"doctype-html": NoStyleTag$1,
|
|
9781
|
+
"doctype-style": DoctypeStyle,
|
|
9782
|
+
"element-case": ElementCase,
|
|
9783
|
+
"element-name": ElementName,
|
|
9784
|
+
"element-permitted-content": ElementPermittedContent,
|
|
9785
|
+
"element-permitted-occurrences": ElementPermittedOccurrences,
|
|
9786
|
+
"element-permitted-order": ElementPermittedOrder,
|
|
9787
|
+
"element-permitted-parent": ElementPermittedParent,
|
|
9788
|
+
"element-required-ancestor": ElementRequiredAncestor,
|
|
9789
|
+
"element-required-attributes": ElementRequiredAttributes,
|
|
9790
|
+
"element-required-content": ElementRequiredContent,
|
|
9791
|
+
"empty-heading": EmptyHeading,
|
|
9792
|
+
"empty-title": EmptyTitle,
|
|
9793
|
+
"form-dup-name": FormDupName,
|
|
9794
|
+
"heading-level": HeadingLevel,
|
|
9795
|
+
"hidden-focusable": HiddenFocusable,
|
|
9796
|
+
"id-pattern": IdPattern,
|
|
9797
|
+
"input-attributes": InputAttributes,
|
|
9798
|
+
"input-missing-label": InputMissingLabel,
|
|
9799
|
+
"long-title": LongTitle,
|
|
9800
|
+
"map-dup-name": MapDupName,
|
|
9801
|
+
"map-id-name": MapIdName,
|
|
9802
|
+
"meta-refresh": MetaRefresh,
|
|
9803
|
+
"missing-doctype": MissingDoctype,
|
|
9804
|
+
"multiple-labeled-controls": MultipleLabeledControls,
|
|
9805
|
+
"name-pattern": NamePattern,
|
|
9806
|
+
"no-abstract-role": NoAbstractRole,
|
|
9807
|
+
"no-autoplay": NoAutoplay,
|
|
9808
|
+
"no-conditional-comment": NoConditionalComment,
|
|
9809
|
+
"no-deprecated-attr": NoDeprecatedAttr,
|
|
9810
|
+
"no-dup-attr": NoDupAttr,
|
|
9811
|
+
"no-dup-class": NoDupClass,
|
|
9812
|
+
"no-dup-id": NoDupID,
|
|
9813
|
+
"no-implicit-button-type": NoImplicitButtonType,
|
|
9814
|
+
"no-implicit-input-type": NoImplicitInputType,
|
|
9815
|
+
"no-implicit-close": NoImplicitClose,
|
|
9816
|
+
"no-inline-style": NoInlineStyle,
|
|
9817
|
+
"no-missing-references": NoMissingReferences,
|
|
9818
|
+
"no-multiple-main": NoMultipleMain,
|
|
9819
|
+
"no-raw-characters": NoRawCharacters,
|
|
9820
|
+
"no-redundant-aria-label": NoRedundantAriaLabel,
|
|
9821
|
+
"no-redundant-for": NoRedundantFor,
|
|
9822
|
+
"no-redundant-role": NoRedundantRole,
|
|
9823
|
+
"no-self-closing": NoSelfClosing,
|
|
9824
|
+
"no-style-tag": NoStyleTag,
|
|
9825
|
+
"no-trailing-whitespace": NoTrailingWhitespace,
|
|
9826
|
+
"no-unknown-elements": NoUnknownElements,
|
|
9827
|
+
"no-unused-disable": NoUnusedDisable,
|
|
9828
|
+
"no-utf8-bom": NoUtf8Bom,
|
|
9829
|
+
"prefer-button": PreferButton,
|
|
9830
|
+
"prefer-native-element": PreferNativeElement,
|
|
9831
|
+
"prefer-tbody": PreferTbody,
|
|
9832
|
+
"require-csp-nonce": RequireCSPNonce,
|
|
9833
|
+
"require-sri": RequireSri,
|
|
9834
|
+
"script-element": ScriptElement,
|
|
9835
|
+
"script-type": ScriptType,
|
|
9836
|
+
"svg-focusable": SvgFocusable,
|
|
9837
|
+
"tel-non-breaking": TelNonBreaking,
|
|
9838
|
+
"text-content": TextContent,
|
|
9839
|
+
"unique-landmark": UniqueLandmark,
|
|
9840
|
+
"unrecognized-char-ref": UnknownCharReference,
|
|
9841
|
+
"valid-autocomplete": ValidAutocomplete,
|
|
9842
|
+
"valid-id": ValidID,
|
|
9843
|
+
"void-content": VoidContent,
|
|
9844
|
+
"void-style": VoidStyle,
|
|
9845
|
+
...bundledRules$1
|
|
9846
|
+
};
|
|
9847
|
+
|
|
9848
|
+
const ruleIds = new Set(Object.keys(bundledRules));
|
|
9849
|
+
function ruleExists(ruleId) {
|
|
9850
|
+
return ruleIds.has(ruleId);
|
|
9987
9851
|
}
|
|
9988
9852
|
|
|
9989
|
-
|
|
9990
|
-
|
|
9991
|
-
|
|
9992
|
-
|
|
9993
|
-
|
|
9994
|
-
|
|
9995
|
-
}
|
|
9996
|
-
setup() {
|
|
9997
|
-
const formTags = this.getTagsWithProperty("form");
|
|
9998
|
-
const formSelector = formTags.join(",");
|
|
9999
|
-
this.on("dom:ready", (event) => {
|
|
10000
|
-
const { document } = event;
|
|
10001
|
-
const forms = document.querySelectorAll(formSelector);
|
|
10002
|
-
for (const form of forms) {
|
|
10003
|
-
if (hasNestedSubmit(form)) {
|
|
10004
|
-
continue;
|
|
10005
|
-
}
|
|
10006
|
-
if (hasAssociatedSubmit(document, form)) {
|
|
10007
|
-
continue;
|
|
10008
|
-
}
|
|
10009
|
-
this.report(form, `<${form.tagName}> element must have a submit button`);
|
|
10010
|
-
}
|
|
10011
|
-
});
|
|
9853
|
+
function depthFirst(root, callback) {
|
|
9854
|
+
if (root instanceof DOMTree) {
|
|
9855
|
+
if (root.readyState !== "complete") {
|
|
9856
|
+
throw new Error(`Cannot call walk.depthFirst(..) before document is ready`);
|
|
9857
|
+
}
|
|
9858
|
+
root = root.root;
|
|
10012
9859
|
}
|
|
10013
|
-
|
|
10014
|
-
|
|
10015
|
-
|
|
10016
|
-
|
|
10017
|
-
}
|
|
10018
|
-
function isAssociated(id, node) {
|
|
10019
|
-
const form = node.getAttribute("form");
|
|
10020
|
-
return Boolean(form == null ? void 0 : form.valueMatches(id, true));
|
|
10021
|
-
}
|
|
10022
|
-
function hasNestedSubmit(form) {
|
|
10023
|
-
const matches = form.querySelectorAll("button,input").filter(isSubmit).filter((node) => !node.hasAttribute("form"));
|
|
10024
|
-
return matches.length > 0;
|
|
10025
|
-
}
|
|
10026
|
-
function hasAssociatedSubmit(document, form) {
|
|
10027
|
-
const { id } = form;
|
|
10028
|
-
if (!id) {
|
|
10029
|
-
return false;
|
|
9860
|
+
function visit(node) {
|
|
9861
|
+
node.childElements.forEach(visit);
|
|
9862
|
+
if (!node.isRootElement()) {
|
|
9863
|
+
callback(node);
|
|
9864
|
+
}
|
|
10030
9865
|
}
|
|
10031
|
-
|
|
10032
|
-
return matches.length > 0;
|
|
9866
|
+
visit(root);
|
|
10033
9867
|
}
|
|
9868
|
+
const walk = {
|
|
9869
|
+
depthFirst
|
|
9870
|
+
};
|
|
10034
9871
|
|
|
10035
|
-
class
|
|
10036
|
-
|
|
10037
|
-
|
|
10038
|
-
|
|
10039
|
-
|
|
10040
|
-
|
|
10041
|
-
|
|
10042
|
-
|
|
10043
|
-
|
|
9872
|
+
class DOMTree {
|
|
9873
|
+
/**
|
|
9874
|
+
* @internal
|
|
9875
|
+
*/
|
|
9876
|
+
constructor(location) {
|
|
9877
|
+
this.root = HtmlElement.rootNode(location);
|
|
9878
|
+
this.active = this.root;
|
|
9879
|
+
this.doctype = null;
|
|
9880
|
+
this._readyState = "loading";
|
|
10044
9881
|
}
|
|
10045
|
-
|
|
10046
|
-
|
|
10047
|
-
|
|
10048
|
-
|
|
10049
|
-
|
|
10050
|
-
|
|
10051
|
-
|
|
10052
|
-
|
|
10053
|
-
|
|
10054
|
-
|
|
10055
|
-
|
|
10056
|
-
|
|
10057
|
-
|
|
10058
|
-
|
|
10059
|
-
|
|
10060
|
-
|
|
10061
|
-
|
|
10062
|
-
|
|
10063
|
-
|
|
10064
|
-
|
|
9882
|
+
/**
|
|
9883
|
+
* @internal
|
|
9884
|
+
*/
|
|
9885
|
+
pushActive(node) {
|
|
9886
|
+
this.active = node;
|
|
9887
|
+
}
|
|
9888
|
+
/**
|
|
9889
|
+
* @internal
|
|
9890
|
+
*/
|
|
9891
|
+
popActive() {
|
|
9892
|
+
if (this.active.isRootElement()) {
|
|
9893
|
+
return;
|
|
9894
|
+
}
|
|
9895
|
+
this.active = this.active.parent ?? this.root;
|
|
9896
|
+
}
|
|
9897
|
+
/**
|
|
9898
|
+
* @internal
|
|
9899
|
+
*/
|
|
9900
|
+
getActive() {
|
|
9901
|
+
return this.active;
|
|
9902
|
+
}
|
|
9903
|
+
/**
|
|
9904
|
+
* Describes the loading state of the document.
|
|
9905
|
+
*
|
|
9906
|
+
* When `"loading"` it is still not safe to use functions such as
|
|
9907
|
+
* `querySelector` or presence of attributes, child nodes, etc.
|
|
9908
|
+
*/
|
|
9909
|
+
get readyState() {
|
|
9910
|
+
return this._readyState;
|
|
9911
|
+
}
|
|
9912
|
+
/**
|
|
9913
|
+
* Resolve dynamic meta expressions.
|
|
9914
|
+
*
|
|
9915
|
+
* @internal
|
|
9916
|
+
*/
|
|
9917
|
+
resolveMeta(table) {
|
|
9918
|
+
this._readyState = "complete";
|
|
9919
|
+
walk.depthFirst(this, (node) => {
|
|
9920
|
+
table.resolve(node);
|
|
10065
9921
|
});
|
|
10066
9922
|
}
|
|
9923
|
+
getElementsByTagName(tagName) {
|
|
9924
|
+
return this.root.getElementsByTagName(tagName);
|
|
9925
|
+
}
|
|
9926
|
+
/**
|
|
9927
|
+
* @deprecated use utility function `walk.depthFirst(..)` instead (since 8.21.0).
|
|
9928
|
+
*/
|
|
9929
|
+
visitDepthFirst(callback) {
|
|
9930
|
+
walk.depthFirst(this, callback);
|
|
9931
|
+
}
|
|
9932
|
+
/**
|
|
9933
|
+
* @deprecated use `querySelector(..)` instead (since 8.21.0)
|
|
9934
|
+
*/
|
|
9935
|
+
find(callback) {
|
|
9936
|
+
return this.root.find(callback);
|
|
9937
|
+
}
|
|
9938
|
+
querySelector(selector) {
|
|
9939
|
+
return this.root.querySelector(selector);
|
|
9940
|
+
}
|
|
9941
|
+
querySelectorAll(selector) {
|
|
9942
|
+
return this.root.querySelectorAll(selector);
|
|
9943
|
+
}
|
|
10067
9944
|
}
|
|
10068
9945
|
|
|
10069
|
-
const
|
|
10070
|
-
|
|
10071
|
-
|
|
10072
|
-
|
|
10073
|
-
|
|
10074
|
-
|
|
10075
|
-
|
|
10076
|
-
|
|
10077
|
-
|
|
9946
|
+
const allowedKeys = ["exclude"];
|
|
9947
|
+
class Validator {
|
|
9948
|
+
/**
|
|
9949
|
+
* Test if element is used in a proper context.
|
|
9950
|
+
*
|
|
9951
|
+
* @param node - Element to test.
|
|
9952
|
+
* @param rules - List of rules.
|
|
9953
|
+
* @returns `true` if element passes all tests.
|
|
9954
|
+
*/
|
|
9955
|
+
static validatePermitted(node, rules) {
|
|
9956
|
+
if (!rules) {
|
|
9957
|
+
return true;
|
|
10078
9958
|
}
|
|
9959
|
+
return rules.some((rule) => {
|
|
9960
|
+
return Validator.validatePermittedRule(node, rule);
|
|
9961
|
+
});
|
|
10079
9962
|
}
|
|
10080
|
-
|
|
10081
|
-
|
|
10082
|
-
|
|
10083
|
-
|
|
10084
|
-
|
|
10085
|
-
|
|
10086
|
-
|
|
10087
|
-
|
|
10088
|
-
|
|
10089
|
-
|
|
10090
|
-
|
|
10091
|
-
|
|
9963
|
+
/**
|
|
9964
|
+
* Test if an element is used the correct amount of times.
|
|
9965
|
+
*
|
|
9966
|
+
* For instance, a `<table>` element can only contain a single `<tbody>`
|
|
9967
|
+
* child. If multiple `<tbody>` exists this test will fail both nodes.
|
|
9968
|
+
* Note that this is called on the parent but will fail the children violating
|
|
9969
|
+
* the rule.
|
|
9970
|
+
*
|
|
9971
|
+
* @param children - Array of children to validate.
|
|
9972
|
+
* @param rules - List of rules of the parent element.
|
|
9973
|
+
* @returns `true` if the parent element of the children passes the test.
|
|
9974
|
+
*/
|
|
9975
|
+
static validateOccurrences(children, rules, cb) {
|
|
9976
|
+
if (!rules) {
|
|
9977
|
+
return true;
|
|
9978
|
+
}
|
|
9979
|
+
let valid = true;
|
|
9980
|
+
for (const rule of rules) {
|
|
9981
|
+
if (typeof rule !== "string") {
|
|
9982
|
+
return false;
|
|
9983
|
+
}
|
|
9984
|
+
const [, category, quantifier] = rule.match(/^(@?.*?)([?*]?)$/);
|
|
9985
|
+
const limit = category && quantifier && parseQuantifier(quantifier);
|
|
9986
|
+
if (limit) {
|
|
9987
|
+
const siblings = children.filter(
|
|
9988
|
+
(cur) => Validator.validatePermittedCategory(cur, rule, true)
|
|
9989
|
+
);
|
|
9990
|
+
if (siblings.length > limit) {
|
|
9991
|
+
for (const child of siblings.slice(limit)) {
|
|
9992
|
+
cb(child, category);
|
|
10092
9993
|
}
|
|
10093
|
-
|
|
10094
|
-
|
|
10095
|
-
allowEmpty: {
|
|
10096
|
-
type: "boolean"
|
|
9994
|
+
valid = false;
|
|
9995
|
+
}
|
|
10097
9996
|
}
|
|
10098
|
-
}
|
|
10099
|
-
|
|
10100
|
-
documentation() {
|
|
10101
|
-
return {
|
|
10102
|
-
description: "Both HTML5 and WCAG 2.0 requires images to have a alternative text for each image.",
|
|
10103
|
-
url: "https://html-validate.org/rules/wcag/h37.html"
|
|
10104
|
-
};
|
|
9997
|
+
}
|
|
9998
|
+
return valid;
|
|
10105
9999
|
}
|
|
10106
|
-
|
|
10107
|
-
|
|
10108
|
-
|
|
10109
|
-
|
|
10110
|
-
|
|
10111
|
-
|
|
10000
|
+
/**
|
|
10001
|
+
* Validate elements order.
|
|
10002
|
+
*
|
|
10003
|
+
* Given a parent element with children and metadata containing permitted
|
|
10004
|
+
* order it will validate each children and ensure each one exists in the
|
|
10005
|
+
* specified order.
|
|
10006
|
+
*
|
|
10007
|
+
* For instance, for a `<table>` element the `<caption>` element must come
|
|
10008
|
+
* before a `<thead>` which must come before `<tbody>`.
|
|
10009
|
+
*
|
|
10010
|
+
* @param children - Array of children to validate.
|
|
10011
|
+
*/
|
|
10012
|
+
static validateOrder(children, rules, cb) {
|
|
10013
|
+
if (!rules) {
|
|
10014
|
+
return true;
|
|
10015
|
+
}
|
|
10016
|
+
let i = 0;
|
|
10017
|
+
let prev = null;
|
|
10018
|
+
for (const node of children) {
|
|
10019
|
+
const old = i;
|
|
10020
|
+
while (rules[i] && !Validator.validatePermittedCategory(node, rules[i], true)) {
|
|
10021
|
+
i++;
|
|
10022
|
+
}
|
|
10023
|
+
if (i >= rules.length) {
|
|
10024
|
+
const orderSpecified = rules.find(
|
|
10025
|
+
(cur) => Validator.validatePermittedCategory(node, cur, true)
|
|
10026
|
+
);
|
|
10027
|
+
if (orderSpecified) {
|
|
10028
|
+
cb(node, prev);
|
|
10029
|
+
return false;
|
|
10030
|
+
}
|
|
10031
|
+
i = old;
|
|
10112
10032
|
}
|
|
10033
|
+
prev = node;
|
|
10034
|
+
}
|
|
10035
|
+
return true;
|
|
10036
|
+
}
|
|
10037
|
+
/**
|
|
10038
|
+
* Validate element ancestors.
|
|
10039
|
+
*
|
|
10040
|
+
* Check if an element has the required set of elements. At least one of the
|
|
10041
|
+
* selectors must match.
|
|
10042
|
+
*/
|
|
10043
|
+
static validateAncestors(node, rules) {
|
|
10044
|
+
if (!rules || rules.length === 0) {
|
|
10045
|
+
return true;
|
|
10046
|
+
}
|
|
10047
|
+
return rules.some((rule) => node.closest(rule));
|
|
10048
|
+
}
|
|
10049
|
+
/**
|
|
10050
|
+
* Validate element required content.
|
|
10051
|
+
*
|
|
10052
|
+
* Check if an element has the required set of elements. At least one of the
|
|
10053
|
+
* selectors must match.
|
|
10054
|
+
*
|
|
10055
|
+
* Returns `[]` when valid or a list of required but missing tagnames or
|
|
10056
|
+
* categories.
|
|
10057
|
+
*/
|
|
10058
|
+
static validateRequiredContent(node, rules) {
|
|
10059
|
+
if (!rules || rules.length === 0) {
|
|
10060
|
+
return [];
|
|
10061
|
+
}
|
|
10062
|
+
return rules.filter((tagName) => {
|
|
10063
|
+
const haveMatchingChild = node.childElements.some(
|
|
10064
|
+
(child) => Validator.validatePermittedCategory(child, tagName, false)
|
|
10065
|
+
);
|
|
10066
|
+
return !haveMatchingChild;
|
|
10113
10067
|
});
|
|
10114
10068
|
}
|
|
10115
|
-
|
|
10116
|
-
|
|
10117
|
-
|
|
10069
|
+
/**
|
|
10070
|
+
* Test if an attribute has an allowed value and/or format.
|
|
10071
|
+
*
|
|
10072
|
+
* @param attr - Attribute to test.
|
|
10073
|
+
* @param rules - Element attribute metadta.
|
|
10074
|
+
* @returns `true` if attribute passes all tests.
|
|
10075
|
+
*/
|
|
10076
|
+
static validateAttribute(attr, rules) {
|
|
10077
|
+
const rule = rules[attr.key];
|
|
10078
|
+
if (!rule) {
|
|
10079
|
+
return true;
|
|
10118
10080
|
}
|
|
10119
|
-
|
|
10120
|
-
|
|
10081
|
+
const value = attr.value;
|
|
10082
|
+
if (value instanceof DynamicValue) {
|
|
10083
|
+
return true;
|
|
10121
10084
|
}
|
|
10122
|
-
|
|
10123
|
-
|
|
10124
|
-
|
|
10125
|
-
}
|
|
10085
|
+
const empty = value === null || value === "";
|
|
10086
|
+
if (rule.boolean) {
|
|
10087
|
+
return empty || value === attr.key;
|
|
10126
10088
|
}
|
|
10127
|
-
|
|
10128
|
-
|
|
10129
|
-
const attr = node.getAttribute("alt");
|
|
10130
|
-
this.report(node, `${tag} cannot have empty "alt" attribute`, attr.keyLocation);
|
|
10131
|
-
} else {
|
|
10132
|
-
this.report(node, `${tag} is missing required "alt" attribute`, node.location);
|
|
10089
|
+
if (rule.omit && empty) {
|
|
10090
|
+
return true;
|
|
10133
10091
|
}
|
|
10092
|
+
if (rule.list) {
|
|
10093
|
+
const tokens = new DOMTokenList(value, attr.valueLocation);
|
|
10094
|
+
return tokens.every((token) => {
|
|
10095
|
+
return this.validateAttributeValue(token, rule);
|
|
10096
|
+
});
|
|
10097
|
+
}
|
|
10098
|
+
return this.validateAttributeValue(value, rule);
|
|
10134
10099
|
}
|
|
10135
|
-
|
|
10136
|
-
|
|
10137
|
-
|
|
10138
|
-
|
|
10139
|
-
|
|
10140
|
-
|
|
10141
|
-
|
|
10142
|
-
|
|
10143
|
-
|
|
10144
|
-
|
|
10145
|
-
|
|
10146
|
-
|
|
10147
|
-
|
|
10148
|
-
this.on("tag:ready", (event) => {
|
|
10149
|
-
const node = event.target;
|
|
10150
|
-
if (node.tagName !== "th") {
|
|
10151
|
-
return;
|
|
10152
|
-
}
|
|
10153
|
-
const scope = node.getAttribute("scope");
|
|
10154
|
-
const value = scope == null ? void 0 : scope.value;
|
|
10155
|
-
if (value instanceof DynamicValue) {
|
|
10156
|
-
return;
|
|
10157
|
-
}
|
|
10158
|
-
if (value && validScopes.includes(value)) {
|
|
10159
|
-
return;
|
|
10160
|
-
}
|
|
10161
|
-
const message = `<th> element must have a valid scope attribute: ${joinedScopes}`;
|
|
10162
|
-
const location = (scope == null ? void 0 : scope.valueLocation) ?? (scope == null ? void 0 : scope.keyLocation) ?? node.location;
|
|
10163
|
-
this.report(node, message, location);
|
|
10164
|
-
});
|
|
10165
|
-
}
|
|
10166
|
-
}
|
|
10167
|
-
|
|
10168
|
-
class H67 extends Rule {
|
|
10169
|
-
documentation() {
|
|
10170
|
-
return {
|
|
10171
|
-
description: "A decorative image cannot have a title attribute. Either remove `title` or add a descriptive `alt` text.",
|
|
10172
|
-
url: "https://html-validate.org/rules/wcag/h67.html"
|
|
10173
|
-
};
|
|
10174
|
-
}
|
|
10175
|
-
setup() {
|
|
10176
|
-
this.on("tag:end", (event) => {
|
|
10177
|
-
const node = event.target;
|
|
10178
|
-
if (!node || node.tagName !== "img") {
|
|
10179
|
-
return;
|
|
10180
|
-
}
|
|
10181
|
-
const title = node.getAttribute("title");
|
|
10182
|
-
if (!title || title.value === "") {
|
|
10183
|
-
return;
|
|
10184
|
-
}
|
|
10185
|
-
const alt = node.getAttributeValue("alt");
|
|
10186
|
-
if (alt && alt !== "") {
|
|
10187
|
-
return;
|
|
10100
|
+
static validateAttributeValue(value, rule) {
|
|
10101
|
+
if (!rule.enum) {
|
|
10102
|
+
return true;
|
|
10103
|
+
}
|
|
10104
|
+
if (value === null) {
|
|
10105
|
+
return false;
|
|
10106
|
+
}
|
|
10107
|
+
const caseInsensitiveValue = value.toLowerCase();
|
|
10108
|
+
return rule.enum.some((entry) => {
|
|
10109
|
+
if (entry instanceof RegExp) {
|
|
10110
|
+
return !!value.match(entry);
|
|
10111
|
+
} else {
|
|
10112
|
+
return caseInsensitiveValue === entry;
|
|
10188
10113
|
}
|
|
10189
|
-
this.report(node, "<img> with empty alt text cannot have title attribute", title.keyLocation);
|
|
10190
10114
|
});
|
|
10191
10115
|
}
|
|
10192
|
-
|
|
10193
|
-
|
|
10194
|
-
|
|
10195
|
-
|
|
10196
|
-
|
|
10197
|
-
|
|
10198
|
-
|
|
10199
|
-
}
|
|
10200
|
-
|
|
10201
|
-
|
|
10202
|
-
|
|
10203
|
-
|
|
10204
|
-
|
|
10205
|
-
|
|
10206
|
-
|
|
10116
|
+
static validatePermittedRule(node, rule, isExclude = false) {
|
|
10117
|
+
if (typeof rule === "string") {
|
|
10118
|
+
return Validator.validatePermittedCategory(node, rule, !isExclude);
|
|
10119
|
+
} else if (Array.isArray(rule)) {
|
|
10120
|
+
return rule.every((inner) => {
|
|
10121
|
+
return Validator.validatePermittedRule(node, inner, isExclude);
|
|
10122
|
+
});
|
|
10123
|
+
} else {
|
|
10124
|
+
validateKeys(rule);
|
|
10125
|
+
if (rule.exclude) {
|
|
10126
|
+
if (Array.isArray(rule.exclude)) {
|
|
10127
|
+
return !rule.exclude.some((inner) => {
|
|
10128
|
+
return Validator.validatePermittedRule(node, inner, true);
|
|
10129
|
+
});
|
|
10130
|
+
} else {
|
|
10131
|
+
return !Validator.validatePermittedRule(node, rule.exclude, true);
|
|
10132
|
+
}
|
|
10133
|
+
} else {
|
|
10134
|
+
return true;
|
|
10207
10135
|
}
|
|
10208
|
-
});
|
|
10209
|
-
}
|
|
10210
|
-
validate(fieldset) {
|
|
10211
|
-
const legend = fieldset.querySelectorAll("> legend");
|
|
10212
|
-
if (legend.length === 0) {
|
|
10213
|
-
this.reportNode(fieldset);
|
|
10214
10136
|
}
|
|
10215
10137
|
}
|
|
10216
|
-
|
|
10217
|
-
|
|
10138
|
+
/**
|
|
10139
|
+
* Validate node against a content category.
|
|
10140
|
+
*
|
|
10141
|
+
* When matching parent nodes against permitted parents use the superset
|
|
10142
|
+
* parameter to also match for `@flow`. E.g. if a node expects a `@phrasing`
|
|
10143
|
+
* parent it should also allow `@flow` parent since `@phrasing` is a subset of
|
|
10144
|
+
* `@flow`.
|
|
10145
|
+
*
|
|
10146
|
+
* @param node - The node to test against
|
|
10147
|
+
* @param category - Name of category with `@` prefix or tag name.
|
|
10148
|
+
* @param defaultMatch - The default return value when node categories is not known.
|
|
10149
|
+
*/
|
|
10150
|
+
/* eslint-disable-next-line complexity -- rule does not like switch */
|
|
10151
|
+
static validatePermittedCategory(node, category, defaultMatch) {
|
|
10152
|
+
const [, rawCategory] = category.match(/^(@?.*?)([?*]?)$/);
|
|
10153
|
+
if (!rawCategory.startsWith("@")) {
|
|
10154
|
+
return node.tagName === rawCategory;
|
|
10155
|
+
}
|
|
10156
|
+
if (!node.meta) {
|
|
10157
|
+
return defaultMatch;
|
|
10158
|
+
}
|
|
10159
|
+
switch (rawCategory) {
|
|
10160
|
+
case "@meta":
|
|
10161
|
+
return node.meta.metadata;
|
|
10162
|
+
case "@flow":
|
|
10163
|
+
return node.meta.flow;
|
|
10164
|
+
case "@sectioning":
|
|
10165
|
+
return node.meta.sectioning;
|
|
10166
|
+
case "@heading":
|
|
10167
|
+
return node.meta.heading;
|
|
10168
|
+
case "@phrasing":
|
|
10169
|
+
return node.meta.phrasing;
|
|
10170
|
+
case "@embedded":
|
|
10171
|
+
return node.meta.embedded;
|
|
10172
|
+
case "@interactive":
|
|
10173
|
+
return node.meta.interactive;
|
|
10174
|
+
case "@script":
|
|
10175
|
+
return Boolean(node.meta.scriptSupporting);
|
|
10176
|
+
case "@form":
|
|
10177
|
+
return Boolean(node.meta.form);
|
|
10178
|
+
default:
|
|
10179
|
+
throw new Error(`Invalid content category "${category}"`);
|
|
10180
|
+
}
|
|
10218
10181
|
}
|
|
10219
|
-
|
|
10220
|
-
|
|
10182
|
+
}
|
|
10183
|
+
function validateKeys(rule) {
|
|
10184
|
+
for (const key of Object.keys(rule)) {
|
|
10185
|
+
if (!allowedKeys.includes(key)) {
|
|
10186
|
+
const str = JSON.stringify(rule);
|
|
10187
|
+
throw new Error(`Permitted rule "${str}" contains unknown property "${key}"`);
|
|
10188
|
+
}
|
|
10221
10189
|
}
|
|
10222
10190
|
}
|
|
10223
|
-
|
|
10224
|
-
|
|
10225
|
-
|
|
10226
|
-
|
|
10227
|
-
|
|
10228
|
-
|
|
10229
|
-
|
|
10230
|
-
|
|
10231
|
-
|
|
10191
|
+
function parseQuantifier(quantifier) {
|
|
10192
|
+
switch (quantifier) {
|
|
10193
|
+
case "?":
|
|
10194
|
+
return 1;
|
|
10195
|
+
case "*":
|
|
10196
|
+
return null;
|
|
10197
|
+
default:
|
|
10198
|
+
throw new Error(`Invalid quantifier "${quantifier}" used`);
|
|
10199
|
+
}
|
|
10200
|
+
}
|
|
10201
|
+
|
|
10202
|
+
const $schema = "http://json-schema.org/draft-06/schema#";
|
|
10203
|
+
const $id = "https://html-validate.org/schemas/config.json";
|
|
10204
|
+
const type = "object";
|
|
10205
|
+
const additionalProperties = false;
|
|
10206
|
+
const properties = {
|
|
10207
|
+
$schema: {
|
|
10208
|
+
type: "string"
|
|
10209
|
+
},
|
|
10210
|
+
root: {
|
|
10211
|
+
type: "boolean",
|
|
10212
|
+
title: "Mark as root configuration",
|
|
10213
|
+
description: "If this is set to true no further configurations will be searched.",
|
|
10214
|
+
"default": false
|
|
10215
|
+
},
|
|
10216
|
+
"extends": {
|
|
10217
|
+
type: "array",
|
|
10218
|
+
items: {
|
|
10219
|
+
type: "string"
|
|
10220
|
+
},
|
|
10221
|
+
title: "Configurations to extend",
|
|
10222
|
+
description: "Array of shareable or builtin configurations to extend."
|
|
10223
|
+
},
|
|
10224
|
+
elements: {
|
|
10225
|
+
type: "array",
|
|
10226
|
+
items: {
|
|
10227
|
+
anyOf: [
|
|
10228
|
+
{
|
|
10229
|
+
type: "string"
|
|
10230
|
+
},
|
|
10231
|
+
{
|
|
10232
|
+
type: "object"
|
|
10233
|
+
}
|
|
10234
|
+
]
|
|
10235
|
+
},
|
|
10236
|
+
title: "Element metadata to load",
|
|
10237
|
+
description: "Array of modules, plugins or files to load element metadata from. Use <rootDir> to refer to the folder with the package.json file.",
|
|
10238
|
+
examples: [
|
|
10239
|
+
[
|
|
10240
|
+
"html-validate:recommended",
|
|
10241
|
+
"plugin:recommended",
|
|
10242
|
+
"module",
|
|
10243
|
+
"./local-file.json"
|
|
10244
|
+
]
|
|
10245
|
+
]
|
|
10246
|
+
},
|
|
10247
|
+
plugins: {
|
|
10248
|
+
type: "array",
|
|
10249
|
+
items: {
|
|
10250
|
+
anyOf: [
|
|
10251
|
+
{
|
|
10252
|
+
type: "string"
|
|
10253
|
+
},
|
|
10254
|
+
{
|
|
10255
|
+
type: "object"
|
|
10256
|
+
}
|
|
10257
|
+
]
|
|
10258
|
+
},
|
|
10259
|
+
title: "Plugins to load",
|
|
10260
|
+
description: "Array of plugins load. Use <rootDir> to refer to the folder with the package.json file.",
|
|
10261
|
+
examples: [
|
|
10262
|
+
[
|
|
10263
|
+
"my-plugin",
|
|
10264
|
+
"./local-plugin"
|
|
10265
|
+
]
|
|
10266
|
+
]
|
|
10267
|
+
},
|
|
10268
|
+
transform: {
|
|
10269
|
+
type: "object",
|
|
10270
|
+
additionalProperties: {
|
|
10271
|
+
type: "string"
|
|
10272
|
+
},
|
|
10273
|
+
title: "File transformations to use.",
|
|
10274
|
+
description: "Object where key is regular expression to match filename and value is name of transformer.",
|
|
10275
|
+
examples: [
|
|
10276
|
+
{
|
|
10277
|
+
"^.*\\.foo$": "my-transformer",
|
|
10278
|
+
"^.*\\.bar$": "my-plugin",
|
|
10279
|
+
"^.*\\.baz$": "my-plugin:named"
|
|
10280
|
+
}
|
|
10281
|
+
]
|
|
10282
|
+
},
|
|
10283
|
+
rules: {
|
|
10284
|
+
type: "object",
|
|
10285
|
+
patternProperties: {
|
|
10286
|
+
".*": {
|
|
10287
|
+
anyOf: [
|
|
10288
|
+
{
|
|
10289
|
+
"enum": [
|
|
10290
|
+
0,
|
|
10291
|
+
1,
|
|
10292
|
+
2,
|
|
10293
|
+
"off",
|
|
10294
|
+
"warn",
|
|
10295
|
+
"error"
|
|
10296
|
+
]
|
|
10297
|
+
},
|
|
10298
|
+
{
|
|
10299
|
+
type: "array",
|
|
10300
|
+
minItems: 1,
|
|
10301
|
+
maxItems: 1,
|
|
10302
|
+
items: [
|
|
10303
|
+
{
|
|
10304
|
+
"enum": [
|
|
10305
|
+
0,
|
|
10306
|
+
1,
|
|
10307
|
+
2,
|
|
10308
|
+
"off",
|
|
10309
|
+
"warn",
|
|
10310
|
+
"error"
|
|
10311
|
+
]
|
|
10312
|
+
}
|
|
10313
|
+
]
|
|
10314
|
+
},
|
|
10315
|
+
{
|
|
10316
|
+
type: "array",
|
|
10317
|
+
minItems: 2,
|
|
10318
|
+
maxItems: 2,
|
|
10319
|
+
items: [
|
|
10320
|
+
{
|
|
10321
|
+
"enum": [
|
|
10322
|
+
0,
|
|
10323
|
+
1,
|
|
10324
|
+
2,
|
|
10325
|
+
"off",
|
|
10326
|
+
"warn",
|
|
10327
|
+
"error"
|
|
10328
|
+
]
|
|
10329
|
+
},
|
|
10330
|
+
{
|
|
10331
|
+
}
|
|
10332
|
+
]
|
|
10333
|
+
}
|
|
10334
|
+
]
|
|
10335
|
+
}
|
|
10336
|
+
},
|
|
10337
|
+
title: "Rule configuration.",
|
|
10338
|
+
description: "Enable/disable rules, set severity. Some rules have additional configuration like style or patterns to use.",
|
|
10339
|
+
examples: [
|
|
10340
|
+
{
|
|
10341
|
+
foo: "error",
|
|
10342
|
+
bar: "off",
|
|
10343
|
+
baz: [
|
|
10344
|
+
"error",
|
|
10345
|
+
{
|
|
10346
|
+
style: "camelcase"
|
|
10347
|
+
}
|
|
10348
|
+
]
|
|
10349
|
+
}
|
|
10350
|
+
]
|
|
10351
|
+
}
|
|
10352
|
+
};
|
|
10353
|
+
var configurationSchema = {
|
|
10354
|
+
$schema: $schema,
|
|
10355
|
+
$id: $id,
|
|
10356
|
+
type: type,
|
|
10357
|
+
additionalProperties: additionalProperties,
|
|
10358
|
+
properties: properties
|
|
10232
10359
|
};
|
|
10233
10360
|
|
|
10234
|
-
const
|
|
10235
|
-
|
|
10236
|
-
"area-alt": AreaAlt,
|
|
10237
|
-
"aria-hidden-body": AriaHiddenBody,
|
|
10238
|
-
"aria-label-misuse": AriaLabelMisuse,
|
|
10239
|
-
"attr-case": AttrCase,
|
|
10240
|
-
"attr-delimiter": AttrDelimiter,
|
|
10241
|
-
"attr-pattern": AttrPattern,
|
|
10242
|
-
"attr-quotes": AttrQuotes,
|
|
10243
|
-
"attr-spacing": AttrSpacing,
|
|
10244
|
-
"attribute-allowed-values": AttributeAllowedValues,
|
|
10245
|
-
"attribute-boolean-style": AttributeBooleanStyle,
|
|
10246
|
-
"attribute-empty-style": AttributeEmptyStyle,
|
|
10247
|
-
"attribute-misuse": AttributeMisuse,
|
|
10248
|
-
"class-pattern": ClassPattern,
|
|
10249
|
-
"close-attr": CloseAttr,
|
|
10250
|
-
"close-order": CloseOrder,
|
|
10251
|
-
deprecated: Deprecated,
|
|
10252
|
-
"deprecated-rule": DeprecatedRule,
|
|
10253
|
-
"doctype-html": NoStyleTag$1,
|
|
10254
|
-
"doctype-style": DoctypeStyle,
|
|
10255
|
-
"element-case": ElementCase,
|
|
10256
|
-
"element-name": ElementName,
|
|
10257
|
-
"element-permitted-content": ElementPermittedContent,
|
|
10258
|
-
"element-permitted-occurrences": ElementPermittedOccurrences,
|
|
10259
|
-
"element-permitted-order": ElementPermittedOrder,
|
|
10260
|
-
"element-permitted-parent": ElementPermittedParent,
|
|
10261
|
-
"element-required-ancestor": ElementRequiredAncestor,
|
|
10262
|
-
"element-required-attributes": ElementRequiredAttributes,
|
|
10263
|
-
"element-required-content": ElementRequiredContent,
|
|
10264
|
-
"empty-heading": EmptyHeading,
|
|
10265
|
-
"empty-title": EmptyTitle,
|
|
10266
|
-
"form-dup-name": FormDupName,
|
|
10267
|
-
"heading-level": HeadingLevel,
|
|
10268
|
-
"hidden-focusable": HiddenFocusable,
|
|
10269
|
-
"id-pattern": IdPattern,
|
|
10270
|
-
"input-attributes": InputAttributes,
|
|
10271
|
-
"input-missing-label": InputMissingLabel,
|
|
10272
|
-
"long-title": LongTitle,
|
|
10273
|
-
"map-dup-name": MapDupName,
|
|
10274
|
-
"map-id-name": MapIdName,
|
|
10275
|
-
"meta-refresh": MetaRefresh,
|
|
10276
|
-
"missing-doctype": MissingDoctype,
|
|
10277
|
-
"multiple-labeled-controls": MultipleLabeledControls,
|
|
10278
|
-
"name-pattern": NamePattern,
|
|
10279
|
-
"no-abstract-role": NoAbstractRole,
|
|
10280
|
-
"no-autoplay": NoAutoplay,
|
|
10281
|
-
"no-conditional-comment": NoConditionalComment,
|
|
10282
|
-
"no-deprecated-attr": NoDeprecatedAttr,
|
|
10283
|
-
"no-dup-attr": NoDupAttr,
|
|
10284
|
-
"no-dup-class": NoDupClass,
|
|
10285
|
-
"no-dup-id": NoDupID,
|
|
10286
|
-
"no-implicit-button-type": NoImplicitButtonType,
|
|
10287
|
-
"no-implicit-input-type": NoImplicitInputType,
|
|
10288
|
-
"no-implicit-close": NoImplicitClose,
|
|
10289
|
-
"no-inline-style": NoInlineStyle,
|
|
10290
|
-
"no-missing-references": NoMissingReferences,
|
|
10291
|
-
"no-multiple-main": NoMultipleMain,
|
|
10292
|
-
"no-raw-characters": NoRawCharacters,
|
|
10293
|
-
"no-redundant-aria-label": NoRedundantAriaLabel,
|
|
10294
|
-
"no-redundant-for": NoRedundantFor,
|
|
10295
|
-
"no-redundant-role": NoRedundantRole,
|
|
10296
|
-
"no-self-closing": NoSelfClosing,
|
|
10297
|
-
"no-style-tag": NoStyleTag,
|
|
10298
|
-
"no-trailing-whitespace": NoTrailingWhitespace,
|
|
10299
|
-
"no-unknown-elements": NoUnknownElements,
|
|
10300
|
-
"no-unused-disable": NoUnusedDisable,
|
|
10301
|
-
"no-utf8-bom": NoUtf8Bom,
|
|
10302
|
-
"prefer-button": PreferButton,
|
|
10303
|
-
"prefer-native-element": PreferNativeElement,
|
|
10304
|
-
"prefer-tbody": PreferTbody,
|
|
10305
|
-
"require-csp-nonce": RequireCSPNonce,
|
|
10306
|
-
"require-sri": RequireSri,
|
|
10307
|
-
"script-element": ScriptElement,
|
|
10308
|
-
"script-type": ScriptType,
|
|
10309
|
-
"svg-focusable": SvgFocusable,
|
|
10310
|
-
"tel-non-breaking": TelNonBreaking,
|
|
10311
|
-
"text-content": TextContent,
|
|
10312
|
-
"unique-landmark": UniqueLandmark,
|
|
10313
|
-
"unrecognized-char-ref": UnknownCharReference,
|
|
10314
|
-
"valid-autocomplete": ValidAutocomplete,
|
|
10315
|
-
"valid-id": ValidID,
|
|
10316
|
-
"void-content": VoidContent,
|
|
10317
|
-
"void-style": VoidStyle,
|
|
10318
|
-
...bundledRules$1
|
|
10361
|
+
const TRANSFORMER_API = {
|
|
10362
|
+
VERSION: 1
|
|
10319
10363
|
};
|
|
10320
10364
|
|
|
10321
10365
|
var defaultConfig = {};
|
|
@@ -11443,7 +11487,7 @@ class Parser {
|
|
|
11443
11487
|
location
|
|
11444
11488
|
};
|
|
11445
11489
|
this.trigger("tag:end", event);
|
|
11446
|
-
if (
|
|
11490
|
+
if (!active.isRootElement()) {
|
|
11447
11491
|
this.trigger("element:ready", {
|
|
11448
11492
|
target: active,
|
|
11449
11493
|
location: active.location
|
|
@@ -11795,15 +11839,6 @@ class Parser {
|
|
|
11795
11839
|
}
|
|
11796
11840
|
}
|
|
11797
11841
|
|
|
11798
|
-
function isThenable(value) {
|
|
11799
|
-
return value && typeof value === "object" && "then" in value && typeof value.then === "function";
|
|
11800
|
-
}
|
|
11801
|
-
|
|
11802
|
-
const ruleIds = new Set(Object.keys(bundledRules));
|
|
11803
|
-
function ruleExists(ruleId) {
|
|
11804
|
-
return ruleIds.has(ruleId);
|
|
11805
|
-
}
|
|
11806
|
-
|
|
11807
11842
|
function freeze(src) {
|
|
11808
11843
|
return {
|
|
11809
11844
|
...src,
|
|
@@ -11974,7 +12009,7 @@ class Engine {
|
|
|
11974
12009
|
const directiveContext = {
|
|
11975
12010
|
rules,
|
|
11976
12011
|
reportUnused(rules2, unused, options, location2) {
|
|
11977
|
-
if (
|
|
12012
|
+
if (!rules2.has(noUnusedDisable.name)) {
|
|
11978
12013
|
noUnusedDisable.reportUnused(unused, options, location2);
|
|
11979
12014
|
}
|
|
11980
12015
|
}
|
|
@@ -12105,7 +12140,9 @@ class Engine {
|
|
|
12105
12140
|
return new this.ParserClass(this.config);
|
|
12106
12141
|
}
|
|
12107
12142
|
processDirective(event, parser, context) {
|
|
12108
|
-
const rules = event.data.split(",").map((name) => name.trim()).map((name) => context.rules[name]).filter((rule) =>
|
|
12143
|
+
const rules = event.data.split(",").map((name) => name.trim()).map((name) => context.rules[name]).filter((rule) => {
|
|
12144
|
+
return Boolean(rule);
|
|
12145
|
+
});
|
|
12109
12146
|
const location = event.optionsLocation ?? event.location;
|
|
12110
12147
|
switch (event.action) {
|
|
12111
12148
|
case "enable":
|
|
@@ -12129,7 +12166,7 @@ class Engine {
|
|
|
12129
12166
|
rule.setServerity(Severity.ERROR);
|
|
12130
12167
|
}
|
|
12131
12168
|
}
|
|
12132
|
-
parser.on("tag:start", (
|
|
12169
|
+
parser.on("tag:start", (_event, data) => {
|
|
12133
12170
|
data.target.enableRules(rules.map((rule) => rule.name));
|
|
12134
12171
|
});
|
|
12135
12172
|
}
|
|
@@ -12137,7 +12174,7 @@ class Engine {
|
|
|
12137
12174
|
for (const rule of rules) {
|
|
12138
12175
|
rule.setEnabled(false);
|
|
12139
12176
|
}
|
|
12140
|
-
parser.on("tag:start", (
|
|
12177
|
+
parser.on("tag:start", (_event, data) => {
|
|
12141
12178
|
data.target.disableRules(rules.map((rule) => rule.name));
|
|
12142
12179
|
});
|
|
12143
12180
|
}
|
|
@@ -12149,14 +12186,14 @@ class Engine {
|
|
|
12149
12186
|
for (const rule of rules) {
|
|
12150
12187
|
rule.block(blocker);
|
|
12151
12188
|
}
|
|
12152
|
-
const unregisterOpen = parser.on("tag:start", (
|
|
12189
|
+
const unregisterOpen = parser.on("tag:start", (_event, data) => {
|
|
12153
12190
|
var _a;
|
|
12154
12191
|
if (directiveBlock === null) {
|
|
12155
12192
|
directiveBlock = ((_a = data.target.parent) == null ? void 0 : _a.unique) ?? null;
|
|
12156
12193
|
}
|
|
12157
12194
|
data.target.blockRules(ruleIds, blocker);
|
|
12158
12195
|
});
|
|
12159
|
-
const unregisterClose = parser.on("tag:end", (
|
|
12196
|
+
const unregisterClose = parser.on("tag:end", (_event, data) => {
|
|
12160
12197
|
const lastNode = directiveBlock === null;
|
|
12161
12198
|
const parentClosed = directiveBlock === data.previous.unique;
|
|
12162
12199
|
if (lastNode || parentClosed) {
|
|
@@ -12167,7 +12204,7 @@ class Engine {
|
|
|
12167
12204
|
}
|
|
12168
12205
|
}
|
|
12169
12206
|
});
|
|
12170
|
-
parser.on("rule:error", (
|
|
12207
|
+
parser.on("rule:error", (_event, data) => {
|
|
12171
12208
|
if (data.blockers.includes(blocker)) {
|
|
12172
12209
|
unused.delete(data.ruleId);
|
|
12173
12210
|
}
|
|
@@ -12183,10 +12220,10 @@ class Engine {
|
|
|
12183
12220
|
for (const rule of rules) {
|
|
12184
12221
|
rule.block(blocker);
|
|
12185
12222
|
}
|
|
12186
|
-
const unregister = parser.on("tag:start", (
|
|
12223
|
+
const unregister = parser.on("tag:start", (_event, data) => {
|
|
12187
12224
|
data.target.blockRules(ruleIds, blocker);
|
|
12188
12225
|
});
|
|
12189
|
-
parser.on("rule:error", (
|
|
12226
|
+
parser.on("rule:error", (_event, data) => {
|
|
12190
12227
|
if (data.blockers.includes(blocker)) {
|
|
12191
12228
|
unused.delete(data.ruleId);
|
|
12192
12229
|
}
|
|
@@ -12750,7 +12787,7 @@ class HtmlValidate {
|
|
|
12750
12787
|
}
|
|
12751
12788
|
|
|
12752
12789
|
const name = "html-validate";
|
|
12753
|
-
const version = "8.
|
|
12790
|
+
const version = "8.21.0";
|
|
12754
12791
|
const bugs = "https://gitlab.com/html-validate/html-validate/issues/new";
|
|
12755
12792
|
|
|
12756
12793
|
function definePlugin(plugin) {
|
|
@@ -13625,5 +13662,5 @@ function compatibilityCheckImpl(name, declared, options) {
|
|
|
13625
13662
|
return false;
|
|
13626
13663
|
}
|
|
13627
13664
|
|
|
13628
|
-
export { Attribute as A, definePlugin as B, Config as C, DOMNode as D, Parser as E, ruleExists as F,
|
|
13665
|
+
export { Attribute as A, definePlugin as B, Config as C, DOMNode as D, Parser as E, ruleExists as F, walk as G, HtmlValidate as H, EventHandler as I, compatibilityCheckImpl as J, codeframe as K, name as L, MetaCopyableProperty as M, NodeClosed as N, bugs as O, Presets as P, ResolvedConfig as R, Severity as S, TextNode as T, UserError as U, Validator as V, WrappedError as W, ConfigError as a, ConfigLoader as b, defineConfig as c, deepmerge$1 as d, ensureError as e, StaticConfigLoader as f, getFormatter as g, DOMTokenList as h, ignore$1 as i, DOMTree as j, DynamicValue as k, HtmlElement as l, NodeType as m, NestedError as n, SchemaValidationError as o, MetaTable as p, TextContent$1 as q, Rule as r, staticResolver as s, ariaNaming as t, TextClassification as u, version as v, classifyNodeText as w, keywordPatternMatcher as x, sliceLocation as y, Reporter as z };
|
|
13629
13666
|
//# sourceMappingURL=core.js.map
|