html-validate 8.14.0 → 8.16.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/core.js +632 -34
- package/dist/cjs/core.js.map +1 -1
- package/dist/cjs/elements.js +13 -0
- package/dist/cjs/elements.js.map +1 -1
- package/dist/es/core.js +632 -34
- package/dist/es/core.js.map +1 -1
- package/dist/es/elements.js +13 -0
- package/dist/es/elements.js.map +1 -1
- package/dist/schema/elements.json +4 -0
- package/dist/types/browser.d.ts +19 -0
- package/dist/types/index.d.ts +19 -0
- package/package.json +8 -8
package/dist/cjs/core.js
CHANGED
|
@@ -811,6 +811,10 @@ const definitions = {
|
|
|
811
811
|
type: "object",
|
|
812
812
|
additionalProperties: false,
|
|
813
813
|
properties: {
|
|
814
|
+
disablable: {
|
|
815
|
+
type: "boolean",
|
|
816
|
+
title: "Disablable elements can be disabled using the disabled attribute."
|
|
817
|
+
},
|
|
814
818
|
listed: {
|
|
815
819
|
type: "boolean",
|
|
816
820
|
title: "Listed elements have a name attribute and is listed in the form and fieldset elements property."
|
|
@@ -1136,6 +1140,7 @@ function migrateElement(src) {
|
|
|
1136
1140
|
}
|
|
1137
1141
|
if (src.formAssociated) {
|
|
1138
1142
|
result.formAssociated = {
|
|
1143
|
+
disablable: Boolean(src.formAssociated.disablable),
|
|
1139
1144
|
listed: Boolean(src.formAssociated.listed)
|
|
1140
1145
|
};
|
|
1141
1146
|
} else {
|
|
@@ -2300,6 +2305,7 @@ class TextNode extends DOMNode {
|
|
|
2300
2305
|
}
|
|
2301
2306
|
|
|
2302
2307
|
const ROLE = Symbol("role");
|
|
2308
|
+
const TABINDEX = Symbol("tabindex");
|
|
2303
2309
|
var NodeClosed = /* @__PURE__ */ ((NodeClosed2) => {
|
|
2304
2310
|
NodeClosed2[NodeClosed2["Open"] = 0] = "Open";
|
|
2305
2311
|
NodeClosed2[NodeClosed2["EndTag"] = 1] = "EndTag";
|
|
@@ -2580,6 +2586,43 @@ class HtmlElement extends DOMNode {
|
|
|
2580
2586
|
}
|
|
2581
2587
|
this.attr[key].push(new Attribute(key, value, keyLocation, valueLocation, originalAttribute));
|
|
2582
2588
|
}
|
|
2589
|
+
/**
|
|
2590
|
+
* Get parsed tabindex for this element.
|
|
2591
|
+
*
|
|
2592
|
+
* - If `tabindex` attribute is not present `null` is returned.
|
|
2593
|
+
* - If attribute value is omitted or the empty string `null` is returned.
|
|
2594
|
+
* - If attribute value cannot be parsed `null` is returned.
|
|
2595
|
+
* - If attribute value is dynamic `0` is returned.
|
|
2596
|
+
* - Otherwise the parsed value is returned.
|
|
2597
|
+
*
|
|
2598
|
+
* This property does *NOT* take into account if the element have a default
|
|
2599
|
+
* `tabindex` (such as `<input>` have). Instead use the `focusable` metadata
|
|
2600
|
+
* property to determine this.
|
|
2601
|
+
*
|
|
2602
|
+
* @public
|
|
2603
|
+
* @since 8.16.0
|
|
2604
|
+
*/
|
|
2605
|
+
get tabIndex() {
|
|
2606
|
+
const cached = this.cacheGet(TABINDEX);
|
|
2607
|
+
if (cached !== void 0) {
|
|
2608
|
+
return cached;
|
|
2609
|
+
}
|
|
2610
|
+
const tabindex = this.getAttribute("tabindex");
|
|
2611
|
+
if (!tabindex) {
|
|
2612
|
+
return this.cacheSet(TABINDEX, null);
|
|
2613
|
+
}
|
|
2614
|
+
if (tabindex.value === null) {
|
|
2615
|
+
return this.cacheSet(TABINDEX, null);
|
|
2616
|
+
}
|
|
2617
|
+
if (tabindex.value instanceof DynamicValue) {
|
|
2618
|
+
return this.cacheSet(TABINDEX, 0);
|
|
2619
|
+
}
|
|
2620
|
+
const parsed = parseInt(tabindex.value, 10);
|
|
2621
|
+
if (isNaN(parsed)) {
|
|
2622
|
+
return this.cacheSet(TABINDEX, null);
|
|
2623
|
+
}
|
|
2624
|
+
return this.cacheSet(TABINDEX, parsed);
|
|
2625
|
+
}
|
|
2583
2626
|
/**
|
|
2584
2627
|
* Get a list of all attributes on this node.
|
|
2585
2628
|
*/
|
|
@@ -3376,6 +3419,7 @@ function isKeywordIgnored(options, keyword, matcher = (list, it) => list.include
|
|
|
3376
3419
|
|
|
3377
3420
|
const ARIA_HIDDEN_CACHE = Symbol(isAriaHidden.name);
|
|
3378
3421
|
const HTML_HIDDEN_CACHE = Symbol(isHTMLHidden.name);
|
|
3422
|
+
const INERT_CACHE = Symbol(isInert.name);
|
|
3379
3423
|
const ROLE_PRESENTATION_CACHE = Symbol(isPresentation.name);
|
|
3380
3424
|
const STYLE_HIDDEN_CACHE = Symbol(isStyleHidden.name);
|
|
3381
3425
|
function inAccessibilityTree(node) {
|
|
@@ -3388,6 +3432,9 @@ function inAccessibilityTree(node) {
|
|
|
3388
3432
|
if (isHTMLHidden(node)) {
|
|
3389
3433
|
return false;
|
|
3390
3434
|
}
|
|
3435
|
+
if (isInert(node)) {
|
|
3436
|
+
return false;
|
|
3437
|
+
}
|
|
3391
3438
|
if (isStyleHidden(node)) {
|
|
3392
3439
|
return false;
|
|
3393
3440
|
}
|
|
@@ -3429,6 +3476,24 @@ function isHTMLHidden(node, details) {
|
|
|
3429
3476
|
const result = node.cacheSet(HTML_HIDDEN_CACHE, isHTMLHiddenImpl(node));
|
|
3430
3477
|
return details ? result : result.byParent || result.bySelf;
|
|
3431
3478
|
}
|
|
3479
|
+
function isInertImpl(node) {
|
|
3480
|
+
const isInert2 = (node2) => {
|
|
3481
|
+
const inert = node2.getAttribute("inert");
|
|
3482
|
+
return inert !== null && inert.isStatic;
|
|
3483
|
+
};
|
|
3484
|
+
return {
|
|
3485
|
+
byParent: node.parent ? isInert2(node.parent) : false,
|
|
3486
|
+
bySelf: isInert2(node)
|
|
3487
|
+
};
|
|
3488
|
+
}
|
|
3489
|
+
function isInert(node, details) {
|
|
3490
|
+
const cached = node.cacheGet(INERT_CACHE);
|
|
3491
|
+
if (cached) {
|
|
3492
|
+
return details ? cached : cached.byParent || cached.bySelf;
|
|
3493
|
+
}
|
|
3494
|
+
const result = node.cacheSet(INERT_CACHE, isInertImpl(node));
|
|
3495
|
+
return details ? result : result.byParent || result.bySelf;
|
|
3496
|
+
}
|
|
3432
3497
|
function isStyleHiddenImpl(node) {
|
|
3433
3498
|
const isHidden = (node2) => {
|
|
3434
3499
|
const style = node2.getAttribute("style");
|
|
@@ -6460,6 +6525,46 @@ class HeadingLevel extends Rule {
|
|
|
6460
6525
|
}
|
|
6461
6526
|
}
|
|
6462
6527
|
|
|
6528
|
+
const FOCUSABLE_CACHE = Symbol(isFocusable.name);
|
|
6529
|
+
function isDisabled(element, meta) {
|
|
6530
|
+
var _a;
|
|
6531
|
+
if (!((_a = meta.formAssociated) == null ? void 0 : _a.disablable)) {
|
|
6532
|
+
return false;
|
|
6533
|
+
}
|
|
6534
|
+
const disabled = element.matches("[disabled]");
|
|
6535
|
+
if (disabled) {
|
|
6536
|
+
return true;
|
|
6537
|
+
}
|
|
6538
|
+
const fieldset = element.closest("fieldset[disabled]");
|
|
6539
|
+
if (fieldset) {
|
|
6540
|
+
return true;
|
|
6541
|
+
}
|
|
6542
|
+
return false;
|
|
6543
|
+
}
|
|
6544
|
+
function isFocusableImpl(element) {
|
|
6545
|
+
if (isHTMLHidden(element) || isInert(element) || isStyleHidden(element)) {
|
|
6546
|
+
return false;
|
|
6547
|
+
}
|
|
6548
|
+
const { tabIndex, meta } = element;
|
|
6549
|
+
if (tabIndex !== null) {
|
|
6550
|
+
return tabIndex >= 0;
|
|
6551
|
+
}
|
|
6552
|
+
if (!meta) {
|
|
6553
|
+
return false;
|
|
6554
|
+
}
|
|
6555
|
+
if (isDisabled(element, meta)) {
|
|
6556
|
+
return false;
|
|
6557
|
+
}
|
|
6558
|
+
return Boolean(meta == null ? void 0 : meta.focusable);
|
|
6559
|
+
}
|
|
6560
|
+
function isFocusable(element) {
|
|
6561
|
+
const cached = element.cacheGet(FOCUSABLE_CACHE);
|
|
6562
|
+
if (cached) {
|
|
6563
|
+
return cached;
|
|
6564
|
+
}
|
|
6565
|
+
return element.cacheSet(FOCUSABLE_CACHE, isFocusableImpl(element));
|
|
6566
|
+
}
|
|
6567
|
+
|
|
6463
6568
|
class HiddenFocusable extends Rule {
|
|
6464
6569
|
documentation(context) {
|
|
6465
6570
|
const byParent = context === "parent" ? " In this case it is being hidden by an ancestor with `aria-hidden.`" : "";
|
|
@@ -6473,7 +6578,8 @@ class HiddenFocusable extends Rule {
|
|
|
6473
6578
|
"To fix this either:",
|
|
6474
6579
|
" - Remove `aria-hidden`.",
|
|
6475
6580
|
" - Remove the element from the DOM instead.",
|
|
6476
|
-
|
|
6581
|
+
' - Use `tabindex="-1"` to remove the element from tab order.',
|
|
6582
|
+
" - Use `hidden`, `inert` or similar means to hide or disable the element."
|
|
6477
6583
|
].join("\n"),
|
|
6478
6584
|
url: "https://html-validate.org/rules/hidden-focusable.html"
|
|
6479
6585
|
};
|
|
@@ -6484,21 +6590,13 @@ class HiddenFocusable extends Rule {
|
|
|
6484
6590
|
this.on("dom:ready", (event) => {
|
|
6485
6591
|
const { document } = event;
|
|
6486
6592
|
for (const element of document.querySelectorAll(selector)) {
|
|
6487
|
-
if (
|
|
6488
|
-
|
|
6489
|
-
}
|
|
6490
|
-
if (isAriaHidden(element)) {
|
|
6491
|
-
this.validateElement(element);
|
|
6593
|
+
if (isFocusable(element) && isAriaHidden(element)) {
|
|
6594
|
+
this.reportElement(element);
|
|
6492
6595
|
}
|
|
6493
6596
|
}
|
|
6494
6597
|
});
|
|
6495
6598
|
}
|
|
6496
|
-
|
|
6497
|
-
const { meta } = element;
|
|
6498
|
-
const tabindex = element.getAttribute("tabindex");
|
|
6499
|
-
if (meta && !meta.focusable && !tabindex) {
|
|
6500
|
-
return;
|
|
6501
|
-
}
|
|
6599
|
+
reportElement(element) {
|
|
6502
6600
|
const attribute = element.getAttribute("aria-hidden");
|
|
6503
6601
|
const message = attribute ? `aria-hidden cannot be used on focusable elements` : `aria-hidden cannot be used on focusable elements (hidden by ancestor element)`;
|
|
6504
6602
|
const location = attribute ? attribute.keyLocation : element.location;
|
|
@@ -6556,26 +6654,6 @@ class IdPattern extends Rule {
|
|
|
6556
6654
|
const restricted = /* @__PURE__ */ new Map([
|
|
6557
6655
|
["accept", ["file"]],
|
|
6558
6656
|
["alt", ["image"]],
|
|
6559
|
-
[
|
|
6560
|
-
"autocomplete",
|
|
6561
|
-
[
|
|
6562
|
-
"hidden",
|
|
6563
|
-
"text",
|
|
6564
|
-
"search",
|
|
6565
|
-
"url",
|
|
6566
|
-
"tel",
|
|
6567
|
-
"email",
|
|
6568
|
-
"password",
|
|
6569
|
-
"date",
|
|
6570
|
-
"month",
|
|
6571
|
-
"week",
|
|
6572
|
-
"time",
|
|
6573
|
-
"datetime-local",
|
|
6574
|
-
"number",
|
|
6575
|
-
"range",
|
|
6576
|
-
"color"
|
|
6577
|
-
]
|
|
6578
|
-
],
|
|
6579
6657
|
["capture", ["file"]],
|
|
6580
6658
|
["checked", ["checkbox", "radio"]],
|
|
6581
6659
|
["dirname", ["text", "search"]],
|
|
@@ -6956,7 +7034,7 @@ class MetaRefresh extends Rule {
|
|
|
6956
7034
|
}
|
|
6957
7035
|
}
|
|
6958
7036
|
function parseContent(text) {
|
|
6959
|
-
const match = text.match(/^(\d+)(?:\s*;\s*url=(.*))?/);
|
|
7037
|
+
const match = text.match(/^(\d+)(?:\s*;\s*url=(.*))?/i);
|
|
6960
7038
|
if (match) {
|
|
6961
7039
|
return {
|
|
6962
7040
|
delay: parseInt(match[1], 10),
|
|
@@ -9043,6 +9121,522 @@ class UnknownCharReference extends Rule {
|
|
|
9043
9121
|
}
|
|
9044
9122
|
}
|
|
9045
9123
|
|
|
9124
|
+
const expectedOrder = ["section", "hint", "contact", "field1", "field2", "webauthn"];
|
|
9125
|
+
const fieldNames1 = [
|
|
9126
|
+
"name",
|
|
9127
|
+
"honorific-prefix",
|
|
9128
|
+
"given-name",
|
|
9129
|
+
"additional-name",
|
|
9130
|
+
"family-name",
|
|
9131
|
+
"honorific-suffix",
|
|
9132
|
+
"nickname",
|
|
9133
|
+
"username",
|
|
9134
|
+
"new-password",
|
|
9135
|
+
"current-password",
|
|
9136
|
+
"one-time-code",
|
|
9137
|
+
"organization-title",
|
|
9138
|
+
"organization",
|
|
9139
|
+
"street-address",
|
|
9140
|
+
"address-line1",
|
|
9141
|
+
"address-line2",
|
|
9142
|
+
"address-line3",
|
|
9143
|
+
"address-level4",
|
|
9144
|
+
"address-level3",
|
|
9145
|
+
"address-level2",
|
|
9146
|
+
"address-level1",
|
|
9147
|
+
"country",
|
|
9148
|
+
"country-name",
|
|
9149
|
+
"postal-code",
|
|
9150
|
+
"cc-name",
|
|
9151
|
+
"cc-given-name",
|
|
9152
|
+
"cc-additional-name",
|
|
9153
|
+
"cc-family-name",
|
|
9154
|
+
"cc-number",
|
|
9155
|
+
"cc-exp",
|
|
9156
|
+
"cc-exp-month",
|
|
9157
|
+
"cc-exp-year",
|
|
9158
|
+
"cc-csc",
|
|
9159
|
+
"cc-type",
|
|
9160
|
+
"transaction-currency",
|
|
9161
|
+
"transaction-amount",
|
|
9162
|
+
"language",
|
|
9163
|
+
"bday",
|
|
9164
|
+
"bday-day",
|
|
9165
|
+
"bday-month",
|
|
9166
|
+
"bday-year",
|
|
9167
|
+
"sex",
|
|
9168
|
+
"url",
|
|
9169
|
+
"photo"
|
|
9170
|
+
];
|
|
9171
|
+
const fieldNames2 = [
|
|
9172
|
+
"tel",
|
|
9173
|
+
"tel-country-code",
|
|
9174
|
+
"tel-national",
|
|
9175
|
+
"tel-area-code",
|
|
9176
|
+
"tel-local",
|
|
9177
|
+
"tel-local-prefix",
|
|
9178
|
+
"tel-local-suffix",
|
|
9179
|
+
"tel-extension",
|
|
9180
|
+
"email",
|
|
9181
|
+
"impp"
|
|
9182
|
+
];
|
|
9183
|
+
const fieldNameGroup = {
|
|
9184
|
+
name: "text",
|
|
9185
|
+
"honorific-prefix": "text",
|
|
9186
|
+
"given-name": "text",
|
|
9187
|
+
"additional-name": "text",
|
|
9188
|
+
"family-name": "text",
|
|
9189
|
+
"honorific-suffix": "text",
|
|
9190
|
+
nickname: "text",
|
|
9191
|
+
username: "username",
|
|
9192
|
+
"new-password": "password",
|
|
9193
|
+
"current-password": "password",
|
|
9194
|
+
"one-time-code": "password",
|
|
9195
|
+
"organization-title": "text",
|
|
9196
|
+
organization: "text",
|
|
9197
|
+
"street-address": "multiline",
|
|
9198
|
+
"address-line1": "text",
|
|
9199
|
+
"address-line2": "text",
|
|
9200
|
+
"address-line3": "text",
|
|
9201
|
+
"address-level4": "text",
|
|
9202
|
+
"address-level3": "text",
|
|
9203
|
+
"address-level2": "text",
|
|
9204
|
+
"address-level1": "text",
|
|
9205
|
+
country: "text",
|
|
9206
|
+
"country-name": "text",
|
|
9207
|
+
"postal-code": "text",
|
|
9208
|
+
"cc-name": "text",
|
|
9209
|
+
"cc-given-name": "text",
|
|
9210
|
+
"cc-additional-name": "text",
|
|
9211
|
+
"cc-family-name": "text",
|
|
9212
|
+
"cc-number": "text",
|
|
9213
|
+
"cc-exp": "month",
|
|
9214
|
+
"cc-exp-month": "numeric",
|
|
9215
|
+
"cc-exp-year": "numeric",
|
|
9216
|
+
"cc-csc": "text",
|
|
9217
|
+
"cc-type": "text",
|
|
9218
|
+
"transaction-currency": "text",
|
|
9219
|
+
"transaction-amount": "numeric",
|
|
9220
|
+
language: "text",
|
|
9221
|
+
bday: "date",
|
|
9222
|
+
"bday-day": "numeric",
|
|
9223
|
+
"bday-month": "numeric",
|
|
9224
|
+
"bday-year": "numeric",
|
|
9225
|
+
sex: "text",
|
|
9226
|
+
url: "url",
|
|
9227
|
+
photo: "url",
|
|
9228
|
+
tel: "tel",
|
|
9229
|
+
"tel-country-code": "text",
|
|
9230
|
+
"tel-national": "text",
|
|
9231
|
+
"tel-area-code": "text",
|
|
9232
|
+
"tel-local": "text",
|
|
9233
|
+
"tel-local-prefix": "text",
|
|
9234
|
+
"tel-local-suffix": "text",
|
|
9235
|
+
"tel-extension": "text",
|
|
9236
|
+
email: "username",
|
|
9237
|
+
impp: "url"
|
|
9238
|
+
};
|
|
9239
|
+
const disallowedInputTypes = ["checkbox", "radio", "file", "submit", "image", "reset", "button"];
|
|
9240
|
+
function matchSection(token) {
|
|
9241
|
+
return token.startsWith("section-");
|
|
9242
|
+
}
|
|
9243
|
+
function matchHint(token) {
|
|
9244
|
+
return token === "shipping" || token === "billing";
|
|
9245
|
+
}
|
|
9246
|
+
function matchFieldNames1(token) {
|
|
9247
|
+
return fieldNames1.includes(token);
|
|
9248
|
+
}
|
|
9249
|
+
function matchContact(token) {
|
|
9250
|
+
const haystack = ["home", "work", "mobile", "fax", "pager"];
|
|
9251
|
+
return haystack.includes(token);
|
|
9252
|
+
}
|
|
9253
|
+
function matchFieldNames2(token) {
|
|
9254
|
+
return fieldNames2.includes(token);
|
|
9255
|
+
}
|
|
9256
|
+
function matchWebauthn(token) {
|
|
9257
|
+
return token === "webauthn";
|
|
9258
|
+
}
|
|
9259
|
+
function matchToken(token) {
|
|
9260
|
+
if (matchSection(token)) {
|
|
9261
|
+
return "section";
|
|
9262
|
+
}
|
|
9263
|
+
if (matchHint(token)) {
|
|
9264
|
+
return "hint";
|
|
9265
|
+
}
|
|
9266
|
+
if (matchFieldNames1(token)) {
|
|
9267
|
+
return "field1";
|
|
9268
|
+
}
|
|
9269
|
+
if (matchFieldNames2(token)) {
|
|
9270
|
+
return "field2";
|
|
9271
|
+
}
|
|
9272
|
+
if (matchContact(token)) {
|
|
9273
|
+
return "contact";
|
|
9274
|
+
}
|
|
9275
|
+
if (matchWebauthn(token)) {
|
|
9276
|
+
return "webauthn";
|
|
9277
|
+
}
|
|
9278
|
+
return null;
|
|
9279
|
+
}
|
|
9280
|
+
function getControlGroups(type) {
|
|
9281
|
+
const allGroups = [
|
|
9282
|
+
"text",
|
|
9283
|
+
"multiline",
|
|
9284
|
+
"password",
|
|
9285
|
+
"url",
|
|
9286
|
+
"username",
|
|
9287
|
+
"tel",
|
|
9288
|
+
"numeric",
|
|
9289
|
+
"month",
|
|
9290
|
+
"date"
|
|
9291
|
+
];
|
|
9292
|
+
const mapping = {
|
|
9293
|
+
hidden: allGroups,
|
|
9294
|
+
text: allGroups.filter((it) => it !== "multiline"),
|
|
9295
|
+
search: allGroups.filter((it) => it !== "multiline"),
|
|
9296
|
+
password: ["password"],
|
|
9297
|
+
url: ["url"],
|
|
9298
|
+
email: ["username"],
|
|
9299
|
+
tel: ["tel"],
|
|
9300
|
+
number: ["numeric"],
|
|
9301
|
+
month: ["month"],
|
|
9302
|
+
date: ["date"]
|
|
9303
|
+
};
|
|
9304
|
+
const groups = mapping[type];
|
|
9305
|
+
if (groups) {
|
|
9306
|
+
return groups;
|
|
9307
|
+
}
|
|
9308
|
+
return [];
|
|
9309
|
+
}
|
|
9310
|
+
function isDisallowedType(node, type) {
|
|
9311
|
+
if (!node.is("input")) {
|
|
9312
|
+
return false;
|
|
9313
|
+
}
|
|
9314
|
+
return disallowedInputTypes.includes(type);
|
|
9315
|
+
}
|
|
9316
|
+
function getTerminalMessage(context) {
|
|
9317
|
+
switch (context.msg) {
|
|
9318
|
+
case 0 /* InvalidAttribute */:
|
|
9319
|
+
return "autocomplete attribute cannot be used on {{ what }}";
|
|
9320
|
+
case 1 /* InvalidValue */:
|
|
9321
|
+
return '"{{ value }}" cannot be used on {{ what }}';
|
|
9322
|
+
case 2 /* InvalidOrder */:
|
|
9323
|
+
return '"{{ second }}" must appear before "{{ first }}"';
|
|
9324
|
+
case 3 /* InvalidToken */:
|
|
9325
|
+
return '"{{ token }}" is not a valid autocomplete token or field name';
|
|
9326
|
+
case 4 /* InvalidCombination */:
|
|
9327
|
+
return '"{{ second }}" cannot be combined with "{{ first }}"';
|
|
9328
|
+
case 5 /* MissingField */:
|
|
9329
|
+
return "autocomplete attribute is missing field name";
|
|
9330
|
+
}
|
|
9331
|
+
}
|
|
9332
|
+
function getMarkdownMessage(context) {
|
|
9333
|
+
switch (context.msg) {
|
|
9334
|
+
case 0 /* InvalidAttribute */:
|
|
9335
|
+
return [
|
|
9336
|
+
`\`autocomplete\` attribute cannot be used on \`${context.what}\``,
|
|
9337
|
+
"",
|
|
9338
|
+
"The following input types cannot use the `autocomplete` attribute:",
|
|
9339
|
+
"",
|
|
9340
|
+
...disallowedInputTypes.map((it) => `- \`${it}\``)
|
|
9341
|
+
].join("\n");
|
|
9342
|
+
case 1 /* InvalidValue */: {
|
|
9343
|
+
const message = `\`"${context.value}"\` cannot be used on \`${context.what}\``;
|
|
9344
|
+
if (context.type === "form") {
|
|
9345
|
+
return [
|
|
9346
|
+
message,
|
|
9347
|
+
"",
|
|
9348
|
+
'The `<form>` element can only use the values `"on"` and `"off"`.'
|
|
9349
|
+
].join("\n");
|
|
9350
|
+
}
|
|
9351
|
+
if (context.type === "hidden") {
|
|
9352
|
+
return [
|
|
9353
|
+
message,
|
|
9354
|
+
"",
|
|
9355
|
+
'`<input type="hidden">` cannot use the values `"on"` and `"off"`.'
|
|
9356
|
+
].join("\n");
|
|
9357
|
+
}
|
|
9358
|
+
const controlGroups = getControlGroups(context.type);
|
|
9359
|
+
const currentGroup = fieldNameGroup[context.value];
|
|
9360
|
+
return [
|
|
9361
|
+
message,
|
|
9362
|
+
"",
|
|
9363
|
+
`\`${context.what}\` allows autocomplete fields from the following group${controlGroups.length > 1 ? "s" : ""}:`,
|
|
9364
|
+
"",
|
|
9365
|
+
...controlGroups.map((it) => `- ${it}`),
|
|
9366
|
+
"",
|
|
9367
|
+
`The field \`"${context.value}"\` belongs to the group /${currentGroup}/ which cannot be used with this input type.`
|
|
9368
|
+
].join("\n");
|
|
9369
|
+
}
|
|
9370
|
+
case 2 /* InvalidOrder */:
|
|
9371
|
+
return [
|
|
9372
|
+
`\`"${context.second}"\` must appear before \`"${context.first}"\``,
|
|
9373
|
+
"",
|
|
9374
|
+
"The autocomplete tokens must appear in the following order:",
|
|
9375
|
+
"",
|
|
9376
|
+
"- Optional section name (`section-` prefix).",
|
|
9377
|
+
"- Optional `shipping` or `billing` token.",
|
|
9378
|
+
"- Optional `home`, `work`, `mobile`, `fax` or `pager` token (for fields supporting it).",
|
|
9379
|
+
"- Field name",
|
|
9380
|
+
"- Optional `webauthn` token."
|
|
9381
|
+
].join("\n");
|
|
9382
|
+
case 3 /* InvalidToken */:
|
|
9383
|
+
return `\`"${context.token}"\` is not a valid autocomplete token or field name`;
|
|
9384
|
+
case 4 /* InvalidCombination */:
|
|
9385
|
+
return `\`"${context.second}"\` cannot be combined with \`"${context.first}"\``;
|
|
9386
|
+
case 5 /* MissingField */:
|
|
9387
|
+
return "Autocomplete attribute is missing field name";
|
|
9388
|
+
}
|
|
9389
|
+
}
|
|
9390
|
+
class ValidAutocomplete extends Rule {
|
|
9391
|
+
documentation(context) {
|
|
9392
|
+
return {
|
|
9393
|
+
description: getMarkdownMessage(context),
|
|
9394
|
+
url: "https://html-validate.org/rules/valid-autocomplete.html"
|
|
9395
|
+
};
|
|
9396
|
+
}
|
|
9397
|
+
setup() {
|
|
9398
|
+
this.on("dom:ready", (event) => {
|
|
9399
|
+
const { document } = event;
|
|
9400
|
+
const elements = document.querySelectorAll("[autocomplete]");
|
|
9401
|
+
for (const element of elements) {
|
|
9402
|
+
const autocomplete = element.getAttribute("autocomplete");
|
|
9403
|
+
if (autocomplete.value === null || autocomplete.value instanceof DynamicValue) {
|
|
9404
|
+
continue;
|
|
9405
|
+
}
|
|
9406
|
+
const location = autocomplete.valueLocation;
|
|
9407
|
+
const value = autocomplete.value.toLowerCase();
|
|
9408
|
+
const tokens = new DOMTokenList(value, location);
|
|
9409
|
+
if (tokens.length === 0) {
|
|
9410
|
+
continue;
|
|
9411
|
+
}
|
|
9412
|
+
this.validate(element, value, tokens, autocomplete.keyLocation, location);
|
|
9413
|
+
}
|
|
9414
|
+
});
|
|
9415
|
+
}
|
|
9416
|
+
validate(node, value, tokens, keyLocation, valueLocation) {
|
|
9417
|
+
switch (node.tagName) {
|
|
9418
|
+
case "form":
|
|
9419
|
+
this.validateFormAutocomplete(node, value, valueLocation);
|
|
9420
|
+
break;
|
|
9421
|
+
case "input":
|
|
9422
|
+
case "textarea":
|
|
9423
|
+
case "select":
|
|
9424
|
+
this.validateControlAutocomplete(node, tokens, keyLocation);
|
|
9425
|
+
break;
|
|
9426
|
+
}
|
|
9427
|
+
}
|
|
9428
|
+
validateControlAutocomplete(node, tokens, keyLocation) {
|
|
9429
|
+
const type = node.getAttributeValue("type") ?? "text";
|
|
9430
|
+
const mantle = type !== "hidden" ? "expectation" : "anchor";
|
|
9431
|
+
if (isDisallowedType(node, type)) {
|
|
9432
|
+
const context = {
|
|
9433
|
+
msg: 0 /* InvalidAttribute */,
|
|
9434
|
+
what: `<input type="${type}">`
|
|
9435
|
+
};
|
|
9436
|
+
this.report({
|
|
9437
|
+
node,
|
|
9438
|
+
message: getTerminalMessage(context),
|
|
9439
|
+
location: keyLocation,
|
|
9440
|
+
context
|
|
9441
|
+
});
|
|
9442
|
+
return;
|
|
9443
|
+
}
|
|
9444
|
+
if (tokens.includes("on") || tokens.includes("off")) {
|
|
9445
|
+
this.validateOnOff(node, mantle, tokens);
|
|
9446
|
+
return;
|
|
9447
|
+
}
|
|
9448
|
+
this.validateTokens(node, tokens, keyLocation);
|
|
9449
|
+
}
|
|
9450
|
+
validateFormAutocomplete(node, value, location) {
|
|
9451
|
+
const trimmed = value.trim();
|
|
9452
|
+
if (["on", "off"].includes(trimmed)) {
|
|
9453
|
+
return;
|
|
9454
|
+
}
|
|
9455
|
+
const context = {
|
|
9456
|
+
msg: 1 /* InvalidValue */,
|
|
9457
|
+
type: "form",
|
|
9458
|
+
value: trimmed,
|
|
9459
|
+
what: "<form>"
|
|
9460
|
+
};
|
|
9461
|
+
this.report({
|
|
9462
|
+
node,
|
|
9463
|
+
message: getTerminalMessage(context),
|
|
9464
|
+
location,
|
|
9465
|
+
context
|
|
9466
|
+
});
|
|
9467
|
+
}
|
|
9468
|
+
validateOnOff(node, mantle, tokens) {
|
|
9469
|
+
const index = tokens.findIndex((it) => it === "on" || it === "off");
|
|
9470
|
+
const value = tokens.item(index);
|
|
9471
|
+
const location = tokens.location(index);
|
|
9472
|
+
if (tokens.length > 1) {
|
|
9473
|
+
const context = {
|
|
9474
|
+
msg: 4 /* InvalidCombination */,
|
|
9475
|
+
/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
|
|
9476
|
+
first: tokens.item(index > 0 ? 0 : 1),
|
|
9477
|
+
second: value
|
|
9478
|
+
};
|
|
9479
|
+
this.report({
|
|
9480
|
+
node,
|
|
9481
|
+
message: getTerminalMessage(context),
|
|
9482
|
+
location,
|
|
9483
|
+
context
|
|
9484
|
+
});
|
|
9485
|
+
}
|
|
9486
|
+
switch (mantle) {
|
|
9487
|
+
case "expectation":
|
|
9488
|
+
return;
|
|
9489
|
+
case "anchor": {
|
|
9490
|
+
const context = {
|
|
9491
|
+
msg: 1 /* InvalidValue */,
|
|
9492
|
+
type: "hidden",
|
|
9493
|
+
value,
|
|
9494
|
+
what: `<input type="hidden">`
|
|
9495
|
+
};
|
|
9496
|
+
this.report({
|
|
9497
|
+
node,
|
|
9498
|
+
message: getTerminalMessage(context),
|
|
9499
|
+
location: tokens.location(0),
|
|
9500
|
+
context
|
|
9501
|
+
});
|
|
9502
|
+
}
|
|
9503
|
+
}
|
|
9504
|
+
}
|
|
9505
|
+
validateTokens(node, tokens, keyLocation) {
|
|
9506
|
+
const order = [];
|
|
9507
|
+
for (const { item, location } of tokens.iterator()) {
|
|
9508
|
+
const tokenType = matchToken(item);
|
|
9509
|
+
if (tokenType) {
|
|
9510
|
+
order.push(tokenType);
|
|
9511
|
+
} else {
|
|
9512
|
+
const context = {
|
|
9513
|
+
msg: 3 /* InvalidToken */,
|
|
9514
|
+
token: item
|
|
9515
|
+
};
|
|
9516
|
+
this.report({
|
|
9517
|
+
node,
|
|
9518
|
+
message: getTerminalMessage(context),
|
|
9519
|
+
location,
|
|
9520
|
+
context
|
|
9521
|
+
});
|
|
9522
|
+
return;
|
|
9523
|
+
}
|
|
9524
|
+
}
|
|
9525
|
+
const fieldTokens = order.map((it) => it === "field1" || it === "field2");
|
|
9526
|
+
this.validateFieldPresence(node, tokens, fieldTokens, keyLocation);
|
|
9527
|
+
this.validateContact(node, tokens, order);
|
|
9528
|
+
this.validateOrder(node, tokens, order);
|
|
9529
|
+
this.validateControlGroup(node, tokens, fieldTokens);
|
|
9530
|
+
}
|
|
9531
|
+
/**
|
|
9532
|
+
* Ensure that exactly one field name is present from the two field lists.
|
|
9533
|
+
*/
|
|
9534
|
+
validateFieldPresence(node, tokens, fieldTokens, keyLocation) {
|
|
9535
|
+
const numFields = fieldTokens.filter(Boolean).length;
|
|
9536
|
+
if (numFields === 0) {
|
|
9537
|
+
const context = {
|
|
9538
|
+
msg: 5 /* MissingField */
|
|
9539
|
+
};
|
|
9540
|
+
this.report({
|
|
9541
|
+
node,
|
|
9542
|
+
message: getTerminalMessage(context),
|
|
9543
|
+
location: keyLocation,
|
|
9544
|
+
context
|
|
9545
|
+
});
|
|
9546
|
+
} else if (numFields > 1) {
|
|
9547
|
+
const a = fieldTokens.indexOf(true);
|
|
9548
|
+
const b = fieldTokens.lastIndexOf(true);
|
|
9549
|
+
const context = {
|
|
9550
|
+
msg: 4 /* InvalidCombination */,
|
|
9551
|
+
/* eslint-disable @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
|
|
9552
|
+
first: tokens.item(a),
|
|
9553
|
+
second: tokens.item(b)
|
|
9554
|
+
/* eslint-enable @typescript-eslint/no-non-null-assertion */
|
|
9555
|
+
};
|
|
9556
|
+
this.report({
|
|
9557
|
+
node,
|
|
9558
|
+
message: getTerminalMessage(context),
|
|
9559
|
+
location: tokens.location(b),
|
|
9560
|
+
context
|
|
9561
|
+
});
|
|
9562
|
+
}
|
|
9563
|
+
}
|
|
9564
|
+
/**
|
|
9565
|
+
* Ensure contact token is only used with field names from the second list.
|
|
9566
|
+
*/
|
|
9567
|
+
validateContact(node, tokens, order) {
|
|
9568
|
+
if (order.includes("contact") && order.includes("field1")) {
|
|
9569
|
+
const a = order.indexOf("field1");
|
|
9570
|
+
const b = order.indexOf("contact");
|
|
9571
|
+
const context = {
|
|
9572
|
+
msg: 4 /* InvalidCombination */,
|
|
9573
|
+
/* eslint-disable @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
|
|
9574
|
+
first: tokens.item(a),
|
|
9575
|
+
second: tokens.item(b)
|
|
9576
|
+
/* eslint-enable @typescript-eslint/no-non-null-assertion */
|
|
9577
|
+
};
|
|
9578
|
+
this.report({
|
|
9579
|
+
node,
|
|
9580
|
+
message: getTerminalMessage(context),
|
|
9581
|
+
location: tokens.location(b),
|
|
9582
|
+
context
|
|
9583
|
+
});
|
|
9584
|
+
}
|
|
9585
|
+
}
|
|
9586
|
+
validateOrder(node, tokens, order) {
|
|
9587
|
+
const indicies = order.map((it) => expectedOrder.indexOf(it));
|
|
9588
|
+
for (let i = 0; i < indicies.length - 1; i++) {
|
|
9589
|
+
if (indicies[0] > indicies[i + 1]) {
|
|
9590
|
+
const context = {
|
|
9591
|
+
msg: 2 /* InvalidOrder */,
|
|
9592
|
+
/* eslint-disable @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
|
|
9593
|
+
first: tokens.item(i),
|
|
9594
|
+
second: tokens.item(i + 1)
|
|
9595
|
+
/* eslint-enable @typescript-eslint/no-non-null-assertion */
|
|
9596
|
+
};
|
|
9597
|
+
this.report({
|
|
9598
|
+
node,
|
|
9599
|
+
message: getTerminalMessage(context),
|
|
9600
|
+
location: tokens.location(i + 1),
|
|
9601
|
+
context
|
|
9602
|
+
});
|
|
9603
|
+
}
|
|
9604
|
+
}
|
|
9605
|
+
}
|
|
9606
|
+
validateControlGroup(node, tokens, fieldTokens) {
|
|
9607
|
+
const numFields = fieldTokens.filter(Boolean).length;
|
|
9608
|
+
if (numFields === 0) {
|
|
9609
|
+
return;
|
|
9610
|
+
}
|
|
9611
|
+
if (!node.is("input")) {
|
|
9612
|
+
return;
|
|
9613
|
+
}
|
|
9614
|
+
const attr = node.getAttribute("type");
|
|
9615
|
+
const type = (attr == null ? void 0 : attr.value) ?? "text";
|
|
9616
|
+
if (type instanceof DynamicValue) {
|
|
9617
|
+
return;
|
|
9618
|
+
}
|
|
9619
|
+
const controlGroups = getControlGroups(type);
|
|
9620
|
+
const fieldIndex = fieldTokens.indexOf(true);
|
|
9621
|
+
const fieldToken = tokens.item(fieldIndex);
|
|
9622
|
+
const fieldGroup = fieldNameGroup[fieldToken];
|
|
9623
|
+
if (!controlGroups.includes(fieldGroup)) {
|
|
9624
|
+
const context = {
|
|
9625
|
+
msg: 1 /* InvalidValue */,
|
|
9626
|
+
type,
|
|
9627
|
+
value: fieldToken,
|
|
9628
|
+
what: `<input type="${type}">`
|
|
9629
|
+
};
|
|
9630
|
+
this.report({
|
|
9631
|
+
node,
|
|
9632
|
+
message: getTerminalMessage(context),
|
|
9633
|
+
location: tokens.location(fieldIndex),
|
|
9634
|
+
context
|
|
9635
|
+
});
|
|
9636
|
+
}
|
|
9637
|
+
}
|
|
9638
|
+
}
|
|
9639
|
+
|
|
9046
9640
|
const defaults$3 = {
|
|
9047
9641
|
relaxed: false
|
|
9048
9642
|
};
|
|
@@ -9597,6 +10191,7 @@ const bundledRules = {
|
|
|
9597
10191
|
"text-content": TextContent,
|
|
9598
10192
|
"unique-landmark": UniqueLandmark,
|
|
9599
10193
|
"unrecognized-char-ref": UnknownCharReference,
|
|
10194
|
+
"valid-autocomplete": ValidAutocomplete,
|
|
9600
10195
|
"valid-id": ValidID,
|
|
9601
10196
|
"void-content": VoidContent,
|
|
9602
10197
|
"void-style": VoidStyle,
|
|
@@ -9627,6 +10222,7 @@ const config$4 = {
|
|
|
9627
10222
|
"svg-focusable": "off",
|
|
9628
10223
|
"text-content": "error",
|
|
9629
10224
|
"unique-landmark": "error",
|
|
10225
|
+
"valid-autocomplete": "error",
|
|
9630
10226
|
"wcag/h30": "error",
|
|
9631
10227
|
"wcag/h32": "error",
|
|
9632
10228
|
"wcag/h36": "error",
|
|
@@ -9726,6 +10322,7 @@ const config$1 = {
|
|
|
9726
10322
|
"text-content": "error",
|
|
9727
10323
|
"unique-landmark": "error",
|
|
9728
10324
|
"unrecognized-char-ref": "error",
|
|
10325
|
+
"valid-autocomplete": "error",
|
|
9729
10326
|
"valid-id": ["error", { relaxed: false }],
|
|
9730
10327
|
void: "off",
|
|
9731
10328
|
"void-content": "error",
|
|
@@ -9773,6 +10370,7 @@ const config = {
|
|
|
9773
10370
|
"no-unused-disable": "error",
|
|
9774
10371
|
"script-element": "error",
|
|
9775
10372
|
"unrecognized-char-ref": "error",
|
|
10373
|
+
"valid-autocomplete": "error",
|
|
9776
10374
|
"valid-id": ["error", { relaxed: true }],
|
|
9777
10375
|
"void-content": "error"
|
|
9778
10376
|
}
|
|
@@ -11998,7 +12596,7 @@ class HtmlValidate {
|
|
|
11998
12596
|
}
|
|
11999
12597
|
|
|
12000
12598
|
const name = "html-validate";
|
|
12001
|
-
const version = "8.
|
|
12599
|
+
const version = "8.16.0";
|
|
12002
12600
|
const bugs = "https://gitlab.com/html-validate/html-validate/issues/new";
|
|
12003
12601
|
|
|
12004
12602
|
function definePlugin(plugin) {
|