html-validate 8.20.1 → 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 +2185 -2143
- 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 +2185 -2144
- 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 +20 -20
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;
|
|
@@ -5072,7 +4607,7 @@ class AttributeBooleanStyle extends Rule {
|
|
|
5072
4607
|
setup() {
|
|
5073
4608
|
this.on("dom:ready", (event) => {
|
|
5074
4609
|
const doc = event.document;
|
|
5075
|
-
|
|
4610
|
+
walk.depthFirst(doc, (node) => {
|
|
5076
4611
|
const meta = node.meta;
|
|
5077
4612
|
if (!(meta == null ? void 0 : meta.attributes))
|
|
5078
4613
|
return;
|
|
@@ -5144,7 +4679,7 @@ class AttributeEmptyStyle extends Rule {
|
|
|
5144
4679
|
setup() {
|
|
5145
4680
|
this.on("dom:ready", (event) => {
|
|
5146
4681
|
const doc = event.document;
|
|
5147
|
-
|
|
4682
|
+
walk.depthFirst(doc, (node) => {
|
|
5148
4683
|
const meta = node.meta;
|
|
5149
4684
|
if (!(meta == null ? void 0 : meta.attributes))
|
|
5150
4685
|
return;
|
|
@@ -5796,7 +5331,7 @@ class ElementPermittedContent extends Rule {
|
|
|
5796
5331
|
setup() {
|
|
5797
5332
|
this.on("dom:ready", (event) => {
|
|
5798
5333
|
const doc = event.document;
|
|
5799
|
-
|
|
5334
|
+
walk.depthFirst(doc, (node) => {
|
|
5800
5335
|
const parent = node.parent;
|
|
5801
5336
|
if (!parent) {
|
|
5802
5337
|
return;
|
|
@@ -5875,7 +5410,7 @@ class ElementPermittedOccurrences extends Rule {
|
|
|
5875
5410
|
setup() {
|
|
5876
5411
|
this.on("dom:ready", (event) => {
|
|
5877
5412
|
const doc = event.document;
|
|
5878
|
-
|
|
5413
|
+
walk.depthFirst(doc, (node) => {
|
|
5879
5414
|
if (!node.meta) {
|
|
5880
5415
|
return;
|
|
5881
5416
|
}
|
|
@@ -5908,7 +5443,7 @@ class ElementPermittedOrder extends Rule {
|
|
|
5908
5443
|
setup() {
|
|
5909
5444
|
this.on("dom:ready", (event) => {
|
|
5910
5445
|
const doc = event.document;
|
|
5911
|
-
|
|
5446
|
+
walk.depthFirst(doc, (node) => {
|
|
5912
5447
|
if (!node.meta) {
|
|
5913
5448
|
return;
|
|
5914
5449
|
}
|
|
@@ -5978,7 +5513,7 @@ class ElementPermittedParent extends Rule {
|
|
|
5978
5513
|
setup() {
|
|
5979
5514
|
this.on("dom:ready", (event) => {
|
|
5980
5515
|
const doc = event.document;
|
|
5981
|
-
|
|
5516
|
+
walk.depthFirst(doc, (node) => {
|
|
5982
5517
|
var _a;
|
|
5983
5518
|
const parent = node.parent;
|
|
5984
5519
|
if (!parent) {
|
|
@@ -6026,7 +5561,7 @@ class ElementRequiredAncestor extends Rule {
|
|
|
6026
5561
|
setup() {
|
|
6027
5562
|
this.on("dom:ready", (event) => {
|
|
6028
5563
|
const doc = event.document;
|
|
6029
|
-
|
|
5564
|
+
walk.depthFirst(doc, (node) => {
|
|
6030
5565
|
const parent = node.parent;
|
|
6031
5566
|
if (!parent) {
|
|
6032
5567
|
return;
|
|
@@ -6110,7 +5645,7 @@ class ElementRequiredContent extends Rule {
|
|
|
6110
5645
|
setup() {
|
|
6111
5646
|
this.on("dom:ready", (event) => {
|
|
6112
5647
|
const doc = event.document;
|
|
6113
|
-
|
|
5648
|
+
walk.depthFirst(doc, (node) => {
|
|
6114
5649
|
if (!node.meta) {
|
|
6115
5650
|
return;
|
|
6116
5651
|
}
|
|
@@ -6630,7 +6165,7 @@ function isFocusableImpl(element) {
|
|
|
6630
6165
|
if (isDisabled(element, meta)) {
|
|
6631
6166
|
return false;
|
|
6632
6167
|
}
|
|
6633
|
-
return Boolean(meta
|
|
6168
|
+
return Boolean(meta.focusable);
|
|
6634
6169
|
}
|
|
6635
6170
|
function isFocusable(element) {
|
|
6636
6171
|
const cached = element.cacheGet(FOCUSABLE_CACHE);
|
|
@@ -8540,1778 +8075,2291 @@ class RequireSri extends Rule {
|
|
|
8540
8075
|
items: {
|
|
8541
8076
|
type: "string"
|
|
8542
8077
|
},
|
|
8543
|
-
type: "array"
|
|
8544
|
-
},
|
|
8545
|
-
{
|
|
8546
|
-
type: "null"
|
|
8547
|
-
}
|
|
8548
|
-
]
|
|
8549
|
-
},
|
|
8550
|
-
exclude: {
|
|
8551
|
-
anyOf: [
|
|
8552
|
-
{
|
|
8553
|
-
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: {
|
|
8554
8279
|
type: "string"
|
|
8555
8280
|
},
|
|
8556
|
-
|
|
8557
|
-
|
|
8558
|
-
|
|
8559
|
-
type: "null"
|
|
8281
|
+
description: {
|
|
8282
|
+
type: "string"
|
|
8283
|
+
}
|
|
8560
8284
|
}
|
|
8561
|
-
|
|
8285
|
+
}
|
|
8286
|
+
},
|
|
8287
|
+
ignoreClasses: {
|
|
8288
|
+
type: "array",
|
|
8289
|
+
items: {
|
|
8290
|
+
type: "string"
|
|
8291
|
+
}
|
|
8292
|
+
},
|
|
8293
|
+
ignoreStyle: {
|
|
8294
|
+
type: "boolean"
|
|
8562
8295
|
}
|
|
8563
8296
|
};
|
|
8564
8297
|
}
|
|
8565
|
-
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
|
+
});
|
|
8566
8303
|
return {
|
|
8567
|
-
description:
|
|
8568
|
-
|
|
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"
|
|
8569
8315
|
};
|
|
8570
8316
|
}
|
|
8571
8317
|
setup() {
|
|
8572
|
-
this.on("
|
|
8573
|
-
const
|
|
8574
|
-
if (
|
|
8575
|
-
return;
|
|
8576
|
-
}
|
|
8577
|
-
if (node.hasAttribute("integrity")) {
|
|
8318
|
+
this.on("element:ready", this.isRelevant, (event) => {
|
|
8319
|
+
const { target } = event;
|
|
8320
|
+
if (this.isIgnored(target)) {
|
|
8578
8321
|
return;
|
|
8579
8322
|
}
|
|
8580
|
-
this.
|
|
8581
|
-
node,
|
|
8582
|
-
`SRI "integrity" attribute is required on <${node.tagName}> element`,
|
|
8583
|
-
node.location
|
|
8584
|
-
);
|
|
8323
|
+
this.walk(target, target);
|
|
8585
8324
|
});
|
|
8586
8325
|
}
|
|
8587
|
-
|
|
8588
|
-
|
|
8589
|
-
|
|
8590
|
-
needSri(node) {
|
|
8591
|
-
const attr = this.elementSourceAttr(node);
|
|
8592
|
-
if (!attr) {
|
|
8326
|
+
isRelevant(event) {
|
|
8327
|
+
const { target } = event;
|
|
8328
|
+
if (!target.is("a")) {
|
|
8593
8329
|
return false;
|
|
8594
8330
|
}
|
|
8595
|
-
|
|
8331
|
+
const attr = target.getAttribute("href");
|
|
8332
|
+
if (!(attr == null ? void 0 : attr.valueMatches(/^tel:/, false))) {
|
|
8596
8333
|
return false;
|
|
8597
8334
|
}
|
|
8598
|
-
|
|
8599
|
-
|
|
8600
|
-
|
|
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;
|
|
8601
8350
|
}
|
|
8602
8351
|
return false;
|
|
8603
8352
|
}
|
|
8604
|
-
|
|
8605
|
-
|
|
8606
|
-
|
|
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`);
|
|
8607
8506
|
}
|
|
8608
|
-
|
|
8609
|
-
|
|
8610
|
-
|
|
8507
|
+
reportError(node, meta, message) {
|
|
8508
|
+
this.report(node, message, null, {
|
|
8509
|
+
tagName: node.tagName,
|
|
8510
|
+
textContent: meta.textContent
|
|
8611
8511
|
});
|
|
8612
8512
|
}
|
|
8613
8513
|
}
|
|
8614
8514
|
|
|
8615
|
-
|
|
8616
|
-
|
|
8617
|
-
|
|
8618
|
-
|
|
8619
|
-
|
|
8620
|
-
|
|
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;
|
|
8621
8530
|
}
|
|
8622
|
-
|
|
8623
|
-
|
|
8624
|
-
|
|
8625
|
-
|
|
8626
|
-
|
|
8627
|
-
|
|
8628
|
-
if (node.closed !== NodeClosed.EndTag) {
|
|
8629
|
-
this.report(node, `End tag for <${node.tagName}> must not be omitted`);
|
|
8630
|
-
}
|
|
8631
|
-
});
|
|
8531
|
+
const selector = `#${id}`;
|
|
8532
|
+
const ref = document.querySelector(selector);
|
|
8533
|
+
if (ref) {
|
|
8534
|
+
return ref.textContent;
|
|
8535
|
+
} else {
|
|
8536
|
+
return selector;
|
|
8632
8537
|
}
|
|
8633
8538
|
}
|
|
8634
|
-
|
|
8635
|
-
const
|
|
8636
|
-
|
|
8637
|
-
|
|
8638
|
-
|
|
8639
|
-
|
|
8640
|
-
|
|
8641
|
-
];
|
|
8642
|
-
|
|
8643
|
-
|
|
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) {
|
|
8644
8554
|
return {
|
|
8645
|
-
|
|
8646
|
-
|
|
8555
|
+
node,
|
|
8556
|
+
text: ariaLabel.value,
|
|
8557
|
+
location: ariaLabel.keyLocation
|
|
8647
8558
|
};
|
|
8648
8559
|
}
|
|
8649
|
-
|
|
8650
|
-
|
|
8651
|
-
|
|
8652
|
-
|
|
8653
|
-
|
|
8654
|
-
|
|
8655
|
-
|
|
8656
|
-
|
|
8657
|
-
return;
|
|
8658
|
-
}
|
|
8659
|
-
const value = attr.value ? attr.value.toString() : "";
|
|
8660
|
-
if (!this.isJavascript(value)) {
|
|
8661
|
-
return;
|
|
8662
|
-
}
|
|
8663
|
-
this.report(
|
|
8664
|
-
node,
|
|
8665
|
-
'"type" attribute is unnecessary for javascript resources',
|
|
8666
|
-
attr.keyLocation
|
|
8667
|
-
);
|
|
8668
|
-
});
|
|
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
|
+
};
|
|
8669
8568
|
}
|
|
8670
|
-
|
|
8671
|
-
|
|
8672
|
-
|
|
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"));
|
|
8673
8579
|
}
|
|
8580
|
+
return true;
|
|
8674
8581
|
}
|
|
8675
|
-
|
|
8676
|
-
class SvgFocusable extends Rule {
|
|
8582
|
+
class UniqueLandmark extends Rule {
|
|
8677
8583
|
documentation() {
|
|
8678
8584
|
return {
|
|
8679
|
-
description:
|
|
8680
|
-
|
|
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"
|
|
8681
8606
|
};
|
|
8682
8607
|
}
|
|
8683
8608
|
setup() {
|
|
8684
|
-
this.on("
|
|
8685
|
-
|
|
8686
|
-
|
|
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
|
+
}
|
|
8687
8634
|
}
|
|
8688
8635
|
});
|
|
8689
8636
|
}
|
|
8690
|
-
validate(svg) {
|
|
8691
|
-
if (svg.hasAttribute("focusable")) {
|
|
8692
|
-
return;
|
|
8693
|
-
}
|
|
8694
|
-
this.report(svg, `<${svg.tagName}> is missing required "focusable" attribute`);
|
|
8695
|
-
}
|
|
8696
8637
|
}
|
|
8697
8638
|
|
|
8698
|
-
const defaults$
|
|
8699
|
-
|
|
8700
|
-
|
|
8701
|
-
{ pattern: "-", replacement: "‑", description: "non-breaking hyphen" }
|
|
8702
|
-
],
|
|
8703
|
-
ignoreClasses: [],
|
|
8704
|
-
ignoreStyle: true
|
|
8639
|
+
const defaults$4 = {
|
|
8640
|
+
ignoreCase: false,
|
|
8641
|
+
requireSemicolon: true
|
|
8705
8642
|
};
|
|
8706
|
-
|
|
8707
|
-
|
|
8708
|
-
|
|
8709
|
-
|
|
8710
|
-
const pattern = `(${disallowed})`;
|
|
8711
|
-
return new RegExp(pattern, "g");
|
|
8712
|
-
}
|
|
8713
|
-
function getText(node) {
|
|
8714
|
-
const match = node.textContent.match(/^(\s*)(.*)$/);
|
|
8715
|
-
const [, leading, text] = match;
|
|
8716
|
-
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("&#");
|
|
8717
8647
|
}
|
|
8718
|
-
function
|
|
8719
|
-
const
|
|
8720
|
-
|
|
8721
|
-
let match;
|
|
8722
|
-
while (match = copy.exec(text)) {
|
|
8723
|
-
matches.push(match);
|
|
8724
|
-
}
|
|
8725
|
-
return matches;
|
|
8648
|
+
function getLocation(location, entity, match) {
|
|
8649
|
+
const index = match.index ?? 0;
|
|
8650
|
+
return sliceLocation(location, index, index + entity.length);
|
|
8726
8651
|
}
|
|
8727
|
-
|
|
8728
|
-
|
|
8729
|
-
|
|
8730
|
-
|
|
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.`;
|
|
8731
8659
|
}
|
|
8732
|
-
|
|
8733
|
-
|
|
8734
|
-
|
|
8735
|
-
|
|
8736
|
-
|
|
8737
|
-
|
|
8738
|
-
|
|
8739
|
-
|
|
8740
|
-
|
|
8741
|
-
|
|
8742
|
-
|
|
8743
|
-
|
|
8744
|
-
|
|
8745
|
-
|
|
8746
|
-
|
|
8747
|
-
|
|
8748
|
-
|
|
8749
|
-
|
|
8750
|
-
|
|
8751
|
-
},
|
|
8752
|
-
ignoreClasses: {
|
|
8753
|
-
type: "array",
|
|
8754
|
-
items: {
|
|
8755
|
-
type: "string"
|
|
8756
|
-
}
|
|
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"
|
|
8757
8679
|
},
|
|
8758
|
-
|
|
8680
|
+
requireSemicolon: {
|
|
8759
8681
|
type: "boolean"
|
|
8760
8682
|
}
|
|
8761
8683
|
};
|
|
8762
8684
|
}
|
|
8763
8685
|
documentation(context) {
|
|
8764
|
-
const { characters } = this.options;
|
|
8765
|
-
const replacements = characters.map((it) => {
|
|
8766
|
-
return ` - \`${it.pattern}\` - replace with \`${it.replacement}\` (${it.description}).`;
|
|
8767
|
-
});
|
|
8768
8686
|
return {
|
|
8769
|
-
description:
|
|
8770
|
-
|
|
8771
|
-
"",
|
|
8772
|
-
"Unless non-breaking characters is used there could be a line break inserted at that character.",
|
|
8773
|
-
"Line breaks make is harder to read and understand the telephone number.",
|
|
8774
|
-
"",
|
|
8775
|
-
"The following characters should be avoided:",
|
|
8776
|
-
"",
|
|
8777
|
-
...replacements
|
|
8778
|
-
].join("\n"),
|
|
8779
|
-
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"
|
|
8780
8689
|
};
|
|
8781
8690
|
}
|
|
8782
8691
|
setup() {
|
|
8783
|
-
this.on("element:ready",
|
|
8784
|
-
const
|
|
8785
|
-
|
|
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) {
|
|
8786
8705
|
return;
|
|
8787
8706
|
}
|
|
8788
|
-
this.
|
|
8707
|
+
this.findCharacterReferences(event.target, event.value.toString(), event.valueLocation, {
|
|
8708
|
+
isAttribute: true
|
|
8709
|
+
});
|
|
8789
8710
|
});
|
|
8790
8711
|
}
|
|
8791
|
-
|
|
8792
|
-
|
|
8793
|
-
|
|
8794
|
-
|
|
8712
|
+
get entities() {
|
|
8713
|
+
if (this.options.ignoreCase) {
|
|
8714
|
+
return lowercaseEntities;
|
|
8715
|
+
} else {
|
|
8716
|
+
return entities$1;
|
|
8795
8717
|
}
|
|
8796
|
-
|
|
8797
|
-
|
|
8798
|
-
|
|
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 });
|
|
8799
8723
|
}
|
|
8800
|
-
return true;
|
|
8801
8724
|
}
|
|
8802
|
-
|
|
8803
|
-
const {
|
|
8804
|
-
const {
|
|
8805
|
-
|
|
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";
|
|
8806
8916
|
}
|
|
8807
|
-
|
|
8808
|
-
|
|
8809
|
-
const { style } = node;
|
|
8810
|
-
if (!ignoreStyle) {
|
|
8811
|
-
return false;
|
|
8812
|
-
}
|
|
8813
|
-
if (style["white-space"] === "nowrap" || style["white-space"] === "pre") {
|
|
8814
|
-
return true;
|
|
8815
|
-
}
|
|
8816
|
-
return false;
|
|
8917
|
+
if (matchFieldNames1(token)) {
|
|
8918
|
+
return "field1";
|
|
8817
8919
|
}
|
|
8818
|
-
|
|
8819
|
-
return
|
|
8920
|
+
if (matchFieldNames2(token)) {
|
|
8921
|
+
return "field2";
|
|
8820
8922
|
}
|
|
8821
|
-
|
|
8822
|
-
|
|
8823
|
-
if (isTextNode(child)) {
|
|
8824
|
-
this.detectDisallowed(anchor, child);
|
|
8825
|
-
} else if (isElementNode(child)) {
|
|
8826
|
-
this.walk(anchor, child);
|
|
8827
|
-
}
|
|
8828
|
-
}
|
|
8923
|
+
if (matchContact(token)) {
|
|
8924
|
+
return "contact";
|
|
8829
8925
|
}
|
|
8830
|
-
|
|
8831
|
-
|
|
8832
|
-
const matches = matchAll(text, this.regex);
|
|
8833
|
-
for (const match of matches) {
|
|
8834
|
-
const detected = match[0];
|
|
8835
|
-
const entry = this.options.characters.find((it) => it.pattern === detected);
|
|
8836
|
-
if (!entry) {
|
|
8837
|
-
throw new Error(`Failed to find entry for "${detected}" when searching text "${text}"`);
|
|
8838
|
-
}
|
|
8839
|
-
const message = `"${detected}" should be replaced with "${entry.replacement}" (${entry.description}) in telephone number`;
|
|
8840
|
-
const begin = offset + match.index;
|
|
8841
|
-
const end = begin + detected.length;
|
|
8842
|
-
const location = sliceLocation(node.location, begin, end);
|
|
8843
|
-
const context = entry;
|
|
8844
|
-
this.report(anchor, message, location, context);
|
|
8845
|
-
}
|
|
8926
|
+
if (matchWebauthn(token)) {
|
|
8927
|
+
return "webauthn";
|
|
8846
8928
|
}
|
|
8929
|
+
return null;
|
|
8847
8930
|
}
|
|
8848
|
-
|
|
8849
|
-
|
|
8850
|
-
|
|
8851
|
-
|
|
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] ?? [];
|
|
8852
8956
|
}
|
|
8853
|
-
function
|
|
8957
|
+
function isDisallowedType(node, type) {
|
|
8854
8958
|
if (!node.is("input")) {
|
|
8855
8959
|
return false;
|
|
8856
8960
|
}
|
|
8857
|
-
|
|
8858
|
-
return false;
|
|
8859
|
-
}
|
|
8860
|
-
const type = node.getAttribute("type");
|
|
8861
|
-
return Boolean(type == null ? void 0 : type.valueMatches(/submit|reset/, false));
|
|
8961
|
+
return disallowedInputTypes.includes(type);
|
|
8862
8962
|
}
|
|
8863
|
-
function
|
|
8864
|
-
|
|
8865
|
-
|
|
8866
|
-
|
|
8867
|
-
|
|
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";
|
|
8868
8977
|
}
|
|
8869
8978
|
}
|
|
8870
|
-
function
|
|
8871
|
-
|
|
8872
|
-
|
|
8873
|
-
|
|
8874
|
-
|
|
8875
|
-
|
|
8876
|
-
|
|
8877
|
-
|
|
8878
|
-
|
|
8879
|
-
|
|
8880
|
-
|
|
8881
|
-
|
|
8882
|
-
|
|
8883
|
-
|
|
8884
|
-
|
|
8885
|
-
|
|
8886
|
-
|
|
8887
|
-
|
|
8888
|
-
|
|
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";
|
|
8889
9035
|
}
|
|
8890
|
-
return node.childElements.some((child) => {
|
|
8891
|
-
return haveAccessibleText(child);
|
|
8892
|
-
});
|
|
8893
9036
|
}
|
|
8894
|
-
class
|
|
9037
|
+
class ValidAutocomplete extends Rule {
|
|
8895
9038
|
documentation(context) {
|
|
8896
|
-
|
|
8897
|
-
description:
|
|
8898
|
-
url: "https://html-validate.org/rules/
|
|
9039
|
+
return {
|
|
9040
|
+
description: getMarkdownMessage(context),
|
|
9041
|
+
url: "https://html-validate.org/rules/valid-autocomplete.html"
|
|
8899
9042
|
};
|
|
8900
|
-
switch (context.textContent) {
|
|
8901
|
-
case TextContent$1.NONE:
|
|
8902
|
-
doc.description = `The \`<${context.tagName}>\` element must not have textual content.`;
|
|
8903
|
-
break;
|
|
8904
|
-
case TextContent$1.REQUIRED:
|
|
8905
|
-
doc.description = `The \`<${context.tagName}>\` element must have textual content.`;
|
|
8906
|
-
break;
|
|
8907
|
-
case TextContent$1.ACCESSIBLE:
|
|
8908
|
-
doc.description = `The \`<${context.tagName}>\` element must have accessible text.`;
|
|
8909
|
-
break;
|
|
8910
|
-
}
|
|
8911
|
-
return doc;
|
|
8912
|
-
}
|
|
8913
|
-
static filter(event) {
|
|
8914
|
-
const { target } = event;
|
|
8915
|
-
if (!target.meta) {
|
|
8916
|
-
return false;
|
|
8917
|
-
}
|
|
8918
|
-
const { textContent } = target.meta;
|
|
8919
|
-
if (!textContent || textContent === TextContent$1.DEFAULT) {
|
|
8920
|
-
return false;
|
|
8921
|
-
}
|
|
8922
|
-
return true;
|
|
8923
9043
|
}
|
|
8924
9044
|
setup() {
|
|
8925
|
-
this.on("
|
|
8926
|
-
const
|
|
8927
|
-
const
|
|
8928
|
-
|
|
8929
|
-
|
|
8930
|
-
|
|
8931
|
-
|
|
8932
|
-
|
|
8933
|
-
|
|
8934
|
-
|
|
8935
|
-
|
|
8936
|
-
|
|
8937
|
-
|
|
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);
|
|
8938
9060
|
}
|
|
8939
9061
|
});
|
|
8940
9062
|
}
|
|
8941
|
-
|
|
8942
|
-
|
|
8943
|
-
|
|
8944
|
-
|
|
8945
|
-
|
|
8946
|
-
|
|
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;
|
|
8947
9073
|
}
|
|
8948
|
-
this.reportError(node, node.meta, `${node.annotatedName} must not have text content`);
|
|
8949
9074
|
}
|
|
8950
|
-
|
|
8951
|
-
|
|
8952
|
-
|
|
8953
|
-
|
|
8954
|
-
|
|
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
|
+
});
|
|
8955
9089
|
return;
|
|
8956
9090
|
}
|
|
8957
|
-
|
|
8958
|
-
|
|
8959
|
-
/**
|
|
8960
|
-
* Validate element has accessible text (either regular text or text only
|
|
8961
|
-
* exposed in accessibility tree via aria-label or similar)
|
|
8962
|
-
*/
|
|
8963
|
-
validateAccessible(node) {
|
|
8964
|
-
if (!inAccessibilityTree(node)) {
|
|
9091
|
+
if (tokens.includes("on") || tokens.includes("off")) {
|
|
9092
|
+
this.validateOnOff(node, mantle, tokens);
|
|
8965
9093
|
return;
|
|
8966
9094
|
}
|
|
8967
|
-
|
|
9095
|
+
this.validateTokens(node, tokens, keyLocation);
|
|
9096
|
+
}
|
|
9097
|
+
validateFormAutocomplete(node, value, location) {
|
|
9098
|
+
const trimmed = value.trim();
|
|
9099
|
+
if (["on", "off"].includes(trimmed)) {
|
|
8968
9100
|
return;
|
|
8969
9101
|
}
|
|
8970
|
-
|
|
8971
|
-
|
|
8972
|
-
|
|
8973
|
-
|
|
8974
|
-
|
|
8975
|
-
|
|
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
|
|
8976
9113
|
});
|
|
8977
9114
|
}
|
|
8978
|
-
|
|
8979
|
-
|
|
8980
|
-
const
|
|
8981
|
-
const
|
|
8982
|
-
|
|
8983
|
-
|
|
8984
|
-
|
|
8985
|
-
|
|
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
|
-
|
|
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
|
+
}
|
|
9012
9150
|
}
|
|
9013
9151
|
}
|
|
9014
|
-
|
|
9015
|
-
|
|
9016
|
-
|
|
9017
|
-
|
|
9018
|
-
|
|
9019
|
-
|
|
9020
|
-
|
|
9021
|
-
|
|
9022
|
-
|
|
9023
|
-
|
|
9024
|
-
|
|
9025
|
-
|
|
9026
|
-
|
|
9027
|
-
|
|
9028
|
-
|
|
9029
|
-
|
|
9030
|
-
|
|
9031
|
-
|
|
9032
|
-
|
|
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);
|
|
9033
9177
|
}
|
|
9034
|
-
|
|
9035
|
-
|
|
9036
|
-
|
|
9037
|
-
|
|
9038
|
-
|
|
9039
|
-
|
|
9040
|
-
|
|
9041
|
-
|
|
9042
|
-
|
|
9043
|
-
|
|
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
|
+
}
|
|
9044
9210
|
}
|
|
9045
|
-
|
|
9046
|
-
|
|
9047
|
-
|
|
9048
|
-
|
|
9049
|
-
|
|
9050
|
-
|
|
9051
|
-
|
|
9052
|
-
|
|
9053
|
-
|
|
9054
|
-
|
|
9055
|
-
|
|
9056
|
-
|
|
9057
|
-
|
|
9058
|
-
|
|
9059
|
-
|
|
9060
|
-
|
|
9061
|
-
|
|
9062
|
-
|
|
9063
|
-
|
|
9064
|
-
|
|
9065
|
-
|
|
9066
|
-
" - Add `aria-label`.",
|
|
9067
|
-
" - Add `aria-labelledby`.",
|
|
9068
|
-
" - Remove one of the landmarks."
|
|
9069
|
-
].join("\n"),
|
|
9070
|
-
url: "https://html-validate.org/rules/unique-landmark.html"
|
|
9071
|
-
};
|
|
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
|
+
}
|
|
9072
9232
|
}
|
|
9073
|
-
|
|
9074
|
-
|
|
9075
|
-
|
|
9076
|
-
|
|
9077
|
-
|
|
9078
|
-
|
|
9079
|
-
|
|
9080
|
-
|
|
9081
|
-
|
|
9082
|
-
|
|
9083
|
-
|
|
9084
|
-
|
|
9085
|
-
|
|
9086
|
-
|
|
9087
|
-
|
|
9088
|
-
|
|
9089
|
-
|
|
9090
|
-
const message = `Landmarks must have a non-empty and unique accessible name (aria-label or aria-labelledby)`;
|
|
9091
|
-
const location = entry.location;
|
|
9092
|
-
this.report({
|
|
9093
|
-
node: entry.node,
|
|
9094
|
-
message,
|
|
9095
|
-
location
|
|
9096
|
-
});
|
|
9097
|
-
}
|
|
9098
|
-
}
|
|
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
|
+
});
|
|
9099
9250
|
}
|
|
9100
|
-
}
|
|
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
|
+
}
|
|
9101
9284
|
}
|
|
9102
9285
|
}
|
|
9103
9286
|
|
|
9104
|
-
const defaults$
|
|
9105
|
-
|
|
9106
|
-
requireSemicolon: true
|
|
9287
|
+
const defaults$3 = {
|
|
9288
|
+
relaxed: false
|
|
9107
9289
|
};
|
|
9108
|
-
|
|
9109
|
-
const lowercaseEntities = entities$1.map((it) => it.toLowerCase());
|
|
9110
|
-
function isNumerical(entity) {
|
|
9111
|
-
return entity.startsWith("&#");
|
|
9112
|
-
}
|
|
9113
|
-
function getLocation(location, entity, match) {
|
|
9114
|
-
const index = match.index ?? 0;
|
|
9115
|
-
return sliceLocation(location, index, index + entity.length);
|
|
9116
|
-
}
|
|
9117
|
-
function getDescription(context, options) {
|
|
9118
|
-
const url = "https://html.spec.whatwg.org/multipage/named-characters.html";
|
|
9119
|
-
let message;
|
|
9120
|
-
if (context.terminated) {
|
|
9121
|
-
message = `Unrecognized character reference \`${context.entity}\`.`;
|
|
9122
|
-
} else {
|
|
9123
|
-
message = `Character reference \`${context.entity}\` must be terminated by a semicolon.`;
|
|
9124
|
-
}
|
|
9125
|
-
return [
|
|
9126
|
-
message,
|
|
9127
|
-
`HTML5 defines a set of [valid character references](${url}) but this is not a valid one.`,
|
|
9128
|
-
"",
|
|
9129
|
-
"Ensure that:",
|
|
9130
|
-
"",
|
|
9131
|
-
"1. The character is one of the listed names.",
|
|
9132
|
-
...options.ignoreCase ? [] : ["1. The case is correct (names are case sensitive)."],
|
|
9133
|
-
...options.requireSemicolon ? ["1. The name is terminated with a `;`."] : []
|
|
9134
|
-
].join("\n");
|
|
9135
|
-
}
|
|
9136
|
-
class UnknownCharReference extends Rule {
|
|
9290
|
+
class ValidID extends Rule {
|
|
9137
9291
|
constructor(options) {
|
|
9138
|
-
super({ ...defaults$
|
|
9292
|
+
super({ ...defaults$3, ...options });
|
|
9139
9293
|
}
|
|
9140
9294
|
static schema() {
|
|
9141
9295
|
return {
|
|
9142
|
-
|
|
9143
|
-
type: "boolean"
|
|
9144
|
-
},
|
|
9145
|
-
requireSemicolon: {
|
|
9296
|
+
relaxed: {
|
|
9146
9297
|
type: "boolean"
|
|
9147
9298
|
}
|
|
9148
9299
|
};
|
|
9149
9300
|
}
|
|
9150
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
|
+
];
|
|
9151
9308
|
return {
|
|
9152
|
-
description:
|
|
9153
|
-
|
|
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"
|
|
9154
9319
|
};
|
|
9155
9320
|
}
|
|
9156
9321
|
setup() {
|
|
9157
|
-
this.on("
|
|
9158
|
-
const
|
|
9159
|
-
|
|
9160
|
-
|
|
9161
|
-
continue;
|
|
9162
|
-
}
|
|
9163
|
-
this.findCharacterReferences(node, child.textContent, child.location, {
|
|
9164
|
-
isAttribute: false
|
|
9165
|
-
});
|
|
9322
|
+
this.on("attr", this.isRelevant, (event) => {
|
|
9323
|
+
const { value } = event;
|
|
9324
|
+
if (value === null || value instanceof DynamicValue) {
|
|
9325
|
+
return;
|
|
9166
9326
|
}
|
|
9167
|
-
|
|
9168
|
-
|
|
9169
|
-
|
|
9327
|
+
if (value === "") {
|
|
9328
|
+
const context = 1 /* EMPTY */;
|
|
9329
|
+
this.report(event.target, this.messages[context], event.location, context);
|
|
9170
9330
|
return;
|
|
9171
9331
|
}
|
|
9172
|
-
|
|
9173
|
-
|
|
9174
|
-
|
|
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
|
+
}
|
|
9175
9350
|
});
|
|
9176
9351
|
}
|
|
9177
|
-
get
|
|
9178
|
-
|
|
9179
|
-
|
|
9180
|
-
|
|
9181
|
-
|
|
9182
|
-
|
|
9183
|
-
}
|
|
9184
|
-
findCharacterReferences(node, text, location, { isAttribute }) {
|
|
9185
|
-
const isQuerystring = isAttribute && text.includes("?");
|
|
9186
|
-
for (const match of this.getMatches(text)) {
|
|
9187
|
-
this.validateCharacterReference(node, location, match, { isQuerystring });
|
|
9188
|
-
}
|
|
9189
|
-
}
|
|
9190
|
-
validateCharacterReference(node, location, foobar, { isQuerystring }) {
|
|
9191
|
-
const { requireSemicolon } = this.options;
|
|
9192
|
-
const { match, entity, raw, terminated } = foobar;
|
|
9193
|
-
if (isNumerical(entity)) {
|
|
9194
|
-
return;
|
|
9195
|
-
}
|
|
9196
|
-
if (isQuerystring && !terminated) {
|
|
9197
|
-
return;
|
|
9198
|
-
}
|
|
9199
|
-
const found = this.entities.includes(entity);
|
|
9200
|
-
if (found && (terminated || !requireSemicolon)) {
|
|
9201
|
-
return;
|
|
9202
|
-
}
|
|
9203
|
-
if (found && !terminated) {
|
|
9204
|
-
const entityLocation2 = getLocation(location, entity, match);
|
|
9205
|
-
const message2 = `Character reference "{{ entity }}" must be terminated by a semicolon`;
|
|
9206
|
-
const context2 = {
|
|
9207
|
-
entity: raw,
|
|
9208
|
-
terminated: false
|
|
9209
|
-
};
|
|
9210
|
-
this.report(node, message2, entityLocation2, context2);
|
|
9211
|
-
return;
|
|
9212
|
-
}
|
|
9213
|
-
const entityLocation = getLocation(location, entity, match);
|
|
9214
|
-
const message = `Unrecognized character reference "{{ entity }}"`;
|
|
9215
|
-
const context = {
|
|
9216
|
-
entity: raw,
|
|
9217
|
-
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"
|
|
9218
9358
|
};
|
|
9219
|
-
this.report(node, message, entityLocation, context);
|
|
9220
9359
|
}
|
|
9221
|
-
|
|
9222
|
-
|
|
9223
|
-
|
|
9224
|
-
|
|
9225
|
-
|
|
9226
|
-
|
|
9227
|
-
|
|
9228
|
-
|
|
9229
|
-
|
|
9230
|
-
|
|
9231
|
-
|
|
9232
|
-
|
|
9233
|
-
|
|
9234
|
-
|
|
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;
|
|
9235
9381
|
}
|
|
9236
|
-
|
|
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
|
+
});
|
|
9237
9394
|
}
|
|
9238
9395
|
}
|
|
9239
9396
|
|
|
9240
|
-
const
|
|
9241
|
-
|
|
9242
|
-
"name",
|
|
9243
|
-
"honorific-prefix",
|
|
9244
|
-
"given-name",
|
|
9245
|
-
"additional-name",
|
|
9246
|
-
"family-name",
|
|
9247
|
-
"honorific-suffix",
|
|
9248
|
-
"nickname",
|
|
9249
|
-
"username",
|
|
9250
|
-
"new-password",
|
|
9251
|
-
"current-password",
|
|
9252
|
-
"one-time-code",
|
|
9253
|
-
"organization-title",
|
|
9254
|
-
"organization",
|
|
9255
|
-
"street-address",
|
|
9256
|
-
"address-line1",
|
|
9257
|
-
"address-line2",
|
|
9258
|
-
"address-line3",
|
|
9259
|
-
"address-level4",
|
|
9260
|
-
"address-level3",
|
|
9261
|
-
"address-level2",
|
|
9262
|
-
"address-level1",
|
|
9263
|
-
"country",
|
|
9264
|
-
"country-name",
|
|
9265
|
-
"postal-code",
|
|
9266
|
-
"cc-name",
|
|
9267
|
-
"cc-given-name",
|
|
9268
|
-
"cc-additional-name",
|
|
9269
|
-
"cc-family-name",
|
|
9270
|
-
"cc-number",
|
|
9271
|
-
"cc-exp",
|
|
9272
|
-
"cc-exp-month",
|
|
9273
|
-
"cc-exp-year",
|
|
9274
|
-
"cc-csc",
|
|
9275
|
-
"cc-type",
|
|
9276
|
-
"transaction-currency",
|
|
9277
|
-
"transaction-amount",
|
|
9278
|
-
"language",
|
|
9279
|
-
"bday",
|
|
9280
|
-
"bday-day",
|
|
9281
|
-
"bday-month",
|
|
9282
|
-
"bday-year",
|
|
9283
|
-
"sex",
|
|
9284
|
-
"url",
|
|
9285
|
-
"photo"
|
|
9286
|
-
];
|
|
9287
|
-
const fieldNames2 = [
|
|
9288
|
-
"tel",
|
|
9289
|
-
"tel-country-code",
|
|
9290
|
-
"tel-national",
|
|
9291
|
-
"tel-area-code",
|
|
9292
|
-
"tel-local",
|
|
9293
|
-
"tel-local-prefix",
|
|
9294
|
-
"tel-local-suffix",
|
|
9295
|
-
"tel-extension",
|
|
9296
|
-
"email",
|
|
9297
|
-
"impp"
|
|
9298
|
-
];
|
|
9299
|
-
const fieldNameGroup = {
|
|
9300
|
-
name: "text",
|
|
9301
|
-
"honorific-prefix": "text",
|
|
9302
|
-
"given-name": "text",
|
|
9303
|
-
"additional-name": "text",
|
|
9304
|
-
"family-name": "text",
|
|
9305
|
-
"honorific-suffix": "text",
|
|
9306
|
-
nickname: "text",
|
|
9307
|
-
username: "username",
|
|
9308
|
-
"new-password": "password",
|
|
9309
|
-
"current-password": "password",
|
|
9310
|
-
"one-time-code": "password",
|
|
9311
|
-
"organization-title": "text",
|
|
9312
|
-
organization: "text",
|
|
9313
|
-
"street-address": "multiline",
|
|
9314
|
-
"address-line1": "text",
|
|
9315
|
-
"address-line2": "text",
|
|
9316
|
-
"address-line3": "text",
|
|
9317
|
-
"address-level4": "text",
|
|
9318
|
-
"address-level3": "text",
|
|
9319
|
-
"address-level2": "text",
|
|
9320
|
-
"address-level1": "text",
|
|
9321
|
-
country: "text",
|
|
9322
|
-
"country-name": "text",
|
|
9323
|
-
"postal-code": "text",
|
|
9324
|
-
"cc-name": "text",
|
|
9325
|
-
"cc-given-name": "text",
|
|
9326
|
-
"cc-additional-name": "text",
|
|
9327
|
-
"cc-family-name": "text",
|
|
9328
|
-
"cc-number": "text",
|
|
9329
|
-
"cc-exp": "month",
|
|
9330
|
-
"cc-exp-month": "numeric",
|
|
9331
|
-
"cc-exp-year": "numeric",
|
|
9332
|
-
"cc-csc": "text",
|
|
9333
|
-
"cc-type": "text",
|
|
9334
|
-
"transaction-currency": "text",
|
|
9335
|
-
"transaction-amount": "numeric",
|
|
9336
|
-
language: "text",
|
|
9337
|
-
bday: "date",
|
|
9338
|
-
"bday-day": "numeric",
|
|
9339
|
-
"bday-month": "numeric",
|
|
9340
|
-
"bday-year": "numeric",
|
|
9341
|
-
sex: "text",
|
|
9342
|
-
url: "url",
|
|
9343
|
-
photo: "url",
|
|
9344
|
-
tel: "tel",
|
|
9345
|
-
"tel-country-code": "text",
|
|
9346
|
-
"tel-national": "text",
|
|
9347
|
-
"tel-area-code": "text",
|
|
9348
|
-
"tel-local": "text",
|
|
9349
|
-
"tel-local-prefix": "text",
|
|
9350
|
-
"tel-local-suffix": "text",
|
|
9351
|
-
"tel-extension": "text",
|
|
9352
|
-
email: "username",
|
|
9353
|
-
impp: "url"
|
|
9397
|
+
const defaults$2 = {
|
|
9398
|
+
style: "omit"
|
|
9354
9399
|
};
|
|
9355
|
-
|
|
9356
|
-
|
|
9357
|
-
|
|
9358
|
-
|
|
9359
|
-
function matchHint(token) {
|
|
9360
|
-
return token === "shipping" || token === "billing";
|
|
9361
|
-
}
|
|
9362
|
-
function matchFieldNames1(token) {
|
|
9363
|
-
return fieldNames1.includes(token);
|
|
9364
|
-
}
|
|
9365
|
-
function matchContact(token) {
|
|
9366
|
-
const haystack = ["home", "work", "mobile", "fax", "pager"];
|
|
9367
|
-
return haystack.includes(token);
|
|
9368
|
-
}
|
|
9369
|
-
function matchFieldNames2(token) {
|
|
9370
|
-
return fieldNames2.includes(token);
|
|
9371
|
-
}
|
|
9372
|
-
function matchWebauthn(token) {
|
|
9373
|
-
return token === "webauthn";
|
|
9374
|
-
}
|
|
9375
|
-
function matchToken(token) {
|
|
9376
|
-
if (matchSection(token)) {
|
|
9377
|
-
return "section";
|
|
9400
|
+
class VoidStyle extends Rule {
|
|
9401
|
+
constructor(options) {
|
|
9402
|
+
super({ ...defaults$2, ...options });
|
|
9403
|
+
this.style = parseStyle(this.options.style);
|
|
9378
9404
|
}
|
|
9379
|
-
|
|
9380
|
-
return
|
|
9405
|
+
static schema() {
|
|
9406
|
+
return {
|
|
9407
|
+
style: {
|
|
9408
|
+
enum: ["omit", "selfclose", "selfclosing"],
|
|
9409
|
+
type: "string"
|
|
9410
|
+
}
|
|
9411
|
+
};
|
|
9381
9412
|
}
|
|
9382
|
-
|
|
9383
|
-
|
|
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
|
+
};
|
|
9384
9419
|
}
|
|
9385
|
-
|
|
9386
|
-
|
|
9420
|
+
setup() {
|
|
9421
|
+
this.on("tag:end", (event) => {
|
|
9422
|
+
const active = event.previous;
|
|
9423
|
+
if (active.meta) {
|
|
9424
|
+
this.validateActive(active);
|
|
9425
|
+
}
|
|
9426
|
+
});
|
|
9387
9427
|
}
|
|
9388
|
-
|
|
9389
|
-
|
|
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
|
+
}
|
|
9390
9444
|
}
|
|
9391
|
-
|
|
9392
|
-
|
|
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;
|
|
9393
9457
|
}
|
|
9394
|
-
return null;
|
|
9395
9458
|
}
|
|
9396
|
-
function
|
|
9397
|
-
|
|
9398
|
-
"
|
|
9399
|
-
|
|
9400
|
-
"
|
|
9401
|
-
"
|
|
9402
|
-
|
|
9403
|
-
|
|
9404
|
-
|
|
9405
|
-
"month",
|
|
9406
|
-
"date"
|
|
9407
|
-
];
|
|
9408
|
-
const mapping = {
|
|
9409
|
-
hidden: allGroups,
|
|
9410
|
-
text: allGroups.filter((it) => it !== "multiline"),
|
|
9411
|
-
search: allGroups.filter((it) => it !== "multiline"),
|
|
9412
|
-
password: ["password"],
|
|
9413
|
-
url: ["url"],
|
|
9414
|
-
email: ["username"],
|
|
9415
|
-
tel: ["tel"],
|
|
9416
|
-
number: ["numeric"],
|
|
9417
|
-
month: ["month"],
|
|
9418
|
-
date: ["date"]
|
|
9419
|
-
};
|
|
9420
|
-
const groups = mapping[type];
|
|
9421
|
-
if (groups) {
|
|
9422
|
-
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`);
|
|
9423
9468
|
}
|
|
9424
|
-
return [];
|
|
9425
9469
|
}
|
|
9426
|
-
function
|
|
9427
|
-
|
|
9428
|
-
|
|
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`);
|
|
9429
9478
|
}
|
|
9430
|
-
return disallowedInputTypes.includes(type);
|
|
9431
9479
|
}
|
|
9432
|
-
|
|
9433
|
-
|
|
9434
|
-
|
|
9435
|
-
|
|
9436
|
-
|
|
9437
|
-
|
|
9438
|
-
|
|
9439
|
-
return '"{{ second }}" must appear before "{{ first }}"';
|
|
9440
|
-
case 3 /* InvalidToken */:
|
|
9441
|
-
return '"{{ token }}" is not a valid autocomplete token or field name';
|
|
9442
|
-
case 4 /* InvalidCombination */:
|
|
9443
|
-
return '"{{ second }}" cannot be combined with "{{ first }}"';
|
|
9444
|
-
case 5 /* MissingField */:
|
|
9445
|
-
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
|
+
};
|
|
9446
9487
|
}
|
|
9447
|
-
|
|
9448
|
-
|
|
9449
|
-
|
|
9450
|
-
|
|
9451
|
-
|
|
9452
|
-
|
|
9453
|
-
|
|
9454
|
-
|
|
9455
|
-
|
|
9456
|
-
|
|
9457
|
-
|
|
9458
|
-
|
|
9459
|
-
|
|
9460
|
-
|
|
9461
|
-
|
|
9462
|
-
|
|
9463
|
-
|
|
9464
|
-
|
|
9465
|
-
|
|
9466
|
-
|
|
9467
|
-
|
|
9468
|
-
|
|
9469
|
-
|
|
9470
|
-
"",
|
|
9471
|
-
'`<input type="hidden">` cannot use the values `"on"` and `"off"`.'
|
|
9472
|
-
].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");
|
|
9473
9511
|
}
|
|
9474
|
-
|
|
9475
|
-
const currentGroup = fieldNameGroup[context.value];
|
|
9476
|
-
return [
|
|
9477
|
-
message,
|
|
9478
|
-
"",
|
|
9479
|
-
`\`${context.what}\` allows autocomplete fields from the following group${controlGroups.length > 1 ? "s" : ""}:`,
|
|
9480
|
-
"",
|
|
9481
|
-
...controlGroups.map((it) => `- ${it}`),
|
|
9482
|
-
"",
|
|
9483
|
-
`The field \`"${context.value}"\` belongs to the group /${currentGroup}/ which cannot be used with this input type.`
|
|
9484
|
-
].join("\n");
|
|
9485
|
-
}
|
|
9486
|
-
case 2 /* InvalidOrder */:
|
|
9487
|
-
return [
|
|
9488
|
-
`\`"${context.second}"\` must appear before \`"${context.first}"\``,
|
|
9489
|
-
"",
|
|
9490
|
-
"The autocomplete tokens must appear in the following order:",
|
|
9491
|
-
"",
|
|
9492
|
-
"- Optional section name (`section-` prefix).",
|
|
9493
|
-
"- Optional `shipping` or `billing` token.",
|
|
9494
|
-
"- Optional `home`, `work`, `mobile`, `fax` or `pager` token (for fields supporting it).",
|
|
9495
|
-
"- Field name",
|
|
9496
|
-
"- Optional `webauthn` token."
|
|
9497
|
-
].join("\n");
|
|
9498
|
-
case 3 /* InvalidToken */:
|
|
9499
|
-
return `\`"${context.token}"\` is not a valid autocomplete token or field name`;
|
|
9500
|
-
case 4 /* InvalidCombination */:
|
|
9501
|
-
return `\`"${context.second}"\` cannot be combined with \`"${context.first}"\``;
|
|
9502
|
-
case 5 /* MissingField */:
|
|
9503
|
-
return "Autocomplete attribute is missing field name";
|
|
9512
|
+
});
|
|
9504
9513
|
}
|
|
9505
9514
|
}
|
|
9506
|
-
|
|
9507
|
-
|
|
9515
|
+
|
|
9516
|
+
class H32 extends Rule {
|
|
9517
|
+
documentation() {
|
|
9508
9518
|
return {
|
|
9509
|
-
description:
|
|
9510
|
-
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"
|
|
9511
9521
|
};
|
|
9512
9522
|
}
|
|
9513
9523
|
setup() {
|
|
9524
|
+
const formTags = this.getTagsWithProperty("form");
|
|
9525
|
+
const formSelector = formTags.join(",");
|
|
9514
9526
|
this.on("dom:ready", (event) => {
|
|
9515
9527
|
const { document } = event;
|
|
9516
|
-
const
|
|
9517
|
-
for (const
|
|
9518
|
-
|
|
9519
|
-
if (autocomplete.value === null || autocomplete.value instanceof DynamicValue) {
|
|
9528
|
+
const forms = document.querySelectorAll(formSelector);
|
|
9529
|
+
for (const form of forms) {
|
|
9530
|
+
if (hasNestedSubmit(form)) {
|
|
9520
9531
|
continue;
|
|
9521
9532
|
}
|
|
9522
|
-
|
|
9523
|
-
const value = autocomplete.value.toLowerCase();
|
|
9524
|
-
const tokens = new DOMTokenList(value, location);
|
|
9525
|
-
if (tokens.length === 0) {
|
|
9533
|
+
if (hasAssociatedSubmit(document, form)) {
|
|
9526
9534
|
continue;
|
|
9527
9535
|
}
|
|
9528
|
-
this.
|
|
9536
|
+
this.report(form, `<${form.tagName}> element must have a submit button`);
|
|
9529
9537
|
}
|
|
9530
9538
|
});
|
|
9531
9539
|
}
|
|
9532
|
-
|
|
9533
|
-
|
|
9534
|
-
|
|
9535
|
-
|
|
9536
|
-
|
|
9537
|
-
|
|
9538
|
-
|
|
9539
|
-
|
|
9540
|
-
|
|
9541
|
-
|
|
9542
|
-
|
|
9543
|
-
|
|
9544
|
-
|
|
9545
|
-
|
|
9546
|
-
|
|
9547
|
-
|
|
9548
|
-
|
|
9549
|
-
msg: 0 /* InvalidAttribute */,
|
|
9550
|
-
what: `<input type="${type}">`
|
|
9551
|
-
};
|
|
9552
|
-
this.report({
|
|
9553
|
-
node,
|
|
9554
|
-
message: getTerminalMessage(context),
|
|
9555
|
-
location: keyLocation,
|
|
9556
|
-
context
|
|
9557
|
-
});
|
|
9558
|
-
return;
|
|
9559
|
-
}
|
|
9560
|
-
if (tokens.includes("on") || tokens.includes("off")) {
|
|
9561
|
-
this.validateOnOff(node, mantle, tokens);
|
|
9562
|
-
return;
|
|
9563
|
-
}
|
|
9564
|
-
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;
|
|
9565
9557
|
}
|
|
9566
|
-
|
|
9567
|
-
|
|
9568
|
-
|
|
9569
|
-
|
|
9570
|
-
|
|
9571
|
-
|
|
9572
|
-
|
|
9573
|
-
|
|
9574
|
-
|
|
9575
|
-
|
|
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"
|
|
9576
9570
|
};
|
|
9577
|
-
this.report({
|
|
9578
|
-
node,
|
|
9579
|
-
message: getTerminalMessage(context),
|
|
9580
|
-
location,
|
|
9581
|
-
context
|
|
9582
|
-
});
|
|
9583
9571
|
}
|
|
9584
|
-
|
|
9585
|
-
|
|
9586
|
-
|
|
9587
|
-
|
|
9588
|
-
|
|
9589
|
-
|
|
9590
|
-
|
|
9591
|
-
/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
|
|
9592
|
-
first: tokens.item(index > 0 ? 0 : 1),
|
|
9593
|
-
second: value
|
|
9594
|
-
};
|
|
9595
|
-
this.report({
|
|
9596
|
-
node,
|
|
9597
|
-
message: getTerminalMessage(context),
|
|
9598
|
-
location,
|
|
9599
|
-
context
|
|
9600
|
-
});
|
|
9601
|
-
}
|
|
9602
|
-
switch (mantle) {
|
|
9603
|
-
case "expectation":
|
|
9604
|
-
return;
|
|
9605
|
-
case "anchor": {
|
|
9606
|
-
const context = {
|
|
9607
|
-
msg: 1 /* InvalidValue */,
|
|
9608
|
-
type: "hidden",
|
|
9609
|
-
value,
|
|
9610
|
-
what: `<input type="hidden">`
|
|
9611
|
-
};
|
|
9612
|
-
this.report({
|
|
9613
|
-
node,
|
|
9614
|
-
message: getTerminalMessage(context),
|
|
9615
|
-
location: tokens.location(0),
|
|
9616
|
-
context
|
|
9617
|
-
});
|
|
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;
|
|
9618
9579
|
}
|
|
9619
|
-
|
|
9620
|
-
}
|
|
9621
|
-
validateTokens(node, tokens, keyLocation) {
|
|
9622
|
-
const order = [];
|
|
9623
|
-
for (const { item, location } of tokens.iterator()) {
|
|
9624
|
-
const tokenType = matchToken(item);
|
|
9625
|
-
if (tokenType) {
|
|
9626
|
-
order.push(tokenType);
|
|
9627
|
-
} else {
|
|
9628
|
-
const context = {
|
|
9629
|
-
msg: 3 /* InvalidToken */,
|
|
9630
|
-
token: item
|
|
9631
|
-
};
|
|
9632
|
-
this.report({
|
|
9633
|
-
node,
|
|
9634
|
-
message: getTerminalMessage(context),
|
|
9635
|
-
location,
|
|
9636
|
-
context
|
|
9637
|
-
});
|
|
9580
|
+
if (!inAccessibilityTree(node)) {
|
|
9638
9581
|
return;
|
|
9639
9582
|
}
|
|
9640
|
-
|
|
9641
|
-
|
|
9642
|
-
|
|
9643
|
-
this.validateContact(node, tokens, order);
|
|
9644
|
-
this.validateOrder(node, tokens, order);
|
|
9645
|
-
this.validateControlGroup(node, tokens, fieldTokens);
|
|
9646
|
-
}
|
|
9647
|
-
/**
|
|
9648
|
-
* Ensure that exactly one field name is present from the two field lists.
|
|
9649
|
-
*/
|
|
9650
|
-
validateFieldPresence(node, tokens, fieldTokens, keyLocation) {
|
|
9651
|
-
const numFields = fieldTokens.filter(Boolean).length;
|
|
9652
|
-
if (numFields === 0) {
|
|
9653
|
-
const context = {
|
|
9654
|
-
msg: 5 /* MissingField */
|
|
9655
|
-
};
|
|
9656
|
-
this.report({
|
|
9657
|
-
node,
|
|
9658
|
-
message: getTerminalMessage(context),
|
|
9659
|
-
location: keyLocation,
|
|
9660
|
-
context
|
|
9661
|
-
});
|
|
9662
|
-
} else if (numFields > 1) {
|
|
9663
|
-
const a = fieldTokens.indexOf(true);
|
|
9664
|
-
const b = fieldTokens.lastIndexOf(true);
|
|
9665
|
-
const context = {
|
|
9666
|
-
msg: 4 /* InvalidCombination */,
|
|
9667
|
-
/* eslint-disable @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
|
|
9668
|
-
first: tokens.item(a),
|
|
9669
|
-
second: tokens.item(b)
|
|
9670
|
-
/* eslint-enable @typescript-eslint/no-non-null-assertion */
|
|
9671
|
-
};
|
|
9672
|
-
this.report({
|
|
9673
|
-
node,
|
|
9674
|
-
message: getTerminalMessage(context),
|
|
9675
|
-
location: tokens.location(b),
|
|
9676
|
-
context
|
|
9677
|
-
});
|
|
9678
|
-
}
|
|
9679
|
-
}
|
|
9680
|
-
/**
|
|
9681
|
-
* Ensure contact token is only used with field names from the second list.
|
|
9682
|
-
*/
|
|
9683
|
-
validateContact(node, tokens, order) {
|
|
9684
|
-
if (order.includes("contact") && order.includes("field1")) {
|
|
9685
|
-
const a = order.indexOf("field1");
|
|
9686
|
-
const b = order.indexOf("contact");
|
|
9687
|
-
const context = {
|
|
9688
|
-
msg: 4 /* InvalidCombination */,
|
|
9689
|
-
/* eslint-disable @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
|
|
9690
|
-
first: tokens.item(a),
|
|
9691
|
-
second: tokens.item(b)
|
|
9692
|
-
/* eslint-enable @typescript-eslint/no-non-null-assertion */
|
|
9693
|
-
};
|
|
9694
|
-
this.report({
|
|
9695
|
-
node,
|
|
9696
|
-
message: getTerminalMessage(context),
|
|
9697
|
-
location: tokens.location(b),
|
|
9698
|
-
context
|
|
9699
|
-
});
|
|
9700
|
-
}
|
|
9701
|
-
}
|
|
9702
|
-
validateOrder(node, tokens, order) {
|
|
9703
|
-
const indicies = order.map((it) => expectedOrder.indexOf(it));
|
|
9704
|
-
for (let i = 0; i < indicies.length - 1; i++) {
|
|
9705
|
-
if (indicies[0] > indicies[i + 1]) {
|
|
9706
|
-
const context = {
|
|
9707
|
-
msg: 2 /* InvalidOrder */,
|
|
9708
|
-
/* eslint-disable @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
|
|
9709
|
-
first: tokens.item(i),
|
|
9710
|
-
second: tokens.item(i + 1)
|
|
9711
|
-
/* eslint-enable @typescript-eslint/no-non-null-assertion */
|
|
9712
|
-
};
|
|
9583
|
+
if (!hasAltText(node)) {
|
|
9584
|
+
const message = "image used as submit button must have non-empty alt text";
|
|
9585
|
+
const alt = node.getAttribute("alt");
|
|
9713
9586
|
this.report({
|
|
9714
9587
|
node,
|
|
9715
|
-
message
|
|
9716
|
-
location:
|
|
9717
|
-
context
|
|
9588
|
+
message,
|
|
9589
|
+
location: alt ? alt.keyLocation : node.location
|
|
9718
9590
|
});
|
|
9719
9591
|
}
|
|
9720
|
-
}
|
|
9721
|
-
}
|
|
9722
|
-
validateControlGroup(node, tokens, fieldTokens) {
|
|
9723
|
-
const numFields = fieldTokens.filter(Boolean).length;
|
|
9724
|
-
if (numFields === 0) {
|
|
9725
|
-
return;
|
|
9726
|
-
}
|
|
9727
|
-
if (!node.is("input")) {
|
|
9728
|
-
return;
|
|
9729
|
-
}
|
|
9730
|
-
const attr = node.getAttribute("type");
|
|
9731
|
-
const type = (attr == null ? void 0 : attr.value) ?? "text";
|
|
9732
|
-
if (type instanceof DynamicValue) {
|
|
9733
|
-
return;
|
|
9734
|
-
}
|
|
9735
|
-
const controlGroups = getControlGroups(type);
|
|
9736
|
-
const fieldIndex = fieldTokens.indexOf(true);
|
|
9737
|
-
const fieldToken = tokens.item(fieldIndex);
|
|
9738
|
-
const fieldGroup = fieldNameGroup[fieldToken];
|
|
9739
|
-
if (!controlGroups.includes(fieldGroup)) {
|
|
9740
|
-
const context = {
|
|
9741
|
-
msg: 1 /* InvalidValue */,
|
|
9742
|
-
type,
|
|
9743
|
-
value: fieldToken,
|
|
9744
|
-
what: `<input type="${type}">`
|
|
9745
|
-
};
|
|
9746
|
-
this.report({
|
|
9747
|
-
node,
|
|
9748
|
-
message: getTerminalMessage(context),
|
|
9749
|
-
location: tokens.location(fieldIndex),
|
|
9750
|
-
context
|
|
9751
|
-
});
|
|
9752
|
-
}
|
|
9592
|
+
});
|
|
9753
9593
|
}
|
|
9754
9594
|
}
|
|
9755
9595
|
|
|
9756
|
-
const defaults$
|
|
9757
|
-
|
|
9596
|
+
const defaults$1 = {
|
|
9597
|
+
allowEmpty: true,
|
|
9598
|
+
alias: []
|
|
9758
9599
|
};
|
|
9759
|
-
class
|
|
9600
|
+
class H37 extends Rule {
|
|
9760
9601
|
constructor(options) {
|
|
9761
|
-
super({ ...defaults$
|
|
9602
|
+
super({ ...defaults$1, ...options });
|
|
9603
|
+
if (!Array.isArray(this.options.alias)) {
|
|
9604
|
+
this.options.alias = [this.options.alias];
|
|
9605
|
+
}
|
|
9762
9606
|
}
|
|
9763
9607
|
static schema() {
|
|
9764
9608
|
return {
|
|
9765
|
-
|
|
9609
|
+
alias: {
|
|
9610
|
+
anyOf: [
|
|
9611
|
+
{
|
|
9612
|
+
items: {
|
|
9613
|
+
type: "string"
|
|
9614
|
+
},
|
|
9615
|
+
type: "array"
|
|
9616
|
+
},
|
|
9617
|
+
{
|
|
9618
|
+
type: "string"
|
|
9619
|
+
}
|
|
9620
|
+
]
|
|
9621
|
+
},
|
|
9622
|
+
allowEmpty: {
|
|
9766
9623
|
type: "boolean"
|
|
9767
9624
|
}
|
|
9768
9625
|
};
|
|
9769
9626
|
}
|
|
9770
|
-
documentation(
|
|
9771
|
-
const { relaxed } = this.options;
|
|
9772
|
-
const message = this.messages[context].replace("id", "ID").replace(/^(.)/, (m) => m.toUpperCase());
|
|
9773
|
-
const relaxedDescription = relaxed ? [] : [
|
|
9774
|
-
" - ID must begin with a letter",
|
|
9775
|
-
" - ID must only contain letters, digits, `-` and `_`"
|
|
9776
|
-
];
|
|
9627
|
+
documentation() {
|
|
9777
9628
|
return {
|
|
9778
|
-
description:
|
|
9779
|
-
|
|
9780
|
-
"",
|
|
9781
|
-
"Under the current configuration the following rules are applied:",
|
|
9782
|
-
"",
|
|
9783
|
-
" - ID must not be empty",
|
|
9784
|
-
" - ID must not contain any whitespace characters",
|
|
9785
|
-
...relaxedDescription
|
|
9786
|
-
].join("\n"),
|
|
9787
|
-
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"
|
|
9788
9631
|
};
|
|
9789
9632
|
}
|
|
9790
9633
|
setup() {
|
|
9791
|
-
this.on("
|
|
9792
|
-
const {
|
|
9793
|
-
|
|
9794
|
-
|
|
9795
|
-
|
|
9796
|
-
if (value === "") {
|
|
9797
|
-
const context = 1 /* EMPTY */;
|
|
9798
|
-
this.report(event.target, this.messages[context], event.location, context);
|
|
9799
|
-
return;
|
|
9800
|
-
}
|
|
9801
|
-
if (value.match(/\s/)) {
|
|
9802
|
-
const context = 2 /* WHITESPACE */;
|
|
9803
|
-
this.report(event.target, this.messages[context], event.valueLocation, context);
|
|
9804
|
-
return;
|
|
9805
|
-
}
|
|
9806
|
-
const { relaxed } = this.options;
|
|
9807
|
-
if (relaxed) {
|
|
9808
|
-
return;
|
|
9809
|
-
}
|
|
9810
|
-
if (value.match(/^[^\p{L}]/u)) {
|
|
9811
|
-
const context = 3 /* LEADING_CHARACTER */;
|
|
9812
|
-
this.report(event.target, this.messages[context], event.valueLocation, context);
|
|
9813
|
-
return;
|
|
9814
|
-
}
|
|
9815
|
-
if (value.match(/[^\p{L}\p{N}_-]/u)) {
|
|
9816
|
-
const context = 4 /* DISALLOWED_CHARACTER */;
|
|
9817
|
-
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);
|
|
9818
9639
|
}
|
|
9819
9640
|
});
|
|
9820
9641
|
}
|
|
9821
|
-
|
|
9822
|
-
|
|
9823
|
-
|
|
9824
|
-
|
|
9825
|
-
|
|
9826
|
-
|
|
9827
|
-
}
|
|
9828
|
-
|
|
9829
|
-
|
|
9830
|
-
|
|
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
|
+
}
|
|
9831
9661
|
}
|
|
9832
9662
|
}
|
|
9833
9663
|
|
|
9834
|
-
|
|
9835
|
-
|
|
9836
|
-
|
|
9837
|
-
|
|
9838
|
-
|
|
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"
|
|
9839
9672
|
};
|
|
9840
|
-
if (tagName) {
|
|
9841
|
-
doc.description = `<${tagName}> is a void element and must not have content or end tag.`;
|
|
9842
|
-
}
|
|
9843
|
-
return doc;
|
|
9844
9673
|
}
|
|
9845
9674
|
setup() {
|
|
9846
|
-
this.on("tag:
|
|
9675
|
+
this.on("tag:ready", (event) => {
|
|
9847
9676
|
const node = event.target;
|
|
9848
|
-
if (
|
|
9677
|
+
if (node.tagName !== "th") {
|
|
9849
9678
|
return;
|
|
9850
9679
|
}
|
|
9851
|
-
|
|
9680
|
+
const scope = node.getAttribute("scope");
|
|
9681
|
+
const value = scope == null ? void 0 : scope.value;
|
|
9682
|
+
if (value instanceof DynamicValue) {
|
|
9852
9683
|
return;
|
|
9853
9684
|
}
|
|
9854
|
-
if (
|
|
9855
|
-
|
|
9856
|
-
null,
|
|
9857
|
-
`End tag for <${node.tagName}> must be omitted`,
|
|
9858
|
-
node.location,
|
|
9859
|
-
node.tagName
|
|
9860
|
-
);
|
|
9685
|
+
if (value && validScopes.includes(value)) {
|
|
9686
|
+
return;
|
|
9861
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);
|
|
9862
9691
|
});
|
|
9863
9692
|
}
|
|
9864
9693
|
}
|
|
9865
9694
|
|
|
9866
|
-
|
|
9867
|
-
|
|
9868
|
-
};
|
|
9869
|
-
class VoidStyle extends Rule {
|
|
9870
|
-
constructor(options) {
|
|
9871
|
-
super({ ...defaults$2, ...options });
|
|
9872
|
-
this.style = parseStyle(this.options.style);
|
|
9873
|
-
}
|
|
9874
|
-
static schema() {
|
|
9875
|
-
return {
|
|
9876
|
-
style: {
|
|
9877
|
-
enum: ["omit", "selfclose", "selfclosing"],
|
|
9878
|
-
type: "string"
|
|
9879
|
-
}
|
|
9880
|
-
};
|
|
9881
|
-
}
|
|
9882
|
-
documentation(context) {
|
|
9883
|
-
const [desc, end] = styleDescription(context.style);
|
|
9695
|
+
class H67 extends Rule {
|
|
9696
|
+
documentation() {
|
|
9884
9697
|
return {
|
|
9885
|
-
description:
|
|
9886
|
-
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"
|
|
9887
9700
|
};
|
|
9888
9701
|
}
|
|
9889
9702
|
setup() {
|
|
9890
9703
|
this.on("tag:end", (event) => {
|
|
9891
|
-
const
|
|
9892
|
-
if (
|
|
9893
|
-
|
|
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;
|
|
9894
9715
|
}
|
|
9716
|
+
this.report(node, "<img> with empty alt text cannot have title attribute", title.keyLocation);
|
|
9895
9717
|
});
|
|
9896
9718
|
}
|
|
9897
|
-
validateActive(node) {
|
|
9898
|
-
if (!node.voidElement) {
|
|
9899
|
-
return;
|
|
9900
|
-
}
|
|
9901
|
-
if (this.shouldBeOmitted(node)) {
|
|
9902
|
-
this.reportError(
|
|
9903
|
-
node,
|
|
9904
|
-
`Expected omitted end tag <${node.tagName}> instead of self-closing element <${node.tagName}/>`
|
|
9905
|
-
);
|
|
9906
|
-
}
|
|
9907
|
-
if (this.shouldBeSelfClosed(node)) {
|
|
9908
|
-
this.reportError(
|
|
9909
|
-
node,
|
|
9910
|
-
`Expected self-closing element <${node.tagName}/> instead of omitted end-tag <${node.tagName}>`
|
|
9911
|
-
);
|
|
9912
|
-
}
|
|
9913
|
-
}
|
|
9914
|
-
reportError(node, message) {
|
|
9915
|
-
const context = {
|
|
9916
|
-
style: this.style,
|
|
9917
|
-
tagName: node.tagName
|
|
9918
|
-
};
|
|
9919
|
-
super.report(node, message, null, context);
|
|
9920
|
-
}
|
|
9921
|
-
shouldBeOmitted(node) {
|
|
9922
|
-
return this.style === 1 /* AlwaysOmit */ && node.closed === NodeClosed.VoidSelfClosed;
|
|
9923
|
-
}
|
|
9924
|
-
shouldBeSelfClosed(node) {
|
|
9925
|
-
return this.style === 2 /* AlwaysSelfclose */ && node.closed === NodeClosed.VoidOmitted;
|
|
9926
|
-
}
|
|
9927
|
-
}
|
|
9928
|
-
function parseStyle(name) {
|
|
9929
|
-
switch (name) {
|
|
9930
|
-
case "omit":
|
|
9931
|
-
return 1 /* AlwaysOmit */;
|
|
9932
|
-
case "selfclose":
|
|
9933
|
-
case "selfclosing":
|
|
9934
|
-
return 2 /* AlwaysSelfclose */;
|
|
9935
|
-
default:
|
|
9936
|
-
throw new Error(`Invalid style "${name}" for "void-style" rule`);
|
|
9937
|
-
}
|
|
9938
|
-
}
|
|
9939
|
-
function styleDescription(style) {
|
|
9940
|
-
switch (style) {
|
|
9941
|
-
case 1 /* AlwaysOmit */:
|
|
9942
|
-
return ["omit end tag", ""];
|
|
9943
|
-
case 2 /* AlwaysSelfclose */:
|
|
9944
|
-
return ["be self-closed", "/"];
|
|
9945
|
-
default:
|
|
9946
|
-
throw new Error(`Unknown style`);
|
|
9947
|
-
}
|
|
9948
9719
|
}
|
|
9949
9720
|
|
|
9950
|
-
class
|
|
9721
|
+
class H71 extends Rule {
|
|
9951
9722
|
documentation() {
|
|
9952
9723
|
return {
|
|
9953
|
-
description: "
|
|
9954
|
-
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"
|
|
9955
9726
|
};
|
|
9956
9727
|
}
|
|
9957
9728
|
setup() {
|
|
9958
9729
|
this.on("dom:ready", (event) => {
|
|
9959
|
-
const
|
|
9960
|
-
|
|
9961
|
-
|
|
9962
|
-
|
|
9963
|
-
}
|
|
9964
|
-
if (!inAccessibilityTree(link)) {
|
|
9965
|
-
continue;
|
|
9966
|
-
}
|
|
9967
|
-
const textClassification = classifyNodeText(link, { ignoreHiddenRoot: true });
|
|
9968
|
-
if (textClassification !== TextClassification.EMPTY_TEXT) {
|
|
9969
|
-
continue;
|
|
9970
|
-
}
|
|
9971
|
-
const images = link.querySelectorAll("img");
|
|
9972
|
-
if (images.some((image) => hasAltText(image))) {
|
|
9973
|
-
continue;
|
|
9974
|
-
}
|
|
9975
|
-
const labels = link.querySelectorAll("[aria-label]");
|
|
9976
|
-
if (hasAriaLabel(link) || labels.some((cur) => hasAriaLabel(cur))) {
|
|
9977
|
-
continue;
|
|
9978
|
-
}
|
|
9979
|
-
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);
|
|
9980
9734
|
}
|
|
9981
9735
|
});
|
|
9982
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);
|
|
9983
9851
|
}
|
|
9984
9852
|
|
|
9985
|
-
|
|
9986
|
-
|
|
9987
|
-
|
|
9988
|
-
|
|
9989
|
-
|
|
9990
|
-
|
|
9991
|
-
}
|
|
9992
|
-
setup() {
|
|
9993
|
-
const formTags = this.getTagsWithProperty("form");
|
|
9994
|
-
const formSelector = formTags.join(",");
|
|
9995
|
-
this.on("dom:ready", (event) => {
|
|
9996
|
-
const { document } = event;
|
|
9997
|
-
const forms = document.querySelectorAll(formSelector);
|
|
9998
|
-
for (const form of forms) {
|
|
9999
|
-
if (hasNestedSubmit(form)) {
|
|
10000
|
-
continue;
|
|
10001
|
-
}
|
|
10002
|
-
if (hasAssociatedSubmit(document, form)) {
|
|
10003
|
-
continue;
|
|
10004
|
-
}
|
|
10005
|
-
this.report(form, `<${form.tagName}> element must have a submit button`);
|
|
10006
|
-
}
|
|
10007
|
-
});
|
|
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;
|
|
10008
9859
|
}
|
|
10009
|
-
|
|
10010
|
-
|
|
10011
|
-
|
|
10012
|
-
|
|
10013
|
-
}
|
|
10014
|
-
function isAssociated(id, node) {
|
|
10015
|
-
const form = node.getAttribute("form");
|
|
10016
|
-
return Boolean(form == null ? void 0 : form.valueMatches(id, true));
|
|
10017
|
-
}
|
|
10018
|
-
function hasNestedSubmit(form) {
|
|
10019
|
-
const matches = form.querySelectorAll("button,input").filter(isSubmit).filter((node) => !node.hasAttribute("form"));
|
|
10020
|
-
return matches.length > 0;
|
|
10021
|
-
}
|
|
10022
|
-
function hasAssociatedSubmit(document, form) {
|
|
10023
|
-
const { id } = form;
|
|
10024
|
-
if (!id) {
|
|
10025
|
-
return false;
|
|
9860
|
+
function visit(node) {
|
|
9861
|
+
node.childElements.forEach(visit);
|
|
9862
|
+
if (!node.isRootElement()) {
|
|
9863
|
+
callback(node);
|
|
9864
|
+
}
|
|
10026
9865
|
}
|
|
10027
|
-
|
|
10028
|
-
return matches.length > 0;
|
|
9866
|
+
visit(root);
|
|
10029
9867
|
}
|
|
9868
|
+
const walk = {
|
|
9869
|
+
depthFirst
|
|
9870
|
+
};
|
|
10030
9871
|
|
|
10031
|
-
class
|
|
10032
|
-
|
|
10033
|
-
|
|
10034
|
-
|
|
10035
|
-
|
|
10036
|
-
|
|
10037
|
-
|
|
10038
|
-
|
|
10039
|
-
|
|
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";
|
|
10040
9881
|
}
|
|
10041
|
-
|
|
10042
|
-
|
|
10043
|
-
|
|
10044
|
-
|
|
10045
|
-
|
|
10046
|
-
|
|
10047
|
-
|
|
10048
|
-
|
|
10049
|
-
|
|
10050
|
-
|
|
10051
|
-
|
|
10052
|
-
|
|
10053
|
-
|
|
10054
|
-
|
|
10055
|
-
|
|
10056
|
-
|
|
10057
|
-
|
|
10058
|
-
|
|
10059
|
-
|
|
10060
|
-
|
|
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);
|
|
10061
9921
|
});
|
|
10062
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
|
+
}
|
|
10063
9944
|
}
|
|
10064
9945
|
|
|
10065
|
-
const
|
|
10066
|
-
|
|
10067
|
-
|
|
10068
|
-
|
|
10069
|
-
|
|
10070
|
-
|
|
10071
|
-
|
|
10072
|
-
|
|
10073
|
-
|
|
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;
|
|
10074
9958
|
}
|
|
9959
|
+
return rules.some((rule) => {
|
|
9960
|
+
return Validator.validatePermittedRule(node, rule);
|
|
9961
|
+
});
|
|
10075
9962
|
}
|
|
10076
|
-
|
|
10077
|
-
|
|
10078
|
-
|
|
10079
|
-
|
|
10080
|
-
|
|
10081
|
-
|
|
10082
|
-
|
|
10083
|
-
|
|
10084
|
-
|
|
10085
|
-
|
|
10086
|
-
|
|
10087
|
-
|
|
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);
|
|
10088
9993
|
}
|
|
10089
|
-
|
|
10090
|
-
|
|
10091
|
-
allowEmpty: {
|
|
10092
|
-
type: "boolean"
|
|
9994
|
+
valid = false;
|
|
9995
|
+
}
|
|
10093
9996
|
}
|
|
10094
|
-
}
|
|
10095
|
-
|
|
10096
|
-
documentation() {
|
|
10097
|
-
return {
|
|
10098
|
-
description: "Both HTML5 and WCAG 2.0 requires images to have a alternative text for each image.",
|
|
10099
|
-
url: "https://html-validate.org/rules/wcag/h37.html"
|
|
10100
|
-
};
|
|
9997
|
+
}
|
|
9998
|
+
return valid;
|
|
10101
9999
|
}
|
|
10102
|
-
|
|
10103
|
-
|
|
10104
|
-
|
|
10105
|
-
|
|
10106
|
-
|
|
10107
|
-
|
|
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;
|
|
10108
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;
|
|
10109
10067
|
});
|
|
10110
10068
|
}
|
|
10111
|
-
|
|
10112
|
-
|
|
10113
|
-
|
|
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;
|
|
10114
10080
|
}
|
|
10115
|
-
|
|
10116
|
-
|
|
10081
|
+
const value = attr.value;
|
|
10082
|
+
if (value instanceof DynamicValue) {
|
|
10083
|
+
return true;
|
|
10117
10084
|
}
|
|
10118
|
-
|
|
10119
|
-
|
|
10120
|
-
|
|
10121
|
-
}
|
|
10085
|
+
const empty = value === null || value === "";
|
|
10086
|
+
if (rule.boolean) {
|
|
10087
|
+
return empty || value === attr.key;
|
|
10122
10088
|
}
|
|
10123
|
-
|
|
10124
|
-
|
|
10125
|
-
const attr = node.getAttribute("alt");
|
|
10126
|
-
this.report(node, `${tag} cannot have empty "alt" attribute`, attr.keyLocation);
|
|
10127
|
-
} else {
|
|
10128
|
-
this.report(node, `${tag} is missing required "alt" attribute`, node.location);
|
|
10089
|
+
if (rule.omit && empty) {
|
|
10090
|
+
return true;
|
|
10129
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);
|
|
10130
10099
|
}
|
|
10131
|
-
|
|
10132
|
-
|
|
10133
|
-
|
|
10134
|
-
|
|
10135
|
-
|
|
10136
|
-
|
|
10137
|
-
|
|
10138
|
-
|
|
10139
|
-
|
|
10140
|
-
|
|
10141
|
-
|
|
10142
|
-
|
|
10143
|
-
|
|
10144
|
-
this.on("tag:ready", (event) => {
|
|
10145
|
-
const node = event.target;
|
|
10146
|
-
if (node.tagName !== "th") {
|
|
10147
|
-
return;
|
|
10148
|
-
}
|
|
10149
|
-
const scope = node.getAttribute("scope");
|
|
10150
|
-
const value = scope == null ? void 0 : scope.value;
|
|
10151
|
-
if (value instanceof DynamicValue) {
|
|
10152
|
-
return;
|
|
10153
|
-
}
|
|
10154
|
-
if (value && validScopes.includes(value)) {
|
|
10155
|
-
return;
|
|
10156
|
-
}
|
|
10157
|
-
const message = `<th> element must have a valid scope attribute: ${joinedScopes}`;
|
|
10158
|
-
const location = (scope == null ? void 0 : scope.valueLocation) ?? (scope == null ? void 0 : scope.keyLocation) ?? node.location;
|
|
10159
|
-
this.report(node, message, location);
|
|
10160
|
-
});
|
|
10161
|
-
}
|
|
10162
|
-
}
|
|
10163
|
-
|
|
10164
|
-
class H67 extends Rule {
|
|
10165
|
-
documentation() {
|
|
10166
|
-
return {
|
|
10167
|
-
description: "A decorative image cannot have a title attribute. Either remove `title` or add a descriptive `alt` text.",
|
|
10168
|
-
url: "https://html-validate.org/rules/wcag/h67.html"
|
|
10169
|
-
};
|
|
10170
|
-
}
|
|
10171
|
-
setup() {
|
|
10172
|
-
this.on("tag:end", (event) => {
|
|
10173
|
-
const node = event.target;
|
|
10174
|
-
if (!node || node.tagName !== "img") {
|
|
10175
|
-
return;
|
|
10176
|
-
}
|
|
10177
|
-
const title = node.getAttribute("title");
|
|
10178
|
-
if (!title || title.value === "") {
|
|
10179
|
-
return;
|
|
10180
|
-
}
|
|
10181
|
-
const alt = node.getAttributeValue("alt");
|
|
10182
|
-
if (alt && alt !== "") {
|
|
10183
|
-
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;
|
|
10184
10113
|
}
|
|
10185
|
-
this.report(node, "<img> with empty alt text cannot have title attribute", title.keyLocation);
|
|
10186
10114
|
});
|
|
10187
10115
|
}
|
|
10188
|
-
|
|
10189
|
-
|
|
10190
|
-
|
|
10191
|
-
|
|
10192
|
-
|
|
10193
|
-
|
|
10194
|
-
|
|
10195
|
-
}
|
|
10196
|
-
|
|
10197
|
-
|
|
10198
|
-
|
|
10199
|
-
|
|
10200
|
-
|
|
10201
|
-
|
|
10202
|
-
|
|
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;
|
|
10203
10135
|
}
|
|
10204
|
-
});
|
|
10205
|
-
}
|
|
10206
|
-
validate(fieldset) {
|
|
10207
|
-
const legend = fieldset.querySelectorAll("> legend");
|
|
10208
|
-
if (legend.length === 0) {
|
|
10209
|
-
this.reportNode(fieldset);
|
|
10210
10136
|
}
|
|
10211
10137
|
}
|
|
10212
|
-
|
|
10213
|
-
|
|
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
|
+
}
|
|
10214
10181
|
}
|
|
10215
|
-
|
|
10216
|
-
|
|
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
|
+
}
|
|
10217
10189
|
}
|
|
10218
10190
|
}
|
|
10219
|
-
|
|
10220
|
-
|
|
10221
|
-
|
|
10222
|
-
|
|
10223
|
-
|
|
10224
|
-
|
|
10225
|
-
|
|
10226
|
-
|
|
10227
|
-
|
|
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
|
|
10228
10359
|
};
|
|
10229
10360
|
|
|
10230
|
-
const
|
|
10231
|
-
|
|
10232
|
-
"area-alt": AreaAlt,
|
|
10233
|
-
"aria-hidden-body": AriaHiddenBody,
|
|
10234
|
-
"aria-label-misuse": AriaLabelMisuse,
|
|
10235
|
-
"attr-case": AttrCase,
|
|
10236
|
-
"attr-delimiter": AttrDelimiter,
|
|
10237
|
-
"attr-pattern": AttrPattern,
|
|
10238
|
-
"attr-quotes": AttrQuotes,
|
|
10239
|
-
"attr-spacing": AttrSpacing,
|
|
10240
|
-
"attribute-allowed-values": AttributeAllowedValues,
|
|
10241
|
-
"attribute-boolean-style": AttributeBooleanStyle,
|
|
10242
|
-
"attribute-empty-style": AttributeEmptyStyle,
|
|
10243
|
-
"attribute-misuse": AttributeMisuse,
|
|
10244
|
-
"class-pattern": ClassPattern,
|
|
10245
|
-
"close-attr": CloseAttr,
|
|
10246
|
-
"close-order": CloseOrder,
|
|
10247
|
-
deprecated: Deprecated,
|
|
10248
|
-
"deprecated-rule": DeprecatedRule,
|
|
10249
|
-
"doctype-html": NoStyleTag$1,
|
|
10250
|
-
"doctype-style": DoctypeStyle,
|
|
10251
|
-
"element-case": ElementCase,
|
|
10252
|
-
"element-name": ElementName,
|
|
10253
|
-
"element-permitted-content": ElementPermittedContent,
|
|
10254
|
-
"element-permitted-occurrences": ElementPermittedOccurrences,
|
|
10255
|
-
"element-permitted-order": ElementPermittedOrder,
|
|
10256
|
-
"element-permitted-parent": ElementPermittedParent,
|
|
10257
|
-
"element-required-ancestor": ElementRequiredAncestor,
|
|
10258
|
-
"element-required-attributes": ElementRequiredAttributes,
|
|
10259
|
-
"element-required-content": ElementRequiredContent,
|
|
10260
|
-
"empty-heading": EmptyHeading,
|
|
10261
|
-
"empty-title": EmptyTitle,
|
|
10262
|
-
"form-dup-name": FormDupName,
|
|
10263
|
-
"heading-level": HeadingLevel,
|
|
10264
|
-
"hidden-focusable": HiddenFocusable,
|
|
10265
|
-
"id-pattern": IdPattern,
|
|
10266
|
-
"input-attributes": InputAttributes,
|
|
10267
|
-
"input-missing-label": InputMissingLabel,
|
|
10268
|
-
"long-title": LongTitle,
|
|
10269
|
-
"map-dup-name": MapDupName,
|
|
10270
|
-
"map-id-name": MapIdName,
|
|
10271
|
-
"meta-refresh": MetaRefresh,
|
|
10272
|
-
"missing-doctype": MissingDoctype,
|
|
10273
|
-
"multiple-labeled-controls": MultipleLabeledControls,
|
|
10274
|
-
"name-pattern": NamePattern,
|
|
10275
|
-
"no-abstract-role": NoAbstractRole,
|
|
10276
|
-
"no-autoplay": NoAutoplay,
|
|
10277
|
-
"no-conditional-comment": NoConditionalComment,
|
|
10278
|
-
"no-deprecated-attr": NoDeprecatedAttr,
|
|
10279
|
-
"no-dup-attr": NoDupAttr,
|
|
10280
|
-
"no-dup-class": NoDupClass,
|
|
10281
|
-
"no-dup-id": NoDupID,
|
|
10282
|
-
"no-implicit-button-type": NoImplicitButtonType,
|
|
10283
|
-
"no-implicit-input-type": NoImplicitInputType,
|
|
10284
|
-
"no-implicit-close": NoImplicitClose,
|
|
10285
|
-
"no-inline-style": NoInlineStyle,
|
|
10286
|
-
"no-missing-references": NoMissingReferences,
|
|
10287
|
-
"no-multiple-main": NoMultipleMain,
|
|
10288
|
-
"no-raw-characters": NoRawCharacters,
|
|
10289
|
-
"no-redundant-aria-label": NoRedundantAriaLabel,
|
|
10290
|
-
"no-redundant-for": NoRedundantFor,
|
|
10291
|
-
"no-redundant-role": NoRedundantRole,
|
|
10292
|
-
"no-self-closing": NoSelfClosing,
|
|
10293
|
-
"no-style-tag": NoStyleTag,
|
|
10294
|
-
"no-trailing-whitespace": NoTrailingWhitespace,
|
|
10295
|
-
"no-unknown-elements": NoUnknownElements,
|
|
10296
|
-
"no-unused-disable": NoUnusedDisable,
|
|
10297
|
-
"no-utf8-bom": NoUtf8Bom,
|
|
10298
|
-
"prefer-button": PreferButton,
|
|
10299
|
-
"prefer-native-element": PreferNativeElement,
|
|
10300
|
-
"prefer-tbody": PreferTbody,
|
|
10301
|
-
"require-csp-nonce": RequireCSPNonce,
|
|
10302
|
-
"require-sri": RequireSri,
|
|
10303
|
-
"script-element": ScriptElement,
|
|
10304
|
-
"script-type": ScriptType,
|
|
10305
|
-
"svg-focusable": SvgFocusable,
|
|
10306
|
-
"tel-non-breaking": TelNonBreaking,
|
|
10307
|
-
"text-content": TextContent,
|
|
10308
|
-
"unique-landmark": UniqueLandmark,
|
|
10309
|
-
"unrecognized-char-ref": UnknownCharReference,
|
|
10310
|
-
"valid-autocomplete": ValidAutocomplete,
|
|
10311
|
-
"valid-id": ValidID,
|
|
10312
|
-
"void-content": VoidContent,
|
|
10313
|
-
"void-style": VoidStyle,
|
|
10314
|
-
...bundledRules$1
|
|
10361
|
+
const TRANSFORMER_API = {
|
|
10362
|
+
VERSION: 1
|
|
10315
10363
|
};
|
|
10316
10364
|
|
|
10317
10365
|
var defaultConfig = {};
|
|
@@ -11439,7 +11487,7 @@ class Parser {
|
|
|
11439
11487
|
location
|
|
11440
11488
|
};
|
|
11441
11489
|
this.trigger("tag:end", event);
|
|
11442
|
-
if (
|
|
11490
|
+
if (!active.isRootElement()) {
|
|
11443
11491
|
this.trigger("element:ready", {
|
|
11444
11492
|
target: active,
|
|
11445
11493
|
location: active.location
|
|
@@ -11791,15 +11839,6 @@ class Parser {
|
|
|
11791
11839
|
}
|
|
11792
11840
|
}
|
|
11793
11841
|
|
|
11794
|
-
function isThenable(value) {
|
|
11795
|
-
return value && typeof value === "object" && "then" in value && typeof value.then === "function";
|
|
11796
|
-
}
|
|
11797
|
-
|
|
11798
|
-
const ruleIds = new Set(Object.keys(bundledRules));
|
|
11799
|
-
function ruleExists(ruleId) {
|
|
11800
|
-
return ruleIds.has(ruleId);
|
|
11801
|
-
}
|
|
11802
|
-
|
|
11803
11842
|
function freeze(src) {
|
|
11804
11843
|
return {
|
|
11805
11844
|
...src,
|
|
@@ -11970,7 +12009,7 @@ class Engine {
|
|
|
11970
12009
|
const directiveContext = {
|
|
11971
12010
|
rules,
|
|
11972
12011
|
reportUnused(rules2, unused, options, location2) {
|
|
11973
|
-
if (
|
|
12012
|
+
if (!rules2.has(noUnusedDisable.name)) {
|
|
11974
12013
|
noUnusedDisable.reportUnused(unused, options, location2);
|
|
11975
12014
|
}
|
|
11976
12015
|
}
|
|
@@ -12101,7 +12140,9 @@ class Engine {
|
|
|
12101
12140
|
return new this.ParserClass(this.config);
|
|
12102
12141
|
}
|
|
12103
12142
|
processDirective(event, parser, context) {
|
|
12104
|
-
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
|
+
});
|
|
12105
12146
|
const location = event.optionsLocation ?? event.location;
|
|
12106
12147
|
switch (event.action) {
|
|
12107
12148
|
case "enable":
|
|
@@ -12125,7 +12166,7 @@ class Engine {
|
|
|
12125
12166
|
rule.setServerity(Severity.ERROR);
|
|
12126
12167
|
}
|
|
12127
12168
|
}
|
|
12128
|
-
parser.on("tag:start", (
|
|
12169
|
+
parser.on("tag:start", (_event, data) => {
|
|
12129
12170
|
data.target.enableRules(rules.map((rule) => rule.name));
|
|
12130
12171
|
});
|
|
12131
12172
|
}
|
|
@@ -12133,7 +12174,7 @@ class Engine {
|
|
|
12133
12174
|
for (const rule of rules) {
|
|
12134
12175
|
rule.setEnabled(false);
|
|
12135
12176
|
}
|
|
12136
|
-
parser.on("tag:start", (
|
|
12177
|
+
parser.on("tag:start", (_event, data) => {
|
|
12137
12178
|
data.target.disableRules(rules.map((rule) => rule.name));
|
|
12138
12179
|
});
|
|
12139
12180
|
}
|
|
@@ -12145,14 +12186,14 @@ class Engine {
|
|
|
12145
12186
|
for (const rule of rules) {
|
|
12146
12187
|
rule.block(blocker);
|
|
12147
12188
|
}
|
|
12148
|
-
const unregisterOpen = parser.on("tag:start", (
|
|
12189
|
+
const unregisterOpen = parser.on("tag:start", (_event, data) => {
|
|
12149
12190
|
var _a;
|
|
12150
12191
|
if (directiveBlock === null) {
|
|
12151
12192
|
directiveBlock = ((_a = data.target.parent) == null ? void 0 : _a.unique) ?? null;
|
|
12152
12193
|
}
|
|
12153
12194
|
data.target.blockRules(ruleIds, blocker);
|
|
12154
12195
|
});
|
|
12155
|
-
const unregisterClose = parser.on("tag:end", (
|
|
12196
|
+
const unregisterClose = parser.on("tag:end", (_event, data) => {
|
|
12156
12197
|
const lastNode = directiveBlock === null;
|
|
12157
12198
|
const parentClosed = directiveBlock === data.previous.unique;
|
|
12158
12199
|
if (lastNode || parentClosed) {
|
|
@@ -12163,7 +12204,7 @@ class Engine {
|
|
|
12163
12204
|
}
|
|
12164
12205
|
}
|
|
12165
12206
|
});
|
|
12166
|
-
parser.on("rule:error", (
|
|
12207
|
+
parser.on("rule:error", (_event, data) => {
|
|
12167
12208
|
if (data.blockers.includes(blocker)) {
|
|
12168
12209
|
unused.delete(data.ruleId);
|
|
12169
12210
|
}
|
|
@@ -12179,10 +12220,10 @@ class Engine {
|
|
|
12179
12220
|
for (const rule of rules) {
|
|
12180
12221
|
rule.block(blocker);
|
|
12181
12222
|
}
|
|
12182
|
-
const unregister = parser.on("tag:start", (
|
|
12223
|
+
const unregister = parser.on("tag:start", (_event, data) => {
|
|
12183
12224
|
data.target.blockRules(ruleIds, blocker);
|
|
12184
12225
|
});
|
|
12185
|
-
parser.on("rule:error", (
|
|
12226
|
+
parser.on("rule:error", (_event, data) => {
|
|
12186
12227
|
if (data.blockers.includes(blocker)) {
|
|
12187
12228
|
unused.delete(data.ruleId);
|
|
12188
12229
|
}
|
|
@@ -12746,7 +12787,7 @@ class HtmlValidate {
|
|
|
12746
12787
|
}
|
|
12747
12788
|
|
|
12748
12789
|
const name = "html-validate";
|
|
12749
|
-
const version = "8.
|
|
12790
|
+
const version = "8.21.0";
|
|
12750
12791
|
const bugs = "https://gitlab.com/html-validate/html-validate/issues/new";
|
|
12751
12792
|
|
|
12752
12793
|
function definePlugin(plugin) {
|
|
@@ -13621,5 +13662,5 @@ function compatibilityCheckImpl(name, declared, options) {
|
|
|
13621
13662
|
return false;
|
|
13622
13663
|
}
|
|
13623
13664
|
|
|
13624
|
-
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 };
|
|
13625
13666
|
//# sourceMappingURL=core.js.map
|