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/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
- " - Use the `hidden` attribute or similar means to hide the element."
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 (isHTMLHidden(element) || isStyleHidden(element)) {
6478
- continue;
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
- validateElement(element) {
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.14.0";
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) {