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/es/core.js
CHANGED
|
@@ -801,6 +801,10 @@ const definitions = {
|
|
|
801
801
|
type: "object",
|
|
802
802
|
additionalProperties: false,
|
|
803
803
|
properties: {
|
|
804
|
+
disablable: {
|
|
805
|
+
type: "boolean",
|
|
806
|
+
title: "Disablable elements can be disabled using the disabled attribute."
|
|
807
|
+
},
|
|
804
808
|
listed: {
|
|
805
809
|
type: "boolean",
|
|
806
810
|
title: "Listed elements have a name attribute and is listed in the form and fieldset elements property."
|
|
@@ -1126,6 +1130,7 @@ function migrateElement(src) {
|
|
|
1126
1130
|
}
|
|
1127
1131
|
if (src.formAssociated) {
|
|
1128
1132
|
result.formAssociated = {
|
|
1133
|
+
disablable: Boolean(src.formAssociated.disablable),
|
|
1129
1134
|
listed: Boolean(src.formAssociated.listed)
|
|
1130
1135
|
};
|
|
1131
1136
|
} else {
|
|
@@ -2290,6 +2295,7 @@ class TextNode extends DOMNode {
|
|
|
2290
2295
|
}
|
|
2291
2296
|
|
|
2292
2297
|
const ROLE = Symbol("role");
|
|
2298
|
+
const TABINDEX = Symbol("tabindex");
|
|
2293
2299
|
var NodeClosed = /* @__PURE__ */ ((NodeClosed2) => {
|
|
2294
2300
|
NodeClosed2[NodeClosed2["Open"] = 0] = "Open";
|
|
2295
2301
|
NodeClosed2[NodeClosed2["EndTag"] = 1] = "EndTag";
|
|
@@ -2570,6 +2576,43 @@ class HtmlElement extends DOMNode {
|
|
|
2570
2576
|
}
|
|
2571
2577
|
this.attr[key].push(new Attribute(key, value, keyLocation, valueLocation, originalAttribute));
|
|
2572
2578
|
}
|
|
2579
|
+
/**
|
|
2580
|
+
* Get parsed tabindex for this element.
|
|
2581
|
+
*
|
|
2582
|
+
* - If `tabindex` attribute is not present `null` is returned.
|
|
2583
|
+
* - If attribute value is omitted or the empty string `null` is returned.
|
|
2584
|
+
* - If attribute value cannot be parsed `null` is returned.
|
|
2585
|
+
* - If attribute value is dynamic `0` is returned.
|
|
2586
|
+
* - Otherwise the parsed value is returned.
|
|
2587
|
+
*
|
|
2588
|
+
* This property does *NOT* take into account if the element have a default
|
|
2589
|
+
* `tabindex` (such as `<input>` have). Instead use the `focusable` metadata
|
|
2590
|
+
* property to determine this.
|
|
2591
|
+
*
|
|
2592
|
+
* @public
|
|
2593
|
+
* @since 8.16.0
|
|
2594
|
+
*/
|
|
2595
|
+
get tabIndex() {
|
|
2596
|
+
const cached = this.cacheGet(TABINDEX);
|
|
2597
|
+
if (cached !== void 0) {
|
|
2598
|
+
return cached;
|
|
2599
|
+
}
|
|
2600
|
+
const tabindex = this.getAttribute("tabindex");
|
|
2601
|
+
if (!tabindex) {
|
|
2602
|
+
return this.cacheSet(TABINDEX, null);
|
|
2603
|
+
}
|
|
2604
|
+
if (tabindex.value === null) {
|
|
2605
|
+
return this.cacheSet(TABINDEX, null);
|
|
2606
|
+
}
|
|
2607
|
+
if (tabindex.value instanceof DynamicValue) {
|
|
2608
|
+
return this.cacheSet(TABINDEX, 0);
|
|
2609
|
+
}
|
|
2610
|
+
const parsed = parseInt(tabindex.value, 10);
|
|
2611
|
+
if (isNaN(parsed)) {
|
|
2612
|
+
return this.cacheSet(TABINDEX, null);
|
|
2613
|
+
}
|
|
2614
|
+
return this.cacheSet(TABINDEX, parsed);
|
|
2615
|
+
}
|
|
2573
2616
|
/**
|
|
2574
2617
|
* Get a list of all attributes on this node.
|
|
2575
2618
|
*/
|
|
@@ -3366,6 +3409,7 @@ function isKeywordIgnored(options, keyword, matcher = (list, it) => list.include
|
|
|
3366
3409
|
|
|
3367
3410
|
const ARIA_HIDDEN_CACHE = Symbol(isAriaHidden.name);
|
|
3368
3411
|
const HTML_HIDDEN_CACHE = Symbol(isHTMLHidden.name);
|
|
3412
|
+
const INERT_CACHE = Symbol(isInert.name);
|
|
3369
3413
|
const ROLE_PRESENTATION_CACHE = Symbol(isPresentation.name);
|
|
3370
3414
|
const STYLE_HIDDEN_CACHE = Symbol(isStyleHidden.name);
|
|
3371
3415
|
function inAccessibilityTree(node) {
|
|
@@ -3378,6 +3422,9 @@ function inAccessibilityTree(node) {
|
|
|
3378
3422
|
if (isHTMLHidden(node)) {
|
|
3379
3423
|
return false;
|
|
3380
3424
|
}
|
|
3425
|
+
if (isInert(node)) {
|
|
3426
|
+
return false;
|
|
3427
|
+
}
|
|
3381
3428
|
if (isStyleHidden(node)) {
|
|
3382
3429
|
return false;
|
|
3383
3430
|
}
|
|
@@ -3419,6 +3466,24 @@ function isHTMLHidden(node, details) {
|
|
|
3419
3466
|
const result = node.cacheSet(HTML_HIDDEN_CACHE, isHTMLHiddenImpl(node));
|
|
3420
3467
|
return details ? result : result.byParent || result.bySelf;
|
|
3421
3468
|
}
|
|
3469
|
+
function isInertImpl(node) {
|
|
3470
|
+
const isInert2 = (node2) => {
|
|
3471
|
+
const inert = node2.getAttribute("inert");
|
|
3472
|
+
return inert !== null && inert.isStatic;
|
|
3473
|
+
};
|
|
3474
|
+
return {
|
|
3475
|
+
byParent: node.parent ? isInert2(node.parent) : false,
|
|
3476
|
+
bySelf: isInert2(node)
|
|
3477
|
+
};
|
|
3478
|
+
}
|
|
3479
|
+
function isInert(node, details) {
|
|
3480
|
+
const cached = node.cacheGet(INERT_CACHE);
|
|
3481
|
+
if (cached) {
|
|
3482
|
+
return details ? cached : cached.byParent || cached.bySelf;
|
|
3483
|
+
}
|
|
3484
|
+
const result = node.cacheSet(INERT_CACHE, isInertImpl(node));
|
|
3485
|
+
return details ? result : result.byParent || result.bySelf;
|
|
3486
|
+
}
|
|
3422
3487
|
function isStyleHiddenImpl(node) {
|
|
3423
3488
|
const isHidden = (node2) => {
|
|
3424
3489
|
const style = node2.getAttribute("style");
|
|
@@ -6450,6 +6515,46 @@ class HeadingLevel extends Rule {
|
|
|
6450
6515
|
}
|
|
6451
6516
|
}
|
|
6452
6517
|
|
|
6518
|
+
const FOCUSABLE_CACHE = Symbol(isFocusable.name);
|
|
6519
|
+
function isDisabled(element, meta) {
|
|
6520
|
+
var _a;
|
|
6521
|
+
if (!((_a = meta.formAssociated) == null ? void 0 : _a.disablable)) {
|
|
6522
|
+
return false;
|
|
6523
|
+
}
|
|
6524
|
+
const disabled = element.matches("[disabled]");
|
|
6525
|
+
if (disabled) {
|
|
6526
|
+
return true;
|
|
6527
|
+
}
|
|
6528
|
+
const fieldset = element.closest("fieldset[disabled]");
|
|
6529
|
+
if (fieldset) {
|
|
6530
|
+
return true;
|
|
6531
|
+
}
|
|
6532
|
+
return false;
|
|
6533
|
+
}
|
|
6534
|
+
function isFocusableImpl(element) {
|
|
6535
|
+
if (isHTMLHidden(element) || isInert(element) || isStyleHidden(element)) {
|
|
6536
|
+
return false;
|
|
6537
|
+
}
|
|
6538
|
+
const { tabIndex, meta } = element;
|
|
6539
|
+
if (tabIndex !== null) {
|
|
6540
|
+
return tabIndex >= 0;
|
|
6541
|
+
}
|
|
6542
|
+
if (!meta) {
|
|
6543
|
+
return false;
|
|
6544
|
+
}
|
|
6545
|
+
if (isDisabled(element, meta)) {
|
|
6546
|
+
return false;
|
|
6547
|
+
}
|
|
6548
|
+
return Boolean(meta == null ? void 0 : meta.focusable);
|
|
6549
|
+
}
|
|
6550
|
+
function isFocusable(element) {
|
|
6551
|
+
const cached = element.cacheGet(FOCUSABLE_CACHE);
|
|
6552
|
+
if (cached) {
|
|
6553
|
+
return cached;
|
|
6554
|
+
}
|
|
6555
|
+
return element.cacheSet(FOCUSABLE_CACHE, isFocusableImpl(element));
|
|
6556
|
+
}
|
|
6557
|
+
|
|
6453
6558
|
class HiddenFocusable extends Rule {
|
|
6454
6559
|
documentation(context) {
|
|
6455
6560
|
const byParent = context === "parent" ? " In this case it is being hidden by an ancestor with `aria-hidden.`" : "";
|
|
@@ -6463,7 +6568,8 @@ class HiddenFocusable extends Rule {
|
|
|
6463
6568
|
"To fix this either:",
|
|
6464
6569
|
" - Remove `aria-hidden`.",
|
|
6465
6570
|
" - Remove the element from the DOM instead.",
|
|
6466
|
-
|
|
6571
|
+
' - Use `tabindex="-1"` to remove the element from tab order.',
|
|
6572
|
+
" - Use `hidden`, `inert` or similar means to hide or disable the element."
|
|
6467
6573
|
].join("\n"),
|
|
6468
6574
|
url: "https://html-validate.org/rules/hidden-focusable.html"
|
|
6469
6575
|
};
|
|
@@ -6474,21 +6580,13 @@ class HiddenFocusable extends Rule {
|
|
|
6474
6580
|
this.on("dom:ready", (event) => {
|
|
6475
6581
|
const { document } = event;
|
|
6476
6582
|
for (const element of document.querySelectorAll(selector)) {
|
|
6477
|
-
if (
|
|
6478
|
-
|
|
6479
|
-
}
|
|
6480
|
-
if (isAriaHidden(element)) {
|
|
6481
|
-
this.validateElement(element);
|
|
6583
|
+
if (isFocusable(element) && isAriaHidden(element)) {
|
|
6584
|
+
this.reportElement(element);
|
|
6482
6585
|
}
|
|
6483
6586
|
}
|
|
6484
6587
|
});
|
|
6485
6588
|
}
|
|
6486
|
-
|
|
6487
|
-
const { meta } = element;
|
|
6488
|
-
const tabindex = element.getAttribute("tabindex");
|
|
6489
|
-
if (meta && !meta.focusable && !tabindex) {
|
|
6490
|
-
return;
|
|
6491
|
-
}
|
|
6589
|
+
reportElement(element) {
|
|
6492
6590
|
const attribute = element.getAttribute("aria-hidden");
|
|
6493
6591
|
const message = attribute ? `aria-hidden cannot be used on focusable elements` : `aria-hidden cannot be used on focusable elements (hidden by ancestor element)`;
|
|
6494
6592
|
const location = attribute ? attribute.keyLocation : element.location;
|
|
@@ -6546,26 +6644,6 @@ class IdPattern extends Rule {
|
|
|
6546
6644
|
const restricted = /* @__PURE__ */ new Map([
|
|
6547
6645
|
["accept", ["file"]],
|
|
6548
6646
|
["alt", ["image"]],
|
|
6549
|
-
[
|
|
6550
|
-
"autocomplete",
|
|
6551
|
-
[
|
|
6552
|
-
"hidden",
|
|
6553
|
-
"text",
|
|
6554
|
-
"search",
|
|
6555
|
-
"url",
|
|
6556
|
-
"tel",
|
|
6557
|
-
"email",
|
|
6558
|
-
"password",
|
|
6559
|
-
"date",
|
|
6560
|
-
"month",
|
|
6561
|
-
"week",
|
|
6562
|
-
"time",
|
|
6563
|
-
"datetime-local",
|
|
6564
|
-
"number",
|
|
6565
|
-
"range",
|
|
6566
|
-
"color"
|
|
6567
|
-
]
|
|
6568
|
-
],
|
|
6569
6647
|
["capture", ["file"]],
|
|
6570
6648
|
["checked", ["checkbox", "radio"]],
|
|
6571
6649
|
["dirname", ["text", "search"]],
|
|
@@ -6946,7 +7024,7 @@ class MetaRefresh extends Rule {
|
|
|
6946
7024
|
}
|
|
6947
7025
|
}
|
|
6948
7026
|
function parseContent(text) {
|
|
6949
|
-
const match = text.match(/^(\d+)(?:\s*;\s*url=(.*))?/);
|
|
7027
|
+
const match = text.match(/^(\d+)(?:\s*;\s*url=(.*))?/i);
|
|
6950
7028
|
if (match) {
|
|
6951
7029
|
return {
|
|
6952
7030
|
delay: parseInt(match[1], 10),
|
|
@@ -9033,6 +9111,522 @@ class UnknownCharReference extends Rule {
|
|
|
9033
9111
|
}
|
|
9034
9112
|
}
|
|
9035
9113
|
|
|
9114
|
+
const expectedOrder = ["section", "hint", "contact", "field1", "field2", "webauthn"];
|
|
9115
|
+
const fieldNames1 = [
|
|
9116
|
+
"name",
|
|
9117
|
+
"honorific-prefix",
|
|
9118
|
+
"given-name",
|
|
9119
|
+
"additional-name",
|
|
9120
|
+
"family-name",
|
|
9121
|
+
"honorific-suffix",
|
|
9122
|
+
"nickname",
|
|
9123
|
+
"username",
|
|
9124
|
+
"new-password",
|
|
9125
|
+
"current-password",
|
|
9126
|
+
"one-time-code",
|
|
9127
|
+
"organization-title",
|
|
9128
|
+
"organization",
|
|
9129
|
+
"street-address",
|
|
9130
|
+
"address-line1",
|
|
9131
|
+
"address-line2",
|
|
9132
|
+
"address-line3",
|
|
9133
|
+
"address-level4",
|
|
9134
|
+
"address-level3",
|
|
9135
|
+
"address-level2",
|
|
9136
|
+
"address-level1",
|
|
9137
|
+
"country",
|
|
9138
|
+
"country-name",
|
|
9139
|
+
"postal-code",
|
|
9140
|
+
"cc-name",
|
|
9141
|
+
"cc-given-name",
|
|
9142
|
+
"cc-additional-name",
|
|
9143
|
+
"cc-family-name",
|
|
9144
|
+
"cc-number",
|
|
9145
|
+
"cc-exp",
|
|
9146
|
+
"cc-exp-month",
|
|
9147
|
+
"cc-exp-year",
|
|
9148
|
+
"cc-csc",
|
|
9149
|
+
"cc-type",
|
|
9150
|
+
"transaction-currency",
|
|
9151
|
+
"transaction-amount",
|
|
9152
|
+
"language",
|
|
9153
|
+
"bday",
|
|
9154
|
+
"bday-day",
|
|
9155
|
+
"bday-month",
|
|
9156
|
+
"bday-year",
|
|
9157
|
+
"sex",
|
|
9158
|
+
"url",
|
|
9159
|
+
"photo"
|
|
9160
|
+
];
|
|
9161
|
+
const fieldNames2 = [
|
|
9162
|
+
"tel",
|
|
9163
|
+
"tel-country-code",
|
|
9164
|
+
"tel-national",
|
|
9165
|
+
"tel-area-code",
|
|
9166
|
+
"tel-local",
|
|
9167
|
+
"tel-local-prefix",
|
|
9168
|
+
"tel-local-suffix",
|
|
9169
|
+
"tel-extension",
|
|
9170
|
+
"email",
|
|
9171
|
+
"impp"
|
|
9172
|
+
];
|
|
9173
|
+
const fieldNameGroup = {
|
|
9174
|
+
name: "text",
|
|
9175
|
+
"honorific-prefix": "text",
|
|
9176
|
+
"given-name": "text",
|
|
9177
|
+
"additional-name": "text",
|
|
9178
|
+
"family-name": "text",
|
|
9179
|
+
"honorific-suffix": "text",
|
|
9180
|
+
nickname: "text",
|
|
9181
|
+
username: "username",
|
|
9182
|
+
"new-password": "password",
|
|
9183
|
+
"current-password": "password",
|
|
9184
|
+
"one-time-code": "password",
|
|
9185
|
+
"organization-title": "text",
|
|
9186
|
+
organization: "text",
|
|
9187
|
+
"street-address": "multiline",
|
|
9188
|
+
"address-line1": "text",
|
|
9189
|
+
"address-line2": "text",
|
|
9190
|
+
"address-line3": "text",
|
|
9191
|
+
"address-level4": "text",
|
|
9192
|
+
"address-level3": "text",
|
|
9193
|
+
"address-level2": "text",
|
|
9194
|
+
"address-level1": "text",
|
|
9195
|
+
country: "text",
|
|
9196
|
+
"country-name": "text",
|
|
9197
|
+
"postal-code": "text",
|
|
9198
|
+
"cc-name": "text",
|
|
9199
|
+
"cc-given-name": "text",
|
|
9200
|
+
"cc-additional-name": "text",
|
|
9201
|
+
"cc-family-name": "text",
|
|
9202
|
+
"cc-number": "text",
|
|
9203
|
+
"cc-exp": "month",
|
|
9204
|
+
"cc-exp-month": "numeric",
|
|
9205
|
+
"cc-exp-year": "numeric",
|
|
9206
|
+
"cc-csc": "text",
|
|
9207
|
+
"cc-type": "text",
|
|
9208
|
+
"transaction-currency": "text",
|
|
9209
|
+
"transaction-amount": "numeric",
|
|
9210
|
+
language: "text",
|
|
9211
|
+
bday: "date",
|
|
9212
|
+
"bday-day": "numeric",
|
|
9213
|
+
"bday-month": "numeric",
|
|
9214
|
+
"bday-year": "numeric",
|
|
9215
|
+
sex: "text",
|
|
9216
|
+
url: "url",
|
|
9217
|
+
photo: "url",
|
|
9218
|
+
tel: "tel",
|
|
9219
|
+
"tel-country-code": "text",
|
|
9220
|
+
"tel-national": "text",
|
|
9221
|
+
"tel-area-code": "text",
|
|
9222
|
+
"tel-local": "text",
|
|
9223
|
+
"tel-local-prefix": "text",
|
|
9224
|
+
"tel-local-suffix": "text",
|
|
9225
|
+
"tel-extension": "text",
|
|
9226
|
+
email: "username",
|
|
9227
|
+
impp: "url"
|
|
9228
|
+
};
|
|
9229
|
+
const disallowedInputTypes = ["checkbox", "radio", "file", "submit", "image", "reset", "button"];
|
|
9230
|
+
function matchSection(token) {
|
|
9231
|
+
return token.startsWith("section-");
|
|
9232
|
+
}
|
|
9233
|
+
function matchHint(token) {
|
|
9234
|
+
return token === "shipping" || token === "billing";
|
|
9235
|
+
}
|
|
9236
|
+
function matchFieldNames1(token) {
|
|
9237
|
+
return fieldNames1.includes(token);
|
|
9238
|
+
}
|
|
9239
|
+
function matchContact(token) {
|
|
9240
|
+
const haystack = ["home", "work", "mobile", "fax", "pager"];
|
|
9241
|
+
return haystack.includes(token);
|
|
9242
|
+
}
|
|
9243
|
+
function matchFieldNames2(token) {
|
|
9244
|
+
return fieldNames2.includes(token);
|
|
9245
|
+
}
|
|
9246
|
+
function matchWebauthn(token) {
|
|
9247
|
+
return token === "webauthn";
|
|
9248
|
+
}
|
|
9249
|
+
function matchToken(token) {
|
|
9250
|
+
if (matchSection(token)) {
|
|
9251
|
+
return "section";
|
|
9252
|
+
}
|
|
9253
|
+
if (matchHint(token)) {
|
|
9254
|
+
return "hint";
|
|
9255
|
+
}
|
|
9256
|
+
if (matchFieldNames1(token)) {
|
|
9257
|
+
return "field1";
|
|
9258
|
+
}
|
|
9259
|
+
if (matchFieldNames2(token)) {
|
|
9260
|
+
return "field2";
|
|
9261
|
+
}
|
|
9262
|
+
if (matchContact(token)) {
|
|
9263
|
+
return "contact";
|
|
9264
|
+
}
|
|
9265
|
+
if (matchWebauthn(token)) {
|
|
9266
|
+
return "webauthn";
|
|
9267
|
+
}
|
|
9268
|
+
return null;
|
|
9269
|
+
}
|
|
9270
|
+
function getControlGroups(type) {
|
|
9271
|
+
const allGroups = [
|
|
9272
|
+
"text",
|
|
9273
|
+
"multiline",
|
|
9274
|
+
"password",
|
|
9275
|
+
"url",
|
|
9276
|
+
"username",
|
|
9277
|
+
"tel",
|
|
9278
|
+
"numeric",
|
|
9279
|
+
"month",
|
|
9280
|
+
"date"
|
|
9281
|
+
];
|
|
9282
|
+
const mapping = {
|
|
9283
|
+
hidden: allGroups,
|
|
9284
|
+
text: allGroups.filter((it) => it !== "multiline"),
|
|
9285
|
+
search: allGroups.filter((it) => it !== "multiline"),
|
|
9286
|
+
password: ["password"],
|
|
9287
|
+
url: ["url"],
|
|
9288
|
+
email: ["username"],
|
|
9289
|
+
tel: ["tel"],
|
|
9290
|
+
number: ["numeric"],
|
|
9291
|
+
month: ["month"],
|
|
9292
|
+
date: ["date"]
|
|
9293
|
+
};
|
|
9294
|
+
const groups = mapping[type];
|
|
9295
|
+
if (groups) {
|
|
9296
|
+
return groups;
|
|
9297
|
+
}
|
|
9298
|
+
return [];
|
|
9299
|
+
}
|
|
9300
|
+
function isDisallowedType(node, type) {
|
|
9301
|
+
if (!node.is("input")) {
|
|
9302
|
+
return false;
|
|
9303
|
+
}
|
|
9304
|
+
return disallowedInputTypes.includes(type);
|
|
9305
|
+
}
|
|
9306
|
+
function getTerminalMessage(context) {
|
|
9307
|
+
switch (context.msg) {
|
|
9308
|
+
case 0 /* InvalidAttribute */:
|
|
9309
|
+
return "autocomplete attribute cannot be used on {{ what }}";
|
|
9310
|
+
case 1 /* InvalidValue */:
|
|
9311
|
+
return '"{{ value }}" cannot be used on {{ what }}';
|
|
9312
|
+
case 2 /* InvalidOrder */:
|
|
9313
|
+
return '"{{ second }}" must appear before "{{ first }}"';
|
|
9314
|
+
case 3 /* InvalidToken */:
|
|
9315
|
+
return '"{{ token }}" is not a valid autocomplete token or field name';
|
|
9316
|
+
case 4 /* InvalidCombination */:
|
|
9317
|
+
return '"{{ second }}" cannot be combined with "{{ first }}"';
|
|
9318
|
+
case 5 /* MissingField */:
|
|
9319
|
+
return "autocomplete attribute is missing field name";
|
|
9320
|
+
}
|
|
9321
|
+
}
|
|
9322
|
+
function getMarkdownMessage(context) {
|
|
9323
|
+
switch (context.msg) {
|
|
9324
|
+
case 0 /* InvalidAttribute */:
|
|
9325
|
+
return [
|
|
9326
|
+
`\`autocomplete\` attribute cannot be used on \`${context.what}\``,
|
|
9327
|
+
"",
|
|
9328
|
+
"The following input types cannot use the `autocomplete` attribute:",
|
|
9329
|
+
"",
|
|
9330
|
+
...disallowedInputTypes.map((it) => `- \`${it}\``)
|
|
9331
|
+
].join("\n");
|
|
9332
|
+
case 1 /* InvalidValue */: {
|
|
9333
|
+
const message = `\`"${context.value}"\` cannot be used on \`${context.what}\``;
|
|
9334
|
+
if (context.type === "form") {
|
|
9335
|
+
return [
|
|
9336
|
+
message,
|
|
9337
|
+
"",
|
|
9338
|
+
'The `<form>` element can only use the values `"on"` and `"off"`.'
|
|
9339
|
+
].join("\n");
|
|
9340
|
+
}
|
|
9341
|
+
if (context.type === "hidden") {
|
|
9342
|
+
return [
|
|
9343
|
+
message,
|
|
9344
|
+
"",
|
|
9345
|
+
'`<input type="hidden">` cannot use the values `"on"` and `"off"`.'
|
|
9346
|
+
].join("\n");
|
|
9347
|
+
}
|
|
9348
|
+
const controlGroups = getControlGroups(context.type);
|
|
9349
|
+
const currentGroup = fieldNameGroup[context.value];
|
|
9350
|
+
return [
|
|
9351
|
+
message,
|
|
9352
|
+
"",
|
|
9353
|
+
`\`${context.what}\` allows autocomplete fields from the following group${controlGroups.length > 1 ? "s" : ""}:`,
|
|
9354
|
+
"",
|
|
9355
|
+
...controlGroups.map((it) => `- ${it}`),
|
|
9356
|
+
"",
|
|
9357
|
+
`The field \`"${context.value}"\` belongs to the group /${currentGroup}/ which cannot be used with this input type.`
|
|
9358
|
+
].join("\n");
|
|
9359
|
+
}
|
|
9360
|
+
case 2 /* InvalidOrder */:
|
|
9361
|
+
return [
|
|
9362
|
+
`\`"${context.second}"\` must appear before \`"${context.first}"\``,
|
|
9363
|
+
"",
|
|
9364
|
+
"The autocomplete tokens must appear in the following order:",
|
|
9365
|
+
"",
|
|
9366
|
+
"- Optional section name (`section-` prefix).",
|
|
9367
|
+
"- Optional `shipping` or `billing` token.",
|
|
9368
|
+
"- Optional `home`, `work`, `mobile`, `fax` or `pager` token (for fields supporting it).",
|
|
9369
|
+
"- Field name",
|
|
9370
|
+
"- Optional `webauthn` token."
|
|
9371
|
+
].join("\n");
|
|
9372
|
+
case 3 /* InvalidToken */:
|
|
9373
|
+
return `\`"${context.token}"\` is not a valid autocomplete token or field name`;
|
|
9374
|
+
case 4 /* InvalidCombination */:
|
|
9375
|
+
return `\`"${context.second}"\` cannot be combined with \`"${context.first}"\``;
|
|
9376
|
+
case 5 /* MissingField */:
|
|
9377
|
+
return "Autocomplete attribute is missing field name";
|
|
9378
|
+
}
|
|
9379
|
+
}
|
|
9380
|
+
class ValidAutocomplete extends Rule {
|
|
9381
|
+
documentation(context) {
|
|
9382
|
+
return {
|
|
9383
|
+
description: getMarkdownMessage(context),
|
|
9384
|
+
url: "https://html-validate.org/rules/valid-autocomplete.html"
|
|
9385
|
+
};
|
|
9386
|
+
}
|
|
9387
|
+
setup() {
|
|
9388
|
+
this.on("dom:ready", (event) => {
|
|
9389
|
+
const { document } = event;
|
|
9390
|
+
const elements = document.querySelectorAll("[autocomplete]");
|
|
9391
|
+
for (const element of elements) {
|
|
9392
|
+
const autocomplete = element.getAttribute("autocomplete");
|
|
9393
|
+
if (autocomplete.value === null || autocomplete.value instanceof DynamicValue) {
|
|
9394
|
+
continue;
|
|
9395
|
+
}
|
|
9396
|
+
const location = autocomplete.valueLocation;
|
|
9397
|
+
const value = autocomplete.value.toLowerCase();
|
|
9398
|
+
const tokens = new DOMTokenList(value, location);
|
|
9399
|
+
if (tokens.length === 0) {
|
|
9400
|
+
continue;
|
|
9401
|
+
}
|
|
9402
|
+
this.validate(element, value, tokens, autocomplete.keyLocation, location);
|
|
9403
|
+
}
|
|
9404
|
+
});
|
|
9405
|
+
}
|
|
9406
|
+
validate(node, value, tokens, keyLocation, valueLocation) {
|
|
9407
|
+
switch (node.tagName) {
|
|
9408
|
+
case "form":
|
|
9409
|
+
this.validateFormAutocomplete(node, value, valueLocation);
|
|
9410
|
+
break;
|
|
9411
|
+
case "input":
|
|
9412
|
+
case "textarea":
|
|
9413
|
+
case "select":
|
|
9414
|
+
this.validateControlAutocomplete(node, tokens, keyLocation);
|
|
9415
|
+
break;
|
|
9416
|
+
}
|
|
9417
|
+
}
|
|
9418
|
+
validateControlAutocomplete(node, tokens, keyLocation) {
|
|
9419
|
+
const type = node.getAttributeValue("type") ?? "text";
|
|
9420
|
+
const mantle = type !== "hidden" ? "expectation" : "anchor";
|
|
9421
|
+
if (isDisallowedType(node, type)) {
|
|
9422
|
+
const context = {
|
|
9423
|
+
msg: 0 /* InvalidAttribute */,
|
|
9424
|
+
what: `<input type="${type}">`
|
|
9425
|
+
};
|
|
9426
|
+
this.report({
|
|
9427
|
+
node,
|
|
9428
|
+
message: getTerminalMessage(context),
|
|
9429
|
+
location: keyLocation,
|
|
9430
|
+
context
|
|
9431
|
+
});
|
|
9432
|
+
return;
|
|
9433
|
+
}
|
|
9434
|
+
if (tokens.includes("on") || tokens.includes("off")) {
|
|
9435
|
+
this.validateOnOff(node, mantle, tokens);
|
|
9436
|
+
return;
|
|
9437
|
+
}
|
|
9438
|
+
this.validateTokens(node, tokens, keyLocation);
|
|
9439
|
+
}
|
|
9440
|
+
validateFormAutocomplete(node, value, location) {
|
|
9441
|
+
const trimmed = value.trim();
|
|
9442
|
+
if (["on", "off"].includes(trimmed)) {
|
|
9443
|
+
return;
|
|
9444
|
+
}
|
|
9445
|
+
const context = {
|
|
9446
|
+
msg: 1 /* InvalidValue */,
|
|
9447
|
+
type: "form",
|
|
9448
|
+
value: trimmed,
|
|
9449
|
+
what: "<form>"
|
|
9450
|
+
};
|
|
9451
|
+
this.report({
|
|
9452
|
+
node,
|
|
9453
|
+
message: getTerminalMessage(context),
|
|
9454
|
+
location,
|
|
9455
|
+
context
|
|
9456
|
+
});
|
|
9457
|
+
}
|
|
9458
|
+
validateOnOff(node, mantle, tokens) {
|
|
9459
|
+
const index = tokens.findIndex((it) => it === "on" || it === "off");
|
|
9460
|
+
const value = tokens.item(index);
|
|
9461
|
+
const location = tokens.location(index);
|
|
9462
|
+
if (tokens.length > 1) {
|
|
9463
|
+
const context = {
|
|
9464
|
+
msg: 4 /* InvalidCombination */,
|
|
9465
|
+
/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
|
|
9466
|
+
first: tokens.item(index > 0 ? 0 : 1),
|
|
9467
|
+
second: value
|
|
9468
|
+
};
|
|
9469
|
+
this.report({
|
|
9470
|
+
node,
|
|
9471
|
+
message: getTerminalMessage(context),
|
|
9472
|
+
location,
|
|
9473
|
+
context
|
|
9474
|
+
});
|
|
9475
|
+
}
|
|
9476
|
+
switch (mantle) {
|
|
9477
|
+
case "expectation":
|
|
9478
|
+
return;
|
|
9479
|
+
case "anchor": {
|
|
9480
|
+
const context = {
|
|
9481
|
+
msg: 1 /* InvalidValue */,
|
|
9482
|
+
type: "hidden",
|
|
9483
|
+
value,
|
|
9484
|
+
what: `<input type="hidden">`
|
|
9485
|
+
};
|
|
9486
|
+
this.report({
|
|
9487
|
+
node,
|
|
9488
|
+
message: getTerminalMessage(context),
|
|
9489
|
+
location: tokens.location(0),
|
|
9490
|
+
context
|
|
9491
|
+
});
|
|
9492
|
+
}
|
|
9493
|
+
}
|
|
9494
|
+
}
|
|
9495
|
+
validateTokens(node, tokens, keyLocation) {
|
|
9496
|
+
const order = [];
|
|
9497
|
+
for (const { item, location } of tokens.iterator()) {
|
|
9498
|
+
const tokenType = matchToken(item);
|
|
9499
|
+
if (tokenType) {
|
|
9500
|
+
order.push(tokenType);
|
|
9501
|
+
} else {
|
|
9502
|
+
const context = {
|
|
9503
|
+
msg: 3 /* InvalidToken */,
|
|
9504
|
+
token: item
|
|
9505
|
+
};
|
|
9506
|
+
this.report({
|
|
9507
|
+
node,
|
|
9508
|
+
message: getTerminalMessage(context),
|
|
9509
|
+
location,
|
|
9510
|
+
context
|
|
9511
|
+
});
|
|
9512
|
+
return;
|
|
9513
|
+
}
|
|
9514
|
+
}
|
|
9515
|
+
const fieldTokens = order.map((it) => it === "field1" || it === "field2");
|
|
9516
|
+
this.validateFieldPresence(node, tokens, fieldTokens, keyLocation);
|
|
9517
|
+
this.validateContact(node, tokens, order);
|
|
9518
|
+
this.validateOrder(node, tokens, order);
|
|
9519
|
+
this.validateControlGroup(node, tokens, fieldTokens);
|
|
9520
|
+
}
|
|
9521
|
+
/**
|
|
9522
|
+
* Ensure that exactly one field name is present from the two field lists.
|
|
9523
|
+
*/
|
|
9524
|
+
validateFieldPresence(node, tokens, fieldTokens, keyLocation) {
|
|
9525
|
+
const numFields = fieldTokens.filter(Boolean).length;
|
|
9526
|
+
if (numFields === 0) {
|
|
9527
|
+
const context = {
|
|
9528
|
+
msg: 5 /* MissingField */
|
|
9529
|
+
};
|
|
9530
|
+
this.report({
|
|
9531
|
+
node,
|
|
9532
|
+
message: getTerminalMessage(context),
|
|
9533
|
+
location: keyLocation,
|
|
9534
|
+
context
|
|
9535
|
+
});
|
|
9536
|
+
} else if (numFields > 1) {
|
|
9537
|
+
const a = fieldTokens.indexOf(true);
|
|
9538
|
+
const b = fieldTokens.lastIndexOf(true);
|
|
9539
|
+
const context = {
|
|
9540
|
+
msg: 4 /* InvalidCombination */,
|
|
9541
|
+
/* eslint-disable @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
|
|
9542
|
+
first: tokens.item(a),
|
|
9543
|
+
second: tokens.item(b)
|
|
9544
|
+
/* eslint-enable @typescript-eslint/no-non-null-assertion */
|
|
9545
|
+
};
|
|
9546
|
+
this.report({
|
|
9547
|
+
node,
|
|
9548
|
+
message: getTerminalMessage(context),
|
|
9549
|
+
location: tokens.location(b),
|
|
9550
|
+
context
|
|
9551
|
+
});
|
|
9552
|
+
}
|
|
9553
|
+
}
|
|
9554
|
+
/**
|
|
9555
|
+
* Ensure contact token is only used with field names from the second list.
|
|
9556
|
+
*/
|
|
9557
|
+
validateContact(node, tokens, order) {
|
|
9558
|
+
if (order.includes("contact") && order.includes("field1")) {
|
|
9559
|
+
const a = order.indexOf("field1");
|
|
9560
|
+
const b = order.indexOf("contact");
|
|
9561
|
+
const context = {
|
|
9562
|
+
msg: 4 /* InvalidCombination */,
|
|
9563
|
+
/* eslint-disable @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
|
|
9564
|
+
first: tokens.item(a),
|
|
9565
|
+
second: tokens.item(b)
|
|
9566
|
+
/* eslint-enable @typescript-eslint/no-non-null-assertion */
|
|
9567
|
+
};
|
|
9568
|
+
this.report({
|
|
9569
|
+
node,
|
|
9570
|
+
message: getTerminalMessage(context),
|
|
9571
|
+
location: tokens.location(b),
|
|
9572
|
+
context
|
|
9573
|
+
});
|
|
9574
|
+
}
|
|
9575
|
+
}
|
|
9576
|
+
validateOrder(node, tokens, order) {
|
|
9577
|
+
const indicies = order.map((it) => expectedOrder.indexOf(it));
|
|
9578
|
+
for (let i = 0; i < indicies.length - 1; i++) {
|
|
9579
|
+
if (indicies[0] > indicies[i + 1]) {
|
|
9580
|
+
const context = {
|
|
9581
|
+
msg: 2 /* InvalidOrder */,
|
|
9582
|
+
/* eslint-disable @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
|
|
9583
|
+
first: tokens.item(i),
|
|
9584
|
+
second: tokens.item(i + 1)
|
|
9585
|
+
/* eslint-enable @typescript-eslint/no-non-null-assertion */
|
|
9586
|
+
};
|
|
9587
|
+
this.report({
|
|
9588
|
+
node,
|
|
9589
|
+
message: getTerminalMessage(context),
|
|
9590
|
+
location: tokens.location(i + 1),
|
|
9591
|
+
context
|
|
9592
|
+
});
|
|
9593
|
+
}
|
|
9594
|
+
}
|
|
9595
|
+
}
|
|
9596
|
+
validateControlGroup(node, tokens, fieldTokens) {
|
|
9597
|
+
const numFields = fieldTokens.filter(Boolean).length;
|
|
9598
|
+
if (numFields === 0) {
|
|
9599
|
+
return;
|
|
9600
|
+
}
|
|
9601
|
+
if (!node.is("input")) {
|
|
9602
|
+
return;
|
|
9603
|
+
}
|
|
9604
|
+
const attr = node.getAttribute("type");
|
|
9605
|
+
const type = (attr == null ? void 0 : attr.value) ?? "text";
|
|
9606
|
+
if (type instanceof DynamicValue) {
|
|
9607
|
+
return;
|
|
9608
|
+
}
|
|
9609
|
+
const controlGroups = getControlGroups(type);
|
|
9610
|
+
const fieldIndex = fieldTokens.indexOf(true);
|
|
9611
|
+
const fieldToken = tokens.item(fieldIndex);
|
|
9612
|
+
const fieldGroup = fieldNameGroup[fieldToken];
|
|
9613
|
+
if (!controlGroups.includes(fieldGroup)) {
|
|
9614
|
+
const context = {
|
|
9615
|
+
msg: 1 /* InvalidValue */,
|
|
9616
|
+
type,
|
|
9617
|
+
value: fieldToken,
|
|
9618
|
+
what: `<input type="${type}">`
|
|
9619
|
+
};
|
|
9620
|
+
this.report({
|
|
9621
|
+
node,
|
|
9622
|
+
message: getTerminalMessage(context),
|
|
9623
|
+
location: tokens.location(fieldIndex),
|
|
9624
|
+
context
|
|
9625
|
+
});
|
|
9626
|
+
}
|
|
9627
|
+
}
|
|
9628
|
+
}
|
|
9629
|
+
|
|
9036
9630
|
const defaults$3 = {
|
|
9037
9631
|
relaxed: false
|
|
9038
9632
|
};
|
|
@@ -9587,6 +10181,7 @@ const bundledRules = {
|
|
|
9587
10181
|
"text-content": TextContent,
|
|
9588
10182
|
"unique-landmark": UniqueLandmark,
|
|
9589
10183
|
"unrecognized-char-ref": UnknownCharReference,
|
|
10184
|
+
"valid-autocomplete": ValidAutocomplete,
|
|
9590
10185
|
"valid-id": ValidID,
|
|
9591
10186
|
"void-content": VoidContent,
|
|
9592
10187
|
"void-style": VoidStyle,
|
|
@@ -9617,6 +10212,7 @@ const config$4 = {
|
|
|
9617
10212
|
"svg-focusable": "off",
|
|
9618
10213
|
"text-content": "error",
|
|
9619
10214
|
"unique-landmark": "error",
|
|
10215
|
+
"valid-autocomplete": "error",
|
|
9620
10216
|
"wcag/h30": "error",
|
|
9621
10217
|
"wcag/h32": "error",
|
|
9622
10218
|
"wcag/h36": "error",
|
|
@@ -9716,6 +10312,7 @@ const config$1 = {
|
|
|
9716
10312
|
"text-content": "error",
|
|
9717
10313
|
"unique-landmark": "error",
|
|
9718
10314
|
"unrecognized-char-ref": "error",
|
|
10315
|
+
"valid-autocomplete": "error",
|
|
9719
10316
|
"valid-id": ["error", { relaxed: false }],
|
|
9720
10317
|
void: "off",
|
|
9721
10318
|
"void-content": "error",
|
|
@@ -9763,6 +10360,7 @@ const config = {
|
|
|
9763
10360
|
"no-unused-disable": "error",
|
|
9764
10361
|
"script-element": "error",
|
|
9765
10362
|
"unrecognized-char-ref": "error",
|
|
10363
|
+
"valid-autocomplete": "error",
|
|
9766
10364
|
"valid-id": ["error", { relaxed: true }],
|
|
9767
10365
|
"void-content": "error"
|
|
9768
10366
|
}
|
|
@@ -11988,7 +12586,7 @@ class HtmlValidate {
|
|
|
11988
12586
|
}
|
|
11989
12587
|
|
|
11990
12588
|
const name = "html-validate";
|
|
11991
|
-
const version = "8.
|
|
12589
|
+
const version = "8.16.0";
|
|
11992
12590
|
const bugs = "https://gitlab.com/html-validate/html-validate/issues/new";
|
|
11993
12591
|
|
|
11994
12592
|
function definePlugin(plugin) {
|