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/cjs/core.js CHANGED
@@ -6556,26 +6556,6 @@ class IdPattern extends Rule {
6556
6556
  const restricted = /* @__PURE__ */ new Map([
6557
6557
  ["accept", ["file"]],
6558
6558
  ["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
6559
  ["capture", ["file"]],
6580
6560
  ["checked", ["checkbox", "radio"]],
6581
6561
  ["dirname", ["text", "search"]],
@@ -6956,7 +6936,7 @@ class MetaRefresh extends Rule {
6956
6936
  }
6957
6937
  }
6958
6938
  function parseContent(text) {
6959
- const match = text.match(/^(\d+)(?:\s*;\s*url=(.*))?/);
6939
+ const match = text.match(/^(\d+)(?:\s*;\s*url=(.*))?/i);
6960
6940
  if (match) {
6961
6941
  return {
6962
6942
  delay: parseInt(match[1], 10),
@@ -9043,6 +9023,522 @@ class UnknownCharReference extends Rule {
9043
9023
  }
9044
9024
  }
9045
9025
 
9026
+ const expectedOrder = ["section", "hint", "contact", "field1", "field2", "webauthn"];
9027
+ const fieldNames1 = [
9028
+ "name",
9029
+ "honorific-prefix",
9030
+ "given-name",
9031
+ "additional-name",
9032
+ "family-name",
9033
+ "honorific-suffix",
9034
+ "nickname",
9035
+ "username",
9036
+ "new-password",
9037
+ "current-password",
9038
+ "one-time-code",
9039
+ "organization-title",
9040
+ "organization",
9041
+ "street-address",
9042
+ "address-line1",
9043
+ "address-line2",
9044
+ "address-line3",
9045
+ "address-level4",
9046
+ "address-level3",
9047
+ "address-level2",
9048
+ "address-level1",
9049
+ "country",
9050
+ "country-name",
9051
+ "postal-code",
9052
+ "cc-name",
9053
+ "cc-given-name",
9054
+ "cc-additional-name",
9055
+ "cc-family-name",
9056
+ "cc-number",
9057
+ "cc-exp",
9058
+ "cc-exp-month",
9059
+ "cc-exp-year",
9060
+ "cc-csc",
9061
+ "cc-type",
9062
+ "transaction-currency",
9063
+ "transaction-amount",
9064
+ "language",
9065
+ "bday",
9066
+ "bday-day",
9067
+ "bday-month",
9068
+ "bday-year",
9069
+ "sex",
9070
+ "url",
9071
+ "photo"
9072
+ ];
9073
+ const fieldNames2 = [
9074
+ "tel",
9075
+ "tel-country-code",
9076
+ "tel-national",
9077
+ "tel-area-code",
9078
+ "tel-local",
9079
+ "tel-local-prefix",
9080
+ "tel-local-suffix",
9081
+ "tel-extension",
9082
+ "email",
9083
+ "impp"
9084
+ ];
9085
+ const fieldNameGroup = {
9086
+ name: "text",
9087
+ "honorific-prefix": "text",
9088
+ "given-name": "text",
9089
+ "additional-name": "text",
9090
+ "family-name": "text",
9091
+ "honorific-suffix": "text",
9092
+ nickname: "text",
9093
+ username: "username",
9094
+ "new-password": "password",
9095
+ "current-password": "password",
9096
+ "one-time-code": "password",
9097
+ "organization-title": "text",
9098
+ organization: "text",
9099
+ "street-address": "multiline",
9100
+ "address-line1": "text",
9101
+ "address-line2": "text",
9102
+ "address-line3": "text",
9103
+ "address-level4": "text",
9104
+ "address-level3": "text",
9105
+ "address-level2": "text",
9106
+ "address-level1": "text",
9107
+ country: "text",
9108
+ "country-name": "text",
9109
+ "postal-code": "text",
9110
+ "cc-name": "text",
9111
+ "cc-given-name": "text",
9112
+ "cc-additional-name": "text",
9113
+ "cc-family-name": "text",
9114
+ "cc-number": "text",
9115
+ "cc-exp": "month",
9116
+ "cc-exp-month": "numeric",
9117
+ "cc-exp-year": "numeric",
9118
+ "cc-csc": "text",
9119
+ "cc-type": "text",
9120
+ "transaction-currency": "text",
9121
+ "transaction-amount": "numeric",
9122
+ language: "text",
9123
+ bday: "date",
9124
+ "bday-day": "numeric",
9125
+ "bday-month": "numeric",
9126
+ "bday-year": "numeric",
9127
+ sex: "text",
9128
+ url: "url",
9129
+ photo: "url",
9130
+ tel: "tel",
9131
+ "tel-country-code": "text",
9132
+ "tel-national": "text",
9133
+ "tel-area-code": "text",
9134
+ "tel-local": "text",
9135
+ "tel-local-prefix": "text",
9136
+ "tel-local-suffix": "text",
9137
+ "tel-extension": "text",
9138
+ email: "username",
9139
+ impp: "url"
9140
+ };
9141
+ const disallowedInputTypes = ["checkbox", "radio", "file", "submit", "image", "reset", "button"];
9142
+ function matchSection(token) {
9143
+ return token.startsWith("section-");
9144
+ }
9145
+ function matchHint(token) {
9146
+ return token === "shipping" || token === "billing";
9147
+ }
9148
+ function matchFieldNames1(token) {
9149
+ return fieldNames1.includes(token);
9150
+ }
9151
+ function matchContact(token) {
9152
+ const haystack = ["home", "work", "mobile", "fax", "pager"];
9153
+ return haystack.includes(token);
9154
+ }
9155
+ function matchFieldNames2(token) {
9156
+ return fieldNames2.includes(token);
9157
+ }
9158
+ function matchWebauthn(token) {
9159
+ return token === "webauthn";
9160
+ }
9161
+ function matchToken(token) {
9162
+ if (matchSection(token)) {
9163
+ return "section";
9164
+ }
9165
+ if (matchHint(token)) {
9166
+ return "hint";
9167
+ }
9168
+ if (matchFieldNames1(token)) {
9169
+ return "field1";
9170
+ }
9171
+ if (matchFieldNames2(token)) {
9172
+ return "field2";
9173
+ }
9174
+ if (matchContact(token)) {
9175
+ return "contact";
9176
+ }
9177
+ if (matchWebauthn(token)) {
9178
+ return "webauthn";
9179
+ }
9180
+ return null;
9181
+ }
9182
+ function getControlGroups(type) {
9183
+ const allGroups = [
9184
+ "text",
9185
+ "multiline",
9186
+ "password",
9187
+ "url",
9188
+ "username",
9189
+ "tel",
9190
+ "numeric",
9191
+ "month",
9192
+ "date"
9193
+ ];
9194
+ const mapping = {
9195
+ hidden: allGroups,
9196
+ text: allGroups.filter((it) => it !== "multiline"),
9197
+ search: allGroups.filter((it) => it !== "multiline"),
9198
+ password: ["password"],
9199
+ url: ["url"],
9200
+ email: ["username"],
9201
+ tel: ["tel"],
9202
+ number: ["numeric"],
9203
+ month: ["month"],
9204
+ date: ["date"]
9205
+ };
9206
+ const groups = mapping[type];
9207
+ if (groups) {
9208
+ return groups;
9209
+ }
9210
+ return [];
9211
+ }
9212
+ function isDisallowedType(node, type) {
9213
+ if (!node.is("input")) {
9214
+ return false;
9215
+ }
9216
+ return disallowedInputTypes.includes(type);
9217
+ }
9218
+ function getTerminalMessage(context) {
9219
+ switch (context.msg) {
9220
+ case 0 /* InvalidAttribute */:
9221
+ return "autocomplete attribute cannot be used on {{ what }}";
9222
+ case 1 /* InvalidValue */:
9223
+ return '"{{ value }}" cannot be used on {{ what }}';
9224
+ case 2 /* InvalidOrder */:
9225
+ return '"{{ second }}" must appear before "{{ first }}"';
9226
+ case 3 /* InvalidToken */:
9227
+ return '"{{ token }}" is not a valid autocomplete token or field name';
9228
+ case 4 /* InvalidCombination */:
9229
+ return '"{{ second }}" cannot be combined with "{{ first }}"';
9230
+ case 5 /* MissingField */:
9231
+ return "autocomplete attribute is missing field name";
9232
+ }
9233
+ }
9234
+ function getMarkdownMessage(context) {
9235
+ switch (context.msg) {
9236
+ case 0 /* InvalidAttribute */:
9237
+ return [
9238
+ `\`autocomplete\` attribute cannot be used on \`${context.what}\``,
9239
+ "",
9240
+ "The following input types cannot use the `autocomplete` attribute:",
9241
+ "",
9242
+ ...disallowedInputTypes.map((it) => `- \`${it}\``)
9243
+ ].join("\n");
9244
+ case 1 /* InvalidValue */: {
9245
+ const message = `\`"${context.value}"\` cannot be used on \`${context.what}\``;
9246
+ if (context.type === "form") {
9247
+ return [
9248
+ message,
9249
+ "",
9250
+ 'The `<form>` element can only use the values `"on"` and `"off"`.'
9251
+ ].join("\n");
9252
+ }
9253
+ if (context.type === "hidden") {
9254
+ return [
9255
+ message,
9256
+ "",
9257
+ '`<input type="hidden">` cannot use the values `"on"` and `"off"`.'
9258
+ ].join("\n");
9259
+ }
9260
+ const controlGroups = getControlGroups(context.type);
9261
+ const currentGroup = fieldNameGroup[context.value];
9262
+ return [
9263
+ message,
9264
+ "",
9265
+ `\`${context.what}\` allows autocomplete fields from the following group${controlGroups.length > 1 ? "s" : ""}:`,
9266
+ "",
9267
+ ...controlGroups.map((it) => `- ${it}`),
9268
+ "",
9269
+ `The field \`"${context.value}"\` belongs to the group /${currentGroup}/ which cannot be used with this input type.`
9270
+ ].join("\n");
9271
+ }
9272
+ case 2 /* InvalidOrder */:
9273
+ return [
9274
+ `\`"${context.second}"\` must appear before \`"${context.first}"\``,
9275
+ "",
9276
+ "The autocomplete tokens must appear in the following order:",
9277
+ "",
9278
+ "- Optional section name (`section-` prefix).",
9279
+ "- Optional `shipping` or `billing` token.",
9280
+ "- Optional `home`, `work`, `mobile`, `fax` or `pager` token (for fields supporting it).",
9281
+ "- Field name",
9282
+ "- Optional `webauthn` token."
9283
+ ].join("\n");
9284
+ case 3 /* InvalidToken */:
9285
+ return `\`"${context.token}"\` is not a valid autocomplete token or field name`;
9286
+ case 4 /* InvalidCombination */:
9287
+ return `\`"${context.second}"\` cannot be combined with \`"${context.first}"\``;
9288
+ case 5 /* MissingField */:
9289
+ return "Autocomplete attribute is missing field name";
9290
+ }
9291
+ }
9292
+ class ValidAutocomplete extends Rule {
9293
+ documentation(context) {
9294
+ return {
9295
+ description: getMarkdownMessage(context),
9296
+ url: "https://html-validate.org/rules/valid-autocomplete.html"
9297
+ };
9298
+ }
9299
+ setup() {
9300
+ this.on("dom:ready", (event) => {
9301
+ const { document } = event;
9302
+ const elements = document.querySelectorAll("[autocomplete]");
9303
+ for (const element of elements) {
9304
+ const autocomplete = element.getAttribute("autocomplete");
9305
+ if (autocomplete.value === null || autocomplete.value instanceof DynamicValue) {
9306
+ continue;
9307
+ }
9308
+ const location = autocomplete.valueLocation;
9309
+ const value = autocomplete.value.toLowerCase();
9310
+ const tokens = new DOMTokenList(value, location);
9311
+ if (tokens.length === 0) {
9312
+ continue;
9313
+ }
9314
+ this.validate(element, value, tokens, autocomplete.keyLocation, location);
9315
+ }
9316
+ });
9317
+ }
9318
+ validate(node, value, tokens, keyLocation, valueLocation) {
9319
+ switch (node.tagName) {
9320
+ case "form":
9321
+ this.validateFormAutocomplete(node, value, valueLocation);
9322
+ break;
9323
+ case "input":
9324
+ case "textarea":
9325
+ case "select":
9326
+ this.validateControlAutocomplete(node, tokens, keyLocation);
9327
+ break;
9328
+ }
9329
+ }
9330
+ validateControlAutocomplete(node, tokens, keyLocation) {
9331
+ const type = node.getAttributeValue("type") ?? "text";
9332
+ const mantle = type !== "hidden" ? "expectation" : "anchor";
9333
+ if (isDisallowedType(node, type)) {
9334
+ const context = {
9335
+ msg: 0 /* InvalidAttribute */,
9336
+ what: `<input type="${type}">`
9337
+ };
9338
+ this.report({
9339
+ node,
9340
+ message: getTerminalMessage(context),
9341
+ location: keyLocation,
9342
+ context
9343
+ });
9344
+ return;
9345
+ }
9346
+ if (tokens.includes("on") || tokens.includes("off")) {
9347
+ this.validateOnOff(node, mantle, tokens);
9348
+ return;
9349
+ }
9350
+ this.validateTokens(node, tokens, keyLocation);
9351
+ }
9352
+ validateFormAutocomplete(node, value, location) {
9353
+ const trimmed = value.trim();
9354
+ if (["on", "off"].includes(trimmed)) {
9355
+ return;
9356
+ }
9357
+ const context = {
9358
+ msg: 1 /* InvalidValue */,
9359
+ type: "form",
9360
+ value: trimmed,
9361
+ what: "<form>"
9362
+ };
9363
+ this.report({
9364
+ node,
9365
+ message: getTerminalMessage(context),
9366
+ location,
9367
+ context
9368
+ });
9369
+ }
9370
+ validateOnOff(node, mantle, tokens) {
9371
+ const index = tokens.findIndex((it) => it === "on" || it === "off");
9372
+ const value = tokens.item(index);
9373
+ const location = tokens.location(index);
9374
+ if (tokens.length > 1) {
9375
+ const context = {
9376
+ msg: 4 /* InvalidCombination */,
9377
+ /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
9378
+ first: tokens.item(index > 0 ? 0 : 1),
9379
+ second: value
9380
+ };
9381
+ this.report({
9382
+ node,
9383
+ message: getTerminalMessage(context),
9384
+ location,
9385
+ context
9386
+ });
9387
+ }
9388
+ switch (mantle) {
9389
+ case "expectation":
9390
+ return;
9391
+ case "anchor": {
9392
+ const context = {
9393
+ msg: 1 /* InvalidValue */,
9394
+ type: "hidden",
9395
+ value,
9396
+ what: `<input type="hidden">`
9397
+ };
9398
+ this.report({
9399
+ node,
9400
+ message: getTerminalMessage(context),
9401
+ location: tokens.location(0),
9402
+ context
9403
+ });
9404
+ }
9405
+ }
9406
+ }
9407
+ validateTokens(node, tokens, keyLocation) {
9408
+ const order = [];
9409
+ for (const { item, location } of tokens.iterator()) {
9410
+ const tokenType = matchToken(item);
9411
+ if (tokenType) {
9412
+ order.push(tokenType);
9413
+ } else {
9414
+ const context = {
9415
+ msg: 3 /* InvalidToken */,
9416
+ token: item
9417
+ };
9418
+ this.report({
9419
+ node,
9420
+ message: getTerminalMessage(context),
9421
+ location,
9422
+ context
9423
+ });
9424
+ return;
9425
+ }
9426
+ }
9427
+ const fieldTokens = order.map((it) => it === "field1" || it === "field2");
9428
+ this.validateFieldPresence(node, tokens, fieldTokens, keyLocation);
9429
+ this.validateContact(node, tokens, order);
9430
+ this.validateOrder(node, tokens, order);
9431
+ this.validateControlGroup(node, tokens, fieldTokens);
9432
+ }
9433
+ /**
9434
+ * Ensure that exactly one field name is present from the two field lists.
9435
+ */
9436
+ validateFieldPresence(node, tokens, fieldTokens, keyLocation) {
9437
+ const numFields = fieldTokens.filter(Boolean).length;
9438
+ if (numFields === 0) {
9439
+ const context = {
9440
+ msg: 5 /* MissingField */
9441
+ };
9442
+ this.report({
9443
+ node,
9444
+ message: getTerminalMessage(context),
9445
+ location: keyLocation,
9446
+ context
9447
+ });
9448
+ } else if (numFields > 1) {
9449
+ const a = fieldTokens.indexOf(true);
9450
+ const b = fieldTokens.lastIndexOf(true);
9451
+ const context = {
9452
+ msg: 4 /* InvalidCombination */,
9453
+ /* eslint-disable @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
9454
+ first: tokens.item(a),
9455
+ second: tokens.item(b)
9456
+ /* eslint-enable @typescript-eslint/no-non-null-assertion */
9457
+ };
9458
+ this.report({
9459
+ node,
9460
+ message: getTerminalMessage(context),
9461
+ location: tokens.location(b),
9462
+ context
9463
+ });
9464
+ }
9465
+ }
9466
+ /**
9467
+ * Ensure contact token is only used with field names from the second list.
9468
+ */
9469
+ validateContact(node, tokens, order) {
9470
+ if (order.includes("contact") && order.includes("field1")) {
9471
+ const a = order.indexOf("field1");
9472
+ const b = order.indexOf("contact");
9473
+ const context = {
9474
+ msg: 4 /* InvalidCombination */,
9475
+ /* eslint-disable @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
9476
+ first: tokens.item(a),
9477
+ second: tokens.item(b)
9478
+ /* eslint-enable @typescript-eslint/no-non-null-assertion */
9479
+ };
9480
+ this.report({
9481
+ node,
9482
+ message: getTerminalMessage(context),
9483
+ location: tokens.location(b),
9484
+ context
9485
+ });
9486
+ }
9487
+ }
9488
+ validateOrder(node, tokens, order) {
9489
+ const indicies = order.map((it) => expectedOrder.indexOf(it));
9490
+ for (let i = 0; i < indicies.length - 1; i++) {
9491
+ if (indicies[0] > indicies[i + 1]) {
9492
+ const context = {
9493
+ msg: 2 /* InvalidOrder */,
9494
+ /* eslint-disable @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
9495
+ first: tokens.item(i),
9496
+ second: tokens.item(i + 1)
9497
+ /* eslint-enable @typescript-eslint/no-non-null-assertion */
9498
+ };
9499
+ this.report({
9500
+ node,
9501
+ message: getTerminalMessage(context),
9502
+ location: tokens.location(i + 1),
9503
+ context
9504
+ });
9505
+ }
9506
+ }
9507
+ }
9508
+ validateControlGroup(node, tokens, fieldTokens) {
9509
+ const numFields = fieldTokens.filter(Boolean).length;
9510
+ if (numFields === 0) {
9511
+ return;
9512
+ }
9513
+ if (!node.is("input")) {
9514
+ return;
9515
+ }
9516
+ const attr = node.getAttribute("type");
9517
+ const type = (attr == null ? void 0 : attr.value) ?? "text";
9518
+ if (type instanceof DynamicValue) {
9519
+ return;
9520
+ }
9521
+ const controlGroups = getControlGroups(type);
9522
+ const fieldIndex = fieldTokens.indexOf(true);
9523
+ const fieldToken = tokens.item(fieldIndex);
9524
+ const fieldGroup = fieldNameGroup[fieldToken];
9525
+ if (!controlGroups.includes(fieldGroup)) {
9526
+ const context = {
9527
+ msg: 1 /* InvalidValue */,
9528
+ type,
9529
+ value: fieldToken,
9530
+ what: `<input type="${type}">`
9531
+ };
9532
+ this.report({
9533
+ node,
9534
+ message: getTerminalMessage(context),
9535
+ location: tokens.location(fieldIndex),
9536
+ context
9537
+ });
9538
+ }
9539
+ }
9540
+ }
9541
+
9046
9542
  const defaults$3 = {
9047
9543
  relaxed: false
9048
9544
  };
@@ -9597,6 +10093,7 @@ const bundledRules = {
9597
10093
  "text-content": TextContent,
9598
10094
  "unique-landmark": UniqueLandmark,
9599
10095
  "unrecognized-char-ref": UnknownCharReference,
10096
+ "valid-autocomplete": ValidAutocomplete,
9600
10097
  "valid-id": ValidID,
9601
10098
  "void-content": VoidContent,
9602
10099
  "void-style": VoidStyle,
@@ -9627,6 +10124,7 @@ const config$4 = {
9627
10124
  "svg-focusable": "off",
9628
10125
  "text-content": "error",
9629
10126
  "unique-landmark": "error",
10127
+ "valid-autocomplete": "error",
9630
10128
  "wcag/h30": "error",
9631
10129
  "wcag/h32": "error",
9632
10130
  "wcag/h36": "error",
@@ -9726,6 +10224,7 @@ const config$1 = {
9726
10224
  "text-content": "error",
9727
10225
  "unique-landmark": "error",
9728
10226
  "unrecognized-char-ref": "error",
10227
+ "valid-autocomplete": "error",
9729
10228
  "valid-id": ["error", { relaxed: false }],
9730
10229
  void: "off",
9731
10230
  "void-content": "error",
@@ -9773,6 +10272,7 @@ const config = {
9773
10272
  "no-unused-disable": "error",
9774
10273
  "script-element": "error",
9775
10274
  "unrecognized-char-ref": "error",
10275
+ "valid-autocomplete": "error",
9776
10276
  "valid-id": ["error", { relaxed: true }],
9777
10277
  "void-content": "error"
9778
10278
  }
@@ -11998,7 +12498,7 @@ class HtmlValidate {
11998
12498
  }
11999
12499
 
12000
12500
  const name = "html-validate";
12001
- const version = "8.14.0";
12501
+ const version = "8.15.0";
12002
12502
  const bugs = "https://gitlab.com/html-validate/html-validate/issues/new";
12003
12503
 
12004
12504
  function definePlugin(plugin) {