html-validate 8.14.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
@@ -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"]],
@@ -6946,7 +6926,7 @@ class MetaRefresh extends Rule {
6946
6926
  }
6947
6927
  }
6948
6928
  function parseContent(text) {
6949
- const match = text.match(/^(\d+)(?:\s*;\s*url=(.*))?/);
6929
+ const match = text.match(/^(\d+)(?:\s*;\s*url=(.*))?/i);
6950
6930
  if (match) {
6951
6931
  return {
6952
6932
  delay: parseInt(match[1], 10),
@@ -9033,6 +9013,522 @@ class UnknownCharReference extends Rule {
9033
9013
  }
9034
9014
  }
9035
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
+
9036
9532
  const defaults$3 = {
9037
9533
  relaxed: false
9038
9534
  };
@@ -9587,6 +10083,7 @@ const bundledRules = {
9587
10083
  "text-content": TextContent,
9588
10084
  "unique-landmark": UniqueLandmark,
9589
10085
  "unrecognized-char-ref": UnknownCharReference,
10086
+ "valid-autocomplete": ValidAutocomplete,
9590
10087
  "valid-id": ValidID,
9591
10088
  "void-content": VoidContent,
9592
10089
  "void-style": VoidStyle,
@@ -9617,6 +10114,7 @@ const config$4 = {
9617
10114
  "svg-focusable": "off",
9618
10115
  "text-content": "error",
9619
10116
  "unique-landmark": "error",
10117
+ "valid-autocomplete": "error",
9620
10118
  "wcag/h30": "error",
9621
10119
  "wcag/h32": "error",
9622
10120
  "wcag/h36": "error",
@@ -9716,6 +10214,7 @@ const config$1 = {
9716
10214
  "text-content": "error",
9717
10215
  "unique-landmark": "error",
9718
10216
  "unrecognized-char-ref": "error",
10217
+ "valid-autocomplete": "error",
9719
10218
  "valid-id": ["error", { relaxed: false }],
9720
10219
  void: "off",
9721
10220
  "void-content": "error",
@@ -9763,6 +10262,7 @@ const config = {
9763
10262
  "no-unused-disable": "error",
9764
10263
  "script-element": "error",
9765
10264
  "unrecognized-char-ref": "error",
10265
+ "valid-autocomplete": "error",
9766
10266
  "valid-id": ["error", { relaxed: true }],
9767
10267
  "void-content": "error"
9768
10268
  }
@@ -11988,7 +12488,7 @@ class HtmlValidate {
11988
12488
  }
11989
12489
 
11990
12490
  const name = "html-validate";
11991
- const version = "8.14.0";
12491
+ const version = "8.15.0";
11992
12492
  const bugs = "https://gitlab.com/html-validate/html-validate/issues/new";
11993
12493
 
11994
12494
  function definePlugin(plugin) {