html-validate 8.13.0 → 8.15.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
@@ -3844,7 +3844,7 @@ class Rule {
3844
3844
  }
3845
3845
  }
3846
3846
 
3847
- const defaults$v = {
3847
+ const defaults$w = {
3848
3848
  allowExternal: true,
3849
3849
  allowRelative: true,
3850
3850
  allowAbsolute: true,
@@ -3885,7 +3885,7 @@ function matchList(value, list) {
3885
3885
  }
3886
3886
  class AllowedLinks extends Rule {
3887
3887
  constructor(options) {
3888
- super({ ...defaults$v, ...options });
3888
+ super({ ...defaults$w, ...options });
3889
3889
  this.allowExternal = parseAllow(this.options.allowExternal);
3890
3890
  this.allowRelative = parseAllow(this.options.allowRelative);
3891
3891
  this.allowAbsolute = parseAllow(this.options.allowAbsolute);
@@ -4049,7 +4049,7 @@ class AllowedLinks extends Rule {
4049
4049
  }
4050
4050
  }
4051
4051
 
4052
- const defaults$u = {
4052
+ const defaults$v = {
4053
4053
  accessible: true
4054
4054
  };
4055
4055
  function findByTarget(target, siblings) {
@@ -4079,7 +4079,7 @@ function getDescription$1(context) {
4079
4079
  }
4080
4080
  class AreaAlt extends Rule {
4081
4081
  constructor(options) {
4082
- super({ ...defaults$u, ...options });
4082
+ super({ ...defaults$v, ...options });
4083
4083
  }
4084
4084
  static schema() {
4085
4085
  return {
@@ -4158,7 +4158,7 @@ class AriaHiddenBody extends Rule {
4158
4158
  }
4159
4159
  }
4160
4160
 
4161
- const defaults$t = {
4161
+ const defaults$u = {
4162
4162
  allowAnyNamable: false
4163
4163
  };
4164
4164
  const whitelisted = [
@@ -4200,7 +4200,7 @@ function isValidUsage(target, meta) {
4200
4200
  }
4201
4201
  class AriaLabelMisuse extends Rule {
4202
4202
  constructor(options) {
4203
- super({ ...defaults$t, ...options });
4203
+ super({ ...defaults$u, ...options });
4204
4204
  }
4205
4205
  documentation() {
4206
4206
  const valid = [
@@ -4310,13 +4310,13 @@ class CaseStyle {
4310
4310
  }
4311
4311
  }
4312
4312
 
4313
- const defaults$s = {
4313
+ const defaults$t = {
4314
4314
  style: "lowercase",
4315
4315
  ignoreForeign: true
4316
4316
  };
4317
4317
  class AttrCase extends Rule {
4318
4318
  constructor(options) {
4319
- super({ ...defaults$s, ...options });
4319
+ super({ ...defaults$t, ...options });
4320
4320
  this.style = new CaseStyle(this.options.style, "attr-case");
4321
4321
  }
4322
4322
  static schema() {
@@ -4672,7 +4672,7 @@ class AttrDelimiter extends Rule {
4672
4672
  }
4673
4673
 
4674
4674
  const DEFAULT_PATTERN = "[a-z0-9-:]+";
4675
- const defaults$r = {
4675
+ const defaults$s = {
4676
4676
  pattern: DEFAULT_PATTERN,
4677
4677
  ignoreForeign: true
4678
4678
  };
@@ -4704,7 +4704,7 @@ function generateDescription(name, pattern) {
4704
4704
  }
4705
4705
  class AttrPattern extends Rule {
4706
4706
  constructor(options) {
4707
- super({ ...defaults$r, ...options });
4707
+ super({ ...defaults$s, ...options });
4708
4708
  this.pattern = generateRegexp(this.options.pattern);
4709
4709
  }
4710
4710
  static schema() {
@@ -4751,7 +4751,7 @@ class AttrPattern extends Rule {
4751
4751
  }
4752
4752
  }
4753
4753
 
4754
- const defaults$q = {
4754
+ const defaults$r = {
4755
4755
  style: "auto",
4756
4756
  unquoted: false
4757
4757
  };
@@ -4790,7 +4790,7 @@ function describeStyle(style, unquoted) {
4790
4790
  }
4791
4791
  class AttrQuotes extends Rule {
4792
4792
  constructor(options) {
4793
- super({ ...defaults$q, ...options });
4793
+ super({ ...defaults$r, ...options });
4794
4794
  this.style = parseStyle$3(this.options.style);
4795
4795
  }
4796
4796
  static schema() {
@@ -4974,12 +4974,12 @@ class AttributeAllowedValues extends Rule {
4974
4974
  }
4975
4975
  }
4976
4976
 
4977
- const defaults$p = {
4977
+ const defaults$q = {
4978
4978
  style: "omit"
4979
4979
  };
4980
4980
  class AttributeBooleanStyle extends Rule {
4981
4981
  constructor(options) {
4982
- super({ ...defaults$p, ...options });
4982
+ super({ ...defaults$q, ...options });
4983
4983
  this.hasInvalidStyle = parseStyle$2(this.options.style);
4984
4984
  }
4985
4985
  static schema() {
@@ -5046,12 +5046,12 @@ function reportMessage$1(attr, style) {
5046
5046
  return "";
5047
5047
  }
5048
5048
 
5049
- const defaults$o = {
5049
+ const defaults$p = {
5050
5050
  style: "omit"
5051
5051
  };
5052
5052
  class AttributeEmptyStyle extends Rule {
5053
5053
  constructor(options) {
5054
- super({ ...defaults$o, ...options });
5054
+ super({ ...defaults$p, ...options });
5055
5055
  this.hasInvalidStyle = parseStyle$1(this.options.style);
5056
5056
  }
5057
5057
  static schema() {
@@ -5191,12 +5191,12 @@ function describePattern(pattern) {
5191
5191
  }
5192
5192
  }
5193
5193
 
5194
- const defaults$n = {
5194
+ const defaults$o = {
5195
5195
  pattern: "kebabcase"
5196
5196
  };
5197
5197
  class ClassPattern extends Rule {
5198
5198
  constructor(options) {
5199
- super({ ...defaults$n, ...options });
5199
+ super({ ...defaults$o, ...options });
5200
5200
  this.pattern = parsePattern(this.options.pattern);
5201
5201
  }
5202
5202
  static schema() {
@@ -5302,13 +5302,13 @@ class CloseOrder extends Rule {
5302
5302
  }
5303
5303
  }
5304
5304
 
5305
- const defaults$m = {
5305
+ const defaults$n = {
5306
5306
  include: null,
5307
5307
  exclude: null
5308
5308
  };
5309
5309
  class Deprecated extends Rule {
5310
5310
  constructor(options) {
5311
- super({ ...defaults$m, ...options });
5311
+ super({ ...defaults$n, ...options });
5312
5312
  }
5313
5313
  static schema() {
5314
5314
  return {
@@ -5462,12 +5462,12 @@ let NoStyleTag$1 = class NoStyleTag extends Rule {
5462
5462
  }
5463
5463
  };
5464
5464
 
5465
- const defaults$l = {
5465
+ const defaults$m = {
5466
5466
  style: "uppercase"
5467
5467
  };
5468
5468
  class DoctypeStyle extends Rule {
5469
5469
  constructor(options) {
5470
- super({ ...defaults$l, ...options });
5470
+ super({ ...defaults$m, ...options });
5471
5471
  }
5472
5472
  static schema() {
5473
5473
  return {
@@ -5495,12 +5495,12 @@ class DoctypeStyle extends Rule {
5495
5495
  }
5496
5496
  }
5497
5497
 
5498
- const defaults$k = {
5498
+ const defaults$l = {
5499
5499
  style: "lowercase"
5500
5500
  };
5501
5501
  class ElementCase extends Rule {
5502
5502
  constructor(options) {
5503
- super({ ...defaults$k, ...options });
5503
+ super({ ...defaults$l, ...options });
5504
5504
  this.style = new CaseStyle(this.options.style, "element-case");
5505
5505
  }
5506
5506
  static schema() {
@@ -5560,14 +5560,14 @@ class ElementCase extends Rule {
5560
5560
  }
5561
5561
  }
5562
5562
 
5563
- const defaults$j = {
5563
+ const defaults$k = {
5564
5564
  pattern: "^[a-z][a-z0-9\\-._]*-[a-z0-9\\-._]*$",
5565
5565
  whitelist: [],
5566
5566
  blacklist: []
5567
5567
  };
5568
5568
  class ElementName extends Rule {
5569
5569
  constructor(options) {
5570
- super({ ...defaults$j, ...options });
5570
+ super({ ...defaults$k, ...options });
5571
5571
  this.pattern = new RegExp(this.options.pattern);
5572
5572
  }
5573
5573
  static schema() {
@@ -5604,7 +5604,7 @@ class ElementName extends Rule {
5604
5604
  ...context.blacklist.map((cur) => `- ${cur}`)
5605
5605
  ];
5606
5606
  }
5607
- if (context.pattern !== defaults$j.pattern) {
5607
+ if (context.pattern !== defaults$k.pattern) {
5608
5608
  return [
5609
5609
  `<${context.tagName}> is not a valid element name. This project is configured to only allow names matching the following regular expression:`,
5610
5610
  "",
@@ -6093,7 +6093,7 @@ class EmptyTitle extends Rule {
6093
6093
  }
6094
6094
  }
6095
6095
 
6096
- const defaults$i = {
6096
+ const defaults$j = {
6097
6097
  allowArrayBrackets: true,
6098
6098
  shared: ["radio", "button", "reset", "submit"]
6099
6099
  };
@@ -6121,7 +6121,7 @@ function getDocumentation(context) {
6121
6121
  }
6122
6122
  class FormDupName extends Rule {
6123
6123
  constructor(options) {
6124
- super({ ...defaults$i, ...options });
6124
+ super({ ...defaults$j, ...options });
6125
6125
  }
6126
6126
  static schema() {
6127
6127
  return {
@@ -6270,7 +6270,7 @@ class FormDupName extends Rule {
6270
6270
  }
6271
6271
  }
6272
6272
 
6273
- const defaults$h = {
6273
+ const defaults$i = {
6274
6274
  allowMultipleH1: false,
6275
6275
  minInitialRank: "h1",
6276
6276
  sectioningRoots: ["dialog", '[role="dialog"]', '[role="alertdialog"]']
@@ -6299,7 +6299,7 @@ function parseMaxInitial(value) {
6299
6299
  }
6300
6300
  class HeadingLevel extends Rule {
6301
6301
  constructor(options) {
6302
- super({ ...defaults$h, ...options });
6302
+ super({ ...defaults$i, ...options });
6303
6303
  this.stack = [];
6304
6304
  this.minInitialRank = parseMaxInitial(this.options.minInitialRank);
6305
6305
  this.sectionRoots = this.options.sectioningRoots.map((it) => new Pattern(it));
@@ -6502,12 +6502,12 @@ class HiddenFocusable extends Rule {
6502
6502
  }
6503
6503
  }
6504
6504
 
6505
- const defaults$g = {
6505
+ const defaults$h = {
6506
6506
  pattern: "kebabcase"
6507
6507
  };
6508
6508
  class IdPattern extends Rule {
6509
6509
  constructor(options) {
6510
- super({ ...defaults$g, ...options });
6510
+ super({ ...defaults$h, ...options });
6511
6511
  this.pattern = parsePattern(this.options.pattern);
6512
6512
  }
6513
6513
  static schema() {
@@ -6546,26 +6546,6 @@ class IdPattern extends Rule {
6546
6546
  const restricted = /* @__PURE__ */ new Map([
6547
6547
  ["accept", ["file"]],
6548
6548
  ["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
6549
  ["capture", ["file"]],
6570
6550
  ["checked", ["checkbox", "radio"]],
6571
6551
  ["dirname", ["text", "search"]],
@@ -6860,12 +6840,12 @@ function findLabelByParent(el) {
6860
6840
  return [];
6861
6841
  }
6862
6842
 
6863
- const defaults$f = {
6843
+ const defaults$g = {
6864
6844
  maxlength: 70
6865
6845
  };
6866
6846
  class LongTitle extends Rule {
6867
6847
  constructor(options) {
6868
- super({ ...defaults$f, ...options });
6848
+ super({ ...defaults$g, ...options });
6869
6849
  this.maxlength = this.options.maxlength;
6870
6850
  }
6871
6851
  static schema() {
@@ -6894,7 +6874,13 @@ class LongTitle extends Rule {
6894
6874
  }
6895
6875
  }
6896
6876
 
6877
+ const defaults$f = {
6878
+ allowLongDelay: false
6879
+ };
6897
6880
  class MetaRefresh extends Rule {
6881
+ constructor(options) {
6882
+ super({ ...defaults$f, ...options });
6883
+ }
6898
6884
  documentation() {
6899
6885
  return {
6900
6886
  description: `Meta refresh directive must use the \`0;url=...\` format. Non-zero values for time interval is disallowed as people with assistive technology might be unable to read and understand the page content before automatically reloading. For the same reason skipping the url is disallowed as it would put the browser in an infinite loop reloading the same page over and over again.`,
@@ -6920,17 +6906,27 @@ class MetaRefresh extends Rule {
6920
6906
  this.report(target, "Malformed meta refresh directive", location);
6921
6907
  return;
6922
6908
  }
6923
- if (!value.url) {
6924
- this.report(target, "Don't use meta refresh to reload the page", location);
6925
- }
6926
- if (value.delay !== 0) {
6927
- this.report(target, "Meta refresh must use 0 second delay", location);
6928
- }
6909
+ const { delay, url } = value;
6910
+ this.validateDelay(target, location, delay, url);
6929
6911
  });
6930
6912
  }
6913
+ validateDelay(target, location, delay, url) {
6914
+ const { allowLongDelay } = this.options;
6915
+ if (allowLongDelay && delay > 72e3) {
6916
+ return;
6917
+ }
6918
+ if (!url && delay === 0) {
6919
+ this.report(target, "Don't use instant meta refresh to reload the page", location);
6920
+ return;
6921
+ }
6922
+ if (delay !== 0) {
6923
+ const message = allowLongDelay ? "Meta refresh must be instant (0 second delay) or greater than 20 hours (72000 second delay)" : "Meta refresh must be instant (0 second delay)";
6924
+ this.report(target, message, location);
6925
+ }
6926
+ }
6931
6927
  }
6932
6928
  function parseContent(text) {
6933
- const match = text.match(/^(\d+)(?:\s*;\s*url=(.*))?/);
6929
+ const match = text.match(/^(\d+)(?:\s*;\s*url=(.*))?/i);
6934
6930
  if (match) {
6935
6931
  return {
6936
6932
  delay: parseInt(match[1], 10),
@@ -9017,6 +9013,522 @@ class UnknownCharReference extends Rule {
9017
9013
  }
9018
9014
  }
9019
9015
 
9016
+ const expectedOrder = ["section", "hint", "contact", "field1", "field2", "webauthn"];
9017
+ const fieldNames1 = [
9018
+ "name",
9019
+ "honorific-prefix",
9020
+ "given-name",
9021
+ "additional-name",
9022
+ "family-name",
9023
+ "honorific-suffix",
9024
+ "nickname",
9025
+ "username",
9026
+ "new-password",
9027
+ "current-password",
9028
+ "one-time-code",
9029
+ "organization-title",
9030
+ "organization",
9031
+ "street-address",
9032
+ "address-line1",
9033
+ "address-line2",
9034
+ "address-line3",
9035
+ "address-level4",
9036
+ "address-level3",
9037
+ "address-level2",
9038
+ "address-level1",
9039
+ "country",
9040
+ "country-name",
9041
+ "postal-code",
9042
+ "cc-name",
9043
+ "cc-given-name",
9044
+ "cc-additional-name",
9045
+ "cc-family-name",
9046
+ "cc-number",
9047
+ "cc-exp",
9048
+ "cc-exp-month",
9049
+ "cc-exp-year",
9050
+ "cc-csc",
9051
+ "cc-type",
9052
+ "transaction-currency",
9053
+ "transaction-amount",
9054
+ "language",
9055
+ "bday",
9056
+ "bday-day",
9057
+ "bday-month",
9058
+ "bday-year",
9059
+ "sex",
9060
+ "url",
9061
+ "photo"
9062
+ ];
9063
+ const fieldNames2 = [
9064
+ "tel",
9065
+ "tel-country-code",
9066
+ "tel-national",
9067
+ "tel-area-code",
9068
+ "tel-local",
9069
+ "tel-local-prefix",
9070
+ "tel-local-suffix",
9071
+ "tel-extension",
9072
+ "email",
9073
+ "impp"
9074
+ ];
9075
+ const fieldNameGroup = {
9076
+ name: "text",
9077
+ "honorific-prefix": "text",
9078
+ "given-name": "text",
9079
+ "additional-name": "text",
9080
+ "family-name": "text",
9081
+ "honorific-suffix": "text",
9082
+ nickname: "text",
9083
+ username: "username",
9084
+ "new-password": "password",
9085
+ "current-password": "password",
9086
+ "one-time-code": "password",
9087
+ "organization-title": "text",
9088
+ organization: "text",
9089
+ "street-address": "multiline",
9090
+ "address-line1": "text",
9091
+ "address-line2": "text",
9092
+ "address-line3": "text",
9093
+ "address-level4": "text",
9094
+ "address-level3": "text",
9095
+ "address-level2": "text",
9096
+ "address-level1": "text",
9097
+ country: "text",
9098
+ "country-name": "text",
9099
+ "postal-code": "text",
9100
+ "cc-name": "text",
9101
+ "cc-given-name": "text",
9102
+ "cc-additional-name": "text",
9103
+ "cc-family-name": "text",
9104
+ "cc-number": "text",
9105
+ "cc-exp": "month",
9106
+ "cc-exp-month": "numeric",
9107
+ "cc-exp-year": "numeric",
9108
+ "cc-csc": "text",
9109
+ "cc-type": "text",
9110
+ "transaction-currency": "text",
9111
+ "transaction-amount": "numeric",
9112
+ language: "text",
9113
+ bday: "date",
9114
+ "bday-day": "numeric",
9115
+ "bday-month": "numeric",
9116
+ "bday-year": "numeric",
9117
+ sex: "text",
9118
+ url: "url",
9119
+ photo: "url",
9120
+ tel: "tel",
9121
+ "tel-country-code": "text",
9122
+ "tel-national": "text",
9123
+ "tel-area-code": "text",
9124
+ "tel-local": "text",
9125
+ "tel-local-prefix": "text",
9126
+ "tel-local-suffix": "text",
9127
+ "tel-extension": "text",
9128
+ email: "username",
9129
+ impp: "url"
9130
+ };
9131
+ const disallowedInputTypes = ["checkbox", "radio", "file", "submit", "image", "reset", "button"];
9132
+ function matchSection(token) {
9133
+ return token.startsWith("section-");
9134
+ }
9135
+ function matchHint(token) {
9136
+ return token === "shipping" || token === "billing";
9137
+ }
9138
+ function matchFieldNames1(token) {
9139
+ return fieldNames1.includes(token);
9140
+ }
9141
+ function matchContact(token) {
9142
+ const haystack = ["home", "work", "mobile", "fax", "pager"];
9143
+ return haystack.includes(token);
9144
+ }
9145
+ function matchFieldNames2(token) {
9146
+ return fieldNames2.includes(token);
9147
+ }
9148
+ function matchWebauthn(token) {
9149
+ return token === "webauthn";
9150
+ }
9151
+ function matchToken(token) {
9152
+ if (matchSection(token)) {
9153
+ return "section";
9154
+ }
9155
+ if (matchHint(token)) {
9156
+ return "hint";
9157
+ }
9158
+ if (matchFieldNames1(token)) {
9159
+ return "field1";
9160
+ }
9161
+ if (matchFieldNames2(token)) {
9162
+ return "field2";
9163
+ }
9164
+ if (matchContact(token)) {
9165
+ return "contact";
9166
+ }
9167
+ if (matchWebauthn(token)) {
9168
+ return "webauthn";
9169
+ }
9170
+ return null;
9171
+ }
9172
+ function getControlGroups(type) {
9173
+ const allGroups = [
9174
+ "text",
9175
+ "multiline",
9176
+ "password",
9177
+ "url",
9178
+ "username",
9179
+ "tel",
9180
+ "numeric",
9181
+ "month",
9182
+ "date"
9183
+ ];
9184
+ const mapping = {
9185
+ hidden: allGroups,
9186
+ text: allGroups.filter((it) => it !== "multiline"),
9187
+ search: allGroups.filter((it) => it !== "multiline"),
9188
+ password: ["password"],
9189
+ url: ["url"],
9190
+ email: ["username"],
9191
+ tel: ["tel"],
9192
+ number: ["numeric"],
9193
+ month: ["month"],
9194
+ date: ["date"]
9195
+ };
9196
+ const groups = mapping[type];
9197
+ if (groups) {
9198
+ return groups;
9199
+ }
9200
+ return [];
9201
+ }
9202
+ function isDisallowedType(node, type) {
9203
+ if (!node.is("input")) {
9204
+ return false;
9205
+ }
9206
+ return disallowedInputTypes.includes(type);
9207
+ }
9208
+ function getTerminalMessage(context) {
9209
+ switch (context.msg) {
9210
+ case 0 /* InvalidAttribute */:
9211
+ return "autocomplete attribute cannot be used on {{ what }}";
9212
+ case 1 /* InvalidValue */:
9213
+ return '"{{ value }}" cannot be used on {{ what }}';
9214
+ case 2 /* InvalidOrder */:
9215
+ return '"{{ second }}" must appear before "{{ first }}"';
9216
+ case 3 /* InvalidToken */:
9217
+ return '"{{ token }}" is not a valid autocomplete token or field name';
9218
+ case 4 /* InvalidCombination */:
9219
+ return '"{{ second }}" cannot be combined with "{{ first }}"';
9220
+ case 5 /* MissingField */:
9221
+ return "autocomplete attribute is missing field name";
9222
+ }
9223
+ }
9224
+ function getMarkdownMessage(context) {
9225
+ switch (context.msg) {
9226
+ case 0 /* InvalidAttribute */:
9227
+ return [
9228
+ `\`autocomplete\` attribute cannot be used on \`${context.what}\``,
9229
+ "",
9230
+ "The following input types cannot use the `autocomplete` attribute:",
9231
+ "",
9232
+ ...disallowedInputTypes.map((it) => `- \`${it}\``)
9233
+ ].join("\n");
9234
+ case 1 /* InvalidValue */: {
9235
+ const message = `\`"${context.value}"\` cannot be used on \`${context.what}\``;
9236
+ if (context.type === "form") {
9237
+ return [
9238
+ message,
9239
+ "",
9240
+ 'The `<form>` element can only use the values `"on"` and `"off"`.'
9241
+ ].join("\n");
9242
+ }
9243
+ if (context.type === "hidden") {
9244
+ return [
9245
+ message,
9246
+ "",
9247
+ '`<input type="hidden">` cannot use the values `"on"` and `"off"`.'
9248
+ ].join("\n");
9249
+ }
9250
+ const controlGroups = getControlGroups(context.type);
9251
+ const currentGroup = fieldNameGroup[context.value];
9252
+ return [
9253
+ message,
9254
+ "",
9255
+ `\`${context.what}\` allows autocomplete fields from the following group${controlGroups.length > 1 ? "s" : ""}:`,
9256
+ "",
9257
+ ...controlGroups.map((it) => `- ${it}`),
9258
+ "",
9259
+ `The field \`"${context.value}"\` belongs to the group /${currentGroup}/ which cannot be used with this input type.`
9260
+ ].join("\n");
9261
+ }
9262
+ case 2 /* InvalidOrder */:
9263
+ return [
9264
+ `\`"${context.second}"\` must appear before \`"${context.first}"\``,
9265
+ "",
9266
+ "The autocomplete tokens must appear in the following order:",
9267
+ "",
9268
+ "- Optional section name (`section-` prefix).",
9269
+ "- Optional `shipping` or `billing` token.",
9270
+ "- Optional `home`, `work`, `mobile`, `fax` or `pager` token (for fields supporting it).",
9271
+ "- Field name",
9272
+ "- Optional `webauthn` token."
9273
+ ].join("\n");
9274
+ case 3 /* InvalidToken */:
9275
+ return `\`"${context.token}"\` is not a valid autocomplete token or field name`;
9276
+ case 4 /* InvalidCombination */:
9277
+ return `\`"${context.second}"\` cannot be combined with \`"${context.first}"\``;
9278
+ case 5 /* MissingField */:
9279
+ return "Autocomplete attribute is missing field name";
9280
+ }
9281
+ }
9282
+ class ValidAutocomplete extends Rule {
9283
+ documentation(context) {
9284
+ return {
9285
+ description: getMarkdownMessage(context),
9286
+ url: "https://html-validate.org/rules/valid-autocomplete.html"
9287
+ };
9288
+ }
9289
+ setup() {
9290
+ this.on("dom:ready", (event) => {
9291
+ const { document } = event;
9292
+ const elements = document.querySelectorAll("[autocomplete]");
9293
+ for (const element of elements) {
9294
+ const autocomplete = element.getAttribute("autocomplete");
9295
+ if (autocomplete.value === null || autocomplete.value instanceof DynamicValue) {
9296
+ continue;
9297
+ }
9298
+ const location = autocomplete.valueLocation;
9299
+ const value = autocomplete.value.toLowerCase();
9300
+ const tokens = new DOMTokenList(value, location);
9301
+ if (tokens.length === 0) {
9302
+ continue;
9303
+ }
9304
+ this.validate(element, value, tokens, autocomplete.keyLocation, location);
9305
+ }
9306
+ });
9307
+ }
9308
+ validate(node, value, tokens, keyLocation, valueLocation) {
9309
+ switch (node.tagName) {
9310
+ case "form":
9311
+ this.validateFormAutocomplete(node, value, valueLocation);
9312
+ break;
9313
+ case "input":
9314
+ case "textarea":
9315
+ case "select":
9316
+ this.validateControlAutocomplete(node, tokens, keyLocation);
9317
+ break;
9318
+ }
9319
+ }
9320
+ validateControlAutocomplete(node, tokens, keyLocation) {
9321
+ const type = node.getAttributeValue("type") ?? "text";
9322
+ const mantle = type !== "hidden" ? "expectation" : "anchor";
9323
+ if (isDisallowedType(node, type)) {
9324
+ const context = {
9325
+ msg: 0 /* InvalidAttribute */,
9326
+ what: `<input type="${type}">`
9327
+ };
9328
+ this.report({
9329
+ node,
9330
+ message: getTerminalMessage(context),
9331
+ location: keyLocation,
9332
+ context
9333
+ });
9334
+ return;
9335
+ }
9336
+ if (tokens.includes("on") || tokens.includes("off")) {
9337
+ this.validateOnOff(node, mantle, tokens);
9338
+ return;
9339
+ }
9340
+ this.validateTokens(node, tokens, keyLocation);
9341
+ }
9342
+ validateFormAutocomplete(node, value, location) {
9343
+ const trimmed = value.trim();
9344
+ if (["on", "off"].includes(trimmed)) {
9345
+ return;
9346
+ }
9347
+ const context = {
9348
+ msg: 1 /* InvalidValue */,
9349
+ type: "form",
9350
+ value: trimmed,
9351
+ what: "<form>"
9352
+ };
9353
+ this.report({
9354
+ node,
9355
+ message: getTerminalMessage(context),
9356
+ location,
9357
+ context
9358
+ });
9359
+ }
9360
+ validateOnOff(node, mantle, tokens) {
9361
+ const index = tokens.findIndex((it) => it === "on" || it === "off");
9362
+ const value = tokens.item(index);
9363
+ const location = tokens.location(index);
9364
+ if (tokens.length > 1) {
9365
+ const context = {
9366
+ msg: 4 /* InvalidCombination */,
9367
+ /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
9368
+ first: tokens.item(index > 0 ? 0 : 1),
9369
+ second: value
9370
+ };
9371
+ this.report({
9372
+ node,
9373
+ message: getTerminalMessage(context),
9374
+ location,
9375
+ context
9376
+ });
9377
+ }
9378
+ switch (mantle) {
9379
+ case "expectation":
9380
+ return;
9381
+ case "anchor": {
9382
+ const context = {
9383
+ msg: 1 /* InvalidValue */,
9384
+ type: "hidden",
9385
+ value,
9386
+ what: `<input type="hidden">`
9387
+ };
9388
+ this.report({
9389
+ node,
9390
+ message: getTerminalMessage(context),
9391
+ location: tokens.location(0),
9392
+ context
9393
+ });
9394
+ }
9395
+ }
9396
+ }
9397
+ validateTokens(node, tokens, keyLocation) {
9398
+ const order = [];
9399
+ for (const { item, location } of tokens.iterator()) {
9400
+ const tokenType = matchToken(item);
9401
+ if (tokenType) {
9402
+ order.push(tokenType);
9403
+ } else {
9404
+ const context = {
9405
+ msg: 3 /* InvalidToken */,
9406
+ token: item
9407
+ };
9408
+ this.report({
9409
+ node,
9410
+ message: getTerminalMessage(context),
9411
+ location,
9412
+ context
9413
+ });
9414
+ return;
9415
+ }
9416
+ }
9417
+ const fieldTokens = order.map((it) => it === "field1" || it === "field2");
9418
+ this.validateFieldPresence(node, tokens, fieldTokens, keyLocation);
9419
+ this.validateContact(node, tokens, order);
9420
+ this.validateOrder(node, tokens, order);
9421
+ this.validateControlGroup(node, tokens, fieldTokens);
9422
+ }
9423
+ /**
9424
+ * Ensure that exactly one field name is present from the two field lists.
9425
+ */
9426
+ validateFieldPresence(node, tokens, fieldTokens, keyLocation) {
9427
+ const numFields = fieldTokens.filter(Boolean).length;
9428
+ if (numFields === 0) {
9429
+ const context = {
9430
+ msg: 5 /* MissingField */
9431
+ };
9432
+ this.report({
9433
+ node,
9434
+ message: getTerminalMessage(context),
9435
+ location: keyLocation,
9436
+ context
9437
+ });
9438
+ } else if (numFields > 1) {
9439
+ const a = fieldTokens.indexOf(true);
9440
+ const b = fieldTokens.lastIndexOf(true);
9441
+ const context = {
9442
+ msg: 4 /* InvalidCombination */,
9443
+ /* eslint-disable @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
9444
+ first: tokens.item(a),
9445
+ second: tokens.item(b)
9446
+ /* eslint-enable @typescript-eslint/no-non-null-assertion */
9447
+ };
9448
+ this.report({
9449
+ node,
9450
+ message: getTerminalMessage(context),
9451
+ location: tokens.location(b),
9452
+ context
9453
+ });
9454
+ }
9455
+ }
9456
+ /**
9457
+ * Ensure contact token is only used with field names from the second list.
9458
+ */
9459
+ validateContact(node, tokens, order) {
9460
+ if (order.includes("contact") && order.includes("field1")) {
9461
+ const a = order.indexOf("field1");
9462
+ const b = order.indexOf("contact");
9463
+ const context = {
9464
+ msg: 4 /* InvalidCombination */,
9465
+ /* eslint-disable @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
9466
+ first: tokens.item(a),
9467
+ second: tokens.item(b)
9468
+ /* eslint-enable @typescript-eslint/no-non-null-assertion */
9469
+ };
9470
+ this.report({
9471
+ node,
9472
+ message: getTerminalMessage(context),
9473
+ location: tokens.location(b),
9474
+ context
9475
+ });
9476
+ }
9477
+ }
9478
+ validateOrder(node, tokens, order) {
9479
+ const indicies = order.map((it) => expectedOrder.indexOf(it));
9480
+ for (let i = 0; i < indicies.length - 1; i++) {
9481
+ if (indicies[0] > indicies[i + 1]) {
9482
+ const context = {
9483
+ msg: 2 /* InvalidOrder */,
9484
+ /* eslint-disable @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
9485
+ first: tokens.item(i),
9486
+ second: tokens.item(i + 1)
9487
+ /* eslint-enable @typescript-eslint/no-non-null-assertion */
9488
+ };
9489
+ this.report({
9490
+ node,
9491
+ message: getTerminalMessage(context),
9492
+ location: tokens.location(i + 1),
9493
+ context
9494
+ });
9495
+ }
9496
+ }
9497
+ }
9498
+ validateControlGroup(node, tokens, fieldTokens) {
9499
+ const numFields = fieldTokens.filter(Boolean).length;
9500
+ if (numFields === 0) {
9501
+ return;
9502
+ }
9503
+ if (!node.is("input")) {
9504
+ return;
9505
+ }
9506
+ const attr = node.getAttribute("type");
9507
+ const type = (attr == null ? void 0 : attr.value) ?? "text";
9508
+ if (type instanceof DynamicValue) {
9509
+ return;
9510
+ }
9511
+ const controlGroups = getControlGroups(type);
9512
+ const fieldIndex = fieldTokens.indexOf(true);
9513
+ const fieldToken = tokens.item(fieldIndex);
9514
+ const fieldGroup = fieldNameGroup[fieldToken];
9515
+ if (!controlGroups.includes(fieldGroup)) {
9516
+ const context = {
9517
+ msg: 1 /* InvalidValue */,
9518
+ type,
9519
+ value: fieldToken,
9520
+ what: `<input type="${type}">`
9521
+ };
9522
+ this.report({
9523
+ node,
9524
+ message: getTerminalMessage(context),
9525
+ location: tokens.location(fieldIndex),
9526
+ context
9527
+ });
9528
+ }
9529
+ }
9530
+ }
9531
+
9020
9532
  const defaults$3 = {
9021
9533
  relaxed: false
9022
9534
  };
@@ -9571,6 +10083,7 @@ const bundledRules = {
9571
10083
  "text-content": TextContent,
9572
10084
  "unique-landmark": UniqueLandmark,
9573
10085
  "unrecognized-char-ref": UnknownCharReference,
10086
+ "valid-autocomplete": ValidAutocomplete,
9574
10087
  "valid-id": ValidID,
9575
10088
  "void-content": VoidContent,
9576
10089
  "void-style": VoidStyle,
@@ -9601,6 +10114,7 @@ const config$4 = {
9601
10114
  "svg-focusable": "off",
9602
10115
  "text-content": "error",
9603
10116
  "unique-landmark": "error",
10117
+ "valid-autocomplete": "error",
9604
10118
  "wcag/h30": "error",
9605
10119
  "wcag/h32": "error",
9606
10120
  "wcag/h36": "error",
@@ -9700,6 +10214,7 @@ const config$1 = {
9700
10214
  "text-content": "error",
9701
10215
  "unique-landmark": "error",
9702
10216
  "unrecognized-char-ref": "error",
10217
+ "valid-autocomplete": "error",
9703
10218
  "valid-id": ["error", { relaxed: false }],
9704
10219
  void: "off",
9705
10220
  "void-content": "error",
@@ -9747,6 +10262,7 @@ const config = {
9747
10262
  "no-unused-disable": "error",
9748
10263
  "script-element": "error",
9749
10264
  "unrecognized-char-ref": "error",
10265
+ "valid-autocomplete": "error",
9750
10266
  "valid-id": ["error", { relaxed: true }],
9751
10267
  "void-content": "error"
9752
10268
  }
@@ -11972,7 +12488,7 @@ class HtmlValidate {
11972
12488
  }
11973
12489
 
11974
12490
  const name = "html-validate";
11975
- const version = "8.13.0";
12491
+ const version = "8.15.0";
11976
12492
  const bugs = "https://gitlab.com/html-validate/html-validate/issues/new";
11977
12493
 
11978
12494
  function definePlugin(plugin) {