html-validate 11.2.0 → 11.4.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/esm/core.js CHANGED
@@ -897,6 +897,13 @@ const definitions = {
897
897
  type: "boolean",
898
898
  title: "Set to true if this attribute can optionally omit its value"
899
899
  },
900
+ reference: {
901
+ type: "string",
902
+ "enum": [
903
+ "id"
904
+ ],
905
+ title: "Set when the attribute references another element."
906
+ },
900
907
  required: {
901
908
  title: "Set to true or a function to evaluate if this attribute is required",
902
909
  oneOf: [
@@ -1066,6 +1073,7 @@ const MetaCopyableProperty = [
1066
1073
  "labelable",
1067
1074
  "submitButton",
1068
1075
  "attributes",
1076
+ "patternAttributes",
1069
1077
  "aria",
1070
1078
  "permittedContent",
1071
1079
  "permittedDescendants",
@@ -1078,6 +1086,54 @@ function setMetaProperty(dst, key, value) {
1078
1086
  dst[key] = value;
1079
1087
  }
1080
1088
 
1089
+ const SYNTAX_CHARACTERS = /[$()*+.?[\\\]^{|}]/;
1090
+ const CONTROL_ESCAPES = /* @__PURE__ */ new Map([
1091
+ [" ", "t"],
1092
+ ["\n", "n"],
1093
+ ["\v", "v"],
1094
+ ["\f", "f"],
1095
+ ["\r", "r"]
1096
+ ]);
1097
+ const OTHER_PUNCTUATORS = /^[!"#%&',:;<=>@`~-]$/;
1098
+ const WHITE_SPACE = /^[\t\v\f\uFEFF\p{Zs}]$/u;
1099
+ const LINE_TERMINATOR = /^[\n\r\u2028\u2029]$/;
1100
+ const SURROGATE = /^[\uD800-\uDFFF]$/;
1101
+ function isDecimalDigitOrASCIILetter(ch) {
1102
+ return /^[\dA-Za-z]$/.test(ch);
1103
+ }
1104
+ function needEscape(ch) {
1105
+ return OTHER_PUNCTUATORS.test(ch) || WHITE_SPACE.test(ch) || LINE_TERMINATOR.test(ch) || SURROGATE.test(ch);
1106
+ }
1107
+ function unicodeEscape(ch) {
1108
+ return `\\u${ch.codePointAt(0).toString(16).padStart(4, "0")}`;
1109
+ }
1110
+ function encodeForRegExpEscape(ch) {
1111
+ if (SYNTAX_CHARACTERS.test(ch) || ch === "/") {
1112
+ return `\\${ch}`;
1113
+ }
1114
+ if (CONTROL_ESCAPES.has(ch)) {
1115
+ return `\\${CONTROL_ESCAPES.get(ch)}`;
1116
+ }
1117
+ if (needEscape(ch)) {
1118
+ if (/[\u0000-\u00FF]/.test(ch)) {
1119
+ return `\\x${ch.codePointAt(0).toString(16).padStart(2, "0")}`;
1120
+ }
1121
+ return ch.split("").map((c) => unicodeEscape(c)).join("");
1122
+ }
1123
+ return ch;
1124
+ }
1125
+ function regexpEscape(str) {
1126
+ let escaped = "";
1127
+ for (const c of str) {
1128
+ if (escaped === "" && isDecimalDigitOrASCIILetter(c)) {
1129
+ escaped += `\\x${c.codePointAt(0).toString(16).padStart(2, "0")}`;
1130
+ } else {
1131
+ escaped += encodeForRegExpEscape(c);
1132
+ }
1133
+ }
1134
+ return escaped;
1135
+ }
1136
+
1081
1137
  function isSet(value) {
1082
1138
  return value !== void 0;
1083
1139
  }
@@ -1115,18 +1171,35 @@ function migrateSingleAttribute(src, key) {
1115
1171
  return stripUndefined({ ...result, ...attr });
1116
1172
  }
1117
1173
  }
1174
+ function isPatternAttribute$1(key) {
1175
+ return key.includes("*");
1176
+ }
1177
+ function patternToRegex(pattern) {
1178
+ const escaped = pattern.split("*").map(regexpEscape).join(".+");
1179
+ return new RegExp(`^${escaped}$`, "i");
1180
+ }
1118
1181
  function migrateAttributes(src) {
1119
1182
  const keys = [
1120
1183
  ...Object.keys(src.attributes ?? {}),
1121
1184
  ...src.requiredAttributes ?? [],
1122
1185
  ...src.deprecatedAttributes ?? []
1123
- /* eslint-disable-next-line sonarjs/no-alphabetical-sort -- not really needed in this case, this is a-z anyway */
1124
- ].toSorted();
1186
+ ].filter((key) => !isPatternAttribute$1(key)).toSorted();
1125
1187
  const entries = keys.map((key) => {
1126
1188
  return [key, migrateSingleAttribute(src, key)];
1127
1189
  });
1128
1190
  return Object.fromEntries(entries);
1129
1191
  }
1192
+ function migratePatternAttributes(src) {
1193
+ const attrs = src.attributes ?? {};
1194
+ return Object.entries(attrs).filter(([key]) => isPatternAttribute$1(key)).map(([pattern]) => {
1195
+ const { delete: deleted, ...attr } = migrateSingleAttribute(src, pattern);
1196
+ const regexp = patternToRegex(pattern);
1197
+ if (deleted) {
1198
+ return { pattern, regexp, delete: true };
1199
+ }
1200
+ return { ...attr, pattern, regexp };
1201
+ });
1202
+ }
1130
1203
  function normalizeAriaImplicitRole(value) {
1131
1204
  if (!value) {
1132
1205
  return () => null;
@@ -1151,6 +1224,7 @@ function migrateElement(src) {
1151
1224
  ...src,
1152
1225
  formAssociated: void 0,
1153
1226
  attributes: migrateAttributes(src),
1227
+ patternAttributes: migratePatternAttributes(src),
1154
1228
  textContent: src.textContent,
1155
1229
  focusable: src.focusable ?? false,
1156
1230
  implicitRole,
@@ -1380,6 +1454,18 @@ class MetaTable {
1380
1454
  }
1381
1455
  }
1382
1456
  merged.attributes = mergedAttrs;
1457
+ const mergedPatterns = [
1458
+ ...b.patternAttributes,
1459
+ ...a.patternAttributes ?? []
1460
+ ];
1461
+ const seenPatterns = /* @__PURE__ */ new Set();
1462
+ merged.patternAttributes = mergedPatterns.filter((entry) => {
1463
+ if (seenPatterns.has(entry.pattern)) {
1464
+ return false;
1465
+ }
1466
+ seenPatterns.add(entry.pattern);
1467
+ return !entry.delete;
1468
+ });
1383
1469
  if (a.aria) {
1384
1470
  merged.aria = { ...a.aria, ...b.aria };
1385
1471
  }
@@ -3457,6 +3543,10 @@ function isKeywordIgnored(options, keyword, matcher = (list, it) => list.include
3457
3543
  return false;
3458
3544
  }
3459
3545
 
3546
+ function isPatternAttribute(attrName, dynamicAttributes) {
3547
+ return dynamicAttributes.some((entry) => entry.regexp.test(attrName));
3548
+ }
3549
+
3460
3550
  const ARIA_HIDDEN_CACHE = Symbol(isAriaHidden.name);
3461
3551
  const HTML_HIDDEN_CACHE = Symbol(isHTMLHidden.name);
3462
3552
  const INERT_CACHE = Symbol(isInert.name);
@@ -7459,10 +7549,58 @@ class IdPattern extends BasePatternRule {
7459
7549
 
7460
7550
  const restricted = /* @__PURE__ */ new Map([
7461
7551
  ["accept", ["file"]],
7552
+ ["alpha", ["color"]],
7462
7553
  ["alt", ["image"]],
7554
+ [
7555
+ "autocapitalize",
7556
+ [
7557
+ "button",
7558
+ "checkbox",
7559
+ "color",
7560
+ "date",
7561
+ "datetime-local",
7562
+ "file",
7563
+ "hidden",
7564
+ "image",
7565
+ "month",
7566
+ "number",
7567
+ "radio",
7568
+ "range",
7569
+ "reset",
7570
+ "search",
7571
+ "submit",
7572
+ "tel",
7573
+ "text",
7574
+ "time",
7575
+ "week"
7576
+ ]
7577
+ ],
7578
+ [
7579
+ "autocomplete",
7580
+ [
7581
+ "color",
7582
+ "date",
7583
+ "datetime-local",
7584
+ "email",
7585
+ "file",
7586
+ "hidden",
7587
+ "image",
7588
+ "month",
7589
+ "number",
7590
+ "password",
7591
+ "range",
7592
+ "search",
7593
+ "tel",
7594
+ "text",
7595
+ "time",
7596
+ "url",
7597
+ "week"
7598
+ ]
7599
+ ],
7463
7600
  ["capture", ["file"]],
7464
7601
  ["checked", ["checkbox", "radio"]],
7465
- ["dirname", ["text", "search"]],
7602
+ ["colorspace", ["color"]],
7603
+ ["dirname", ["hidden", "text", "search", "url", "tel", "email"]],
7466
7604
  ["height", ["image"]],
7467
7605
  [
7468
7606
  "list",
@@ -7489,6 +7627,8 @@ const restricted = /* @__PURE__ */ new Map([
7489
7627
  ["multiple", ["email", "file"]],
7490
7628
  ["pattern", ["text", "search", "url", "tel", "email", "password"]],
7491
7629
  ["placeholder", ["text", "search", "url", "tel", "email", "password", "number"]],
7630
+ ["popovertarget", ["button"]],
7631
+ ["popovertargetaction", ["button"]],
7492
7632
  [
7493
7633
  "readonly",
7494
7634
  [
@@ -7529,6 +7669,31 @@ const restricted = /* @__PURE__ */ new Map([
7529
7669
  ["size", ["text", "search", "url", "tel", "email", "password"]],
7530
7670
  ["src", ["image"]],
7531
7671
  ["step", ["date", "month", "week", "time", "datetime-local", "number", "range"]],
7672
+ [
7673
+ "value",
7674
+ [
7675
+ "button",
7676
+ "checkbox",
7677
+ "color",
7678
+ "date",
7679
+ "datetime-local",
7680
+ "email",
7681
+ "hidden",
7682
+ "month",
7683
+ "number",
7684
+ "password",
7685
+ "radio",
7686
+ "range",
7687
+ "reset",
7688
+ "search",
7689
+ "submit",
7690
+ "tel",
7691
+ "text",
7692
+ "time",
7693
+ "url",
7694
+ "week"
7695
+ ]
7696
+ ],
7532
7697
  ["width", ["image"]]
7533
7698
  ]);
7534
7699
  function isInput(event) {
@@ -8520,16 +8685,6 @@ class NoInlineStyle extends Rule {
8520
8685
  }
8521
8686
  }
8522
8687
 
8523
- const ARIA = [
8524
- { property: "aria-activedescendant", isList: false },
8525
- { property: "aria-controls", isList: true },
8526
- { property: "aria-describedby", isList: true },
8527
- { property: "aria-details", isList: false },
8528
- { property: "aria-errormessage", isList: false },
8529
- { property: "aria-flowto", isList: true },
8530
- { property: "aria-labelledby", isList: true },
8531
- { property: "aria-owns", isList: true }
8532
- ];
8533
8688
  function idMissing(document, id) {
8534
8689
  const nodes = document.querySelectorAll(generateIdSelector(id));
8535
8690
  return nodes.length === 0;
@@ -8544,20 +8699,18 @@ class NoMissingReferences extends Rule {
8544
8699
  setup() {
8545
8700
  this.on("dom:ready", (event) => {
8546
8701
  const document = event.document;
8547
- for (const node of document.querySelectorAll("label[for]")) {
8548
- const attr = node.getAttribute("for");
8549
- this.validateReference(document, node, attr, false);
8550
- }
8551
- for (const node of document.querySelectorAll("input[list]")) {
8552
- const attr = node.getAttribute("list");
8553
- this.validateReference(document, node, attr, false);
8554
- }
8555
- for (const { property, isList } of ARIA) {
8556
- for (const node of document.querySelectorAll(`[${property}]`)) {
8557
- const attr = node.getAttribute(property);
8558
- this.validateReference(document, node, attr, isList);
8702
+ walk.depthFirst(document, (node) => {
8703
+ const meta = node.meta;
8704
+ if (!meta?.attributes) {
8705
+ return;
8559
8706
  }
8560
- }
8707
+ for (const attr of node.attributes) {
8708
+ const attrMeta = meta.attributes[attr.key];
8709
+ if (attrMeta?.reference === "id") {
8710
+ this.validateReference(document, node, attr, attrMeta.list ?? false);
8711
+ }
8712
+ }
8713
+ });
8561
8714
  });
8562
8715
  }
8563
8716
  validateReference(document, node, attr, isList) {
@@ -8959,18 +9112,7 @@ class NoTrailingWhitespace extends Rule {
8959
9112
  }
8960
9113
  }
8961
9114
 
8962
- const skipPatterns = [
8963
- /^data-/i,
8964
- /^aria-/i,
8965
- /^on[a-z]/i,
8966
- /^xml(ns)?:/i,
8967
- /^:/,
8968
- /^@/,
8969
- /^ng-/i,
8970
- /^v-/i,
8971
- /^x-/i,
8972
- /^\[/
8973
- ];
9115
+ const skipPatterns = [/^:/, /^@/, /^ng-/i, /^v-/i, /^x-/i, /^\[/, /^#/];
8974
9116
  function isKnownDynamicAttr(attr) {
8975
9117
  return skipPatterns.some((pattern) => pattern.test(attr));
8976
9118
  }
@@ -8992,6 +9134,9 @@ class NoUnknownAttributes extends Rule {
8992
9134
  if (attr in meta.attributes) {
8993
9135
  return;
8994
9136
  }
9137
+ if (isPatternAttribute(attr, meta.patternAttributes)) {
9138
+ return;
9139
+ }
8995
9140
  if (isKnownDynamicAttr(attr)) {
8996
9141
  return;
8997
9142
  }
@@ -12630,7 +12775,7 @@ class EventHandler {
12630
12775
  }
12631
12776
 
12632
12777
  const name = "html-validate";
12633
- const version = "11.2.0";
12778
+ const version = "11.4.0";
12634
12779
  const bugs = "https://gitlab.com/html-validate/html-validate/issues/new";
12635
12780
 
12636
12781
  function freeze(src) {