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