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/cjs/core.js CHANGED
@@ -906,6 +906,13 @@ const definitions = {
906
906
  type: "boolean",
907
907
  title: "Set to true if this attribute can optionally omit its value"
908
908
  },
909
+ reference: {
910
+ type: "string",
911
+ "enum": [
912
+ "id"
913
+ ],
914
+ title: "Set when the attribute references another element."
915
+ },
909
916
  required: {
910
917
  title: "Set to true or a function to evaluate if this attribute is required",
911
918
  oneOf: [
@@ -1075,6 +1082,7 @@ const MetaCopyableProperty = [
1075
1082
  "labelable",
1076
1083
  "submitButton",
1077
1084
  "attributes",
1085
+ "patternAttributes",
1078
1086
  "aria",
1079
1087
  "permittedContent",
1080
1088
  "permittedDescendants",
@@ -1087,6 +1095,54 @@ function setMetaProperty(dst, key, value) {
1087
1095
  dst[key] = value;
1088
1096
  }
1089
1097
 
1098
+ const SYNTAX_CHARACTERS = /[$()*+.?[\\\]^{|}]/;
1099
+ const CONTROL_ESCAPES = /* @__PURE__ */ new Map([
1100
+ [" ", "t"],
1101
+ ["\n", "n"],
1102
+ ["\v", "v"],
1103
+ ["\f", "f"],
1104
+ ["\r", "r"]
1105
+ ]);
1106
+ const OTHER_PUNCTUATORS = /^[!"#%&',:;<=>@`~-]$/;
1107
+ const WHITE_SPACE = /^[\t\v\f\uFEFF\p{Zs}]$/u;
1108
+ const LINE_TERMINATOR = /^[\n\r\u2028\u2029]$/;
1109
+ const SURROGATE = /^[\uD800-\uDFFF]$/;
1110
+ function isDecimalDigitOrASCIILetter(ch) {
1111
+ return /^[\dA-Za-z]$/.test(ch);
1112
+ }
1113
+ function needEscape(ch) {
1114
+ return OTHER_PUNCTUATORS.test(ch) || WHITE_SPACE.test(ch) || LINE_TERMINATOR.test(ch) || SURROGATE.test(ch);
1115
+ }
1116
+ function unicodeEscape(ch) {
1117
+ return `\\u${ch.codePointAt(0).toString(16).padStart(4, "0")}`;
1118
+ }
1119
+ function encodeForRegExpEscape(ch) {
1120
+ if (SYNTAX_CHARACTERS.test(ch) || ch === "/") {
1121
+ return `\\${ch}`;
1122
+ }
1123
+ if (CONTROL_ESCAPES.has(ch)) {
1124
+ return `\\${CONTROL_ESCAPES.get(ch)}`;
1125
+ }
1126
+ if (needEscape(ch)) {
1127
+ if (/[\u0000-\u00FF]/.test(ch)) {
1128
+ return `\\x${ch.codePointAt(0).toString(16).padStart(2, "0")}`;
1129
+ }
1130
+ return ch.split("").map((c) => unicodeEscape(c)).join("");
1131
+ }
1132
+ return ch;
1133
+ }
1134
+ function regexpEscape(str) {
1135
+ let escaped = "";
1136
+ for (const c of str) {
1137
+ if (escaped === "" && isDecimalDigitOrASCIILetter(c)) {
1138
+ escaped += `\\x${c.codePointAt(0).toString(16).padStart(2, "0")}`;
1139
+ } else {
1140
+ escaped += encodeForRegExpEscape(c);
1141
+ }
1142
+ }
1143
+ return escaped;
1144
+ }
1145
+
1090
1146
  function isSet(value) {
1091
1147
  return value !== void 0;
1092
1148
  }
@@ -1124,18 +1180,35 @@ function migrateSingleAttribute(src, key) {
1124
1180
  return stripUndefined({ ...result, ...attr });
1125
1181
  }
1126
1182
  }
1183
+ function isPatternAttribute$1(key) {
1184
+ return key.includes("*");
1185
+ }
1186
+ function patternToRegex(pattern) {
1187
+ const escaped = pattern.split("*").map(regexpEscape).join(".+");
1188
+ return new RegExp(`^${escaped}$`, "i");
1189
+ }
1127
1190
  function migrateAttributes(src) {
1128
1191
  const keys = [
1129
1192
  ...Object.keys(src.attributes ?? {}),
1130
1193
  ...src.requiredAttributes ?? [],
1131
1194
  ...src.deprecatedAttributes ?? []
1132
- /* eslint-disable-next-line sonarjs/no-alphabetical-sort -- not really needed in this case, this is a-z anyway */
1133
- ].toSorted();
1195
+ ].filter((key) => !isPatternAttribute$1(key)).toSorted();
1134
1196
  const entries = keys.map((key) => {
1135
1197
  return [key, migrateSingleAttribute(src, key)];
1136
1198
  });
1137
1199
  return Object.fromEntries(entries);
1138
1200
  }
1201
+ function migratePatternAttributes(src) {
1202
+ const attrs = src.attributes ?? {};
1203
+ return Object.entries(attrs).filter(([key]) => isPatternAttribute$1(key)).map(([pattern]) => {
1204
+ const { delete: deleted, ...attr } = migrateSingleAttribute(src, pattern);
1205
+ const regexp = patternToRegex(pattern);
1206
+ if (deleted) {
1207
+ return { pattern, regexp, delete: true };
1208
+ }
1209
+ return { ...attr, pattern, regexp };
1210
+ });
1211
+ }
1139
1212
  function normalizeAriaImplicitRole(value) {
1140
1213
  if (!value) {
1141
1214
  return () => null;
@@ -1160,6 +1233,7 @@ function migrateElement(src) {
1160
1233
  ...src,
1161
1234
  formAssociated: void 0,
1162
1235
  attributes: migrateAttributes(src),
1236
+ patternAttributes: migratePatternAttributes(src),
1163
1237
  textContent: src.textContent,
1164
1238
  focusable: src.focusable ?? false,
1165
1239
  implicitRole,
@@ -1389,6 +1463,18 @@ class MetaTable {
1389
1463
  }
1390
1464
  }
1391
1465
  merged.attributes = mergedAttrs;
1466
+ const mergedPatterns = [
1467
+ ...b.patternAttributes,
1468
+ ...a.patternAttributes ?? []
1469
+ ];
1470
+ const seenPatterns = /* @__PURE__ */ new Set();
1471
+ merged.patternAttributes = mergedPatterns.filter((entry) => {
1472
+ if (seenPatterns.has(entry.pattern)) {
1473
+ return false;
1474
+ }
1475
+ seenPatterns.add(entry.pattern);
1476
+ return !entry.delete;
1477
+ });
1392
1478
  if (a.aria) {
1393
1479
  merged.aria = { ...a.aria, ...b.aria };
1394
1480
  }
@@ -3466,6 +3552,10 @@ function isKeywordIgnored(options, keyword, matcher = (list, it) => list.include
3466
3552
  return false;
3467
3553
  }
3468
3554
 
3555
+ function isPatternAttribute(attrName, dynamicAttributes) {
3556
+ return dynamicAttributes.some((entry) => entry.regexp.test(attrName));
3557
+ }
3558
+
3469
3559
  const ARIA_HIDDEN_CACHE = Symbol(isAriaHidden.name);
3470
3560
  const HTML_HIDDEN_CACHE = Symbol(isHTMLHidden.name);
3471
3561
  const INERT_CACHE = Symbol(isInert.name);
@@ -7468,10 +7558,58 @@ class IdPattern extends BasePatternRule {
7468
7558
 
7469
7559
  const restricted = /* @__PURE__ */ new Map([
7470
7560
  ["accept", ["file"]],
7561
+ ["alpha", ["color"]],
7471
7562
  ["alt", ["image"]],
7563
+ [
7564
+ "autocapitalize",
7565
+ [
7566
+ "button",
7567
+ "checkbox",
7568
+ "color",
7569
+ "date",
7570
+ "datetime-local",
7571
+ "file",
7572
+ "hidden",
7573
+ "image",
7574
+ "month",
7575
+ "number",
7576
+ "radio",
7577
+ "range",
7578
+ "reset",
7579
+ "search",
7580
+ "submit",
7581
+ "tel",
7582
+ "text",
7583
+ "time",
7584
+ "week"
7585
+ ]
7586
+ ],
7587
+ [
7588
+ "autocomplete",
7589
+ [
7590
+ "color",
7591
+ "date",
7592
+ "datetime-local",
7593
+ "email",
7594
+ "file",
7595
+ "hidden",
7596
+ "image",
7597
+ "month",
7598
+ "number",
7599
+ "password",
7600
+ "range",
7601
+ "search",
7602
+ "tel",
7603
+ "text",
7604
+ "time",
7605
+ "url",
7606
+ "week"
7607
+ ]
7608
+ ],
7472
7609
  ["capture", ["file"]],
7473
7610
  ["checked", ["checkbox", "radio"]],
7474
- ["dirname", ["text", "search"]],
7611
+ ["colorspace", ["color"]],
7612
+ ["dirname", ["hidden", "text", "search", "url", "tel", "email"]],
7475
7613
  ["height", ["image"]],
7476
7614
  [
7477
7615
  "list",
@@ -7498,6 +7636,8 @@ const restricted = /* @__PURE__ */ new Map([
7498
7636
  ["multiple", ["email", "file"]],
7499
7637
  ["pattern", ["text", "search", "url", "tel", "email", "password"]],
7500
7638
  ["placeholder", ["text", "search", "url", "tel", "email", "password", "number"]],
7639
+ ["popovertarget", ["button"]],
7640
+ ["popovertargetaction", ["button"]],
7501
7641
  [
7502
7642
  "readonly",
7503
7643
  [
@@ -7538,6 +7678,31 @@ const restricted = /* @__PURE__ */ new Map([
7538
7678
  ["size", ["text", "search", "url", "tel", "email", "password"]],
7539
7679
  ["src", ["image"]],
7540
7680
  ["step", ["date", "month", "week", "time", "datetime-local", "number", "range"]],
7681
+ [
7682
+ "value",
7683
+ [
7684
+ "button",
7685
+ "checkbox",
7686
+ "color",
7687
+ "date",
7688
+ "datetime-local",
7689
+ "email",
7690
+ "hidden",
7691
+ "month",
7692
+ "number",
7693
+ "password",
7694
+ "radio",
7695
+ "range",
7696
+ "reset",
7697
+ "search",
7698
+ "submit",
7699
+ "tel",
7700
+ "text",
7701
+ "time",
7702
+ "url",
7703
+ "week"
7704
+ ]
7705
+ ],
7541
7706
  ["width", ["image"]]
7542
7707
  ]);
7543
7708
  function isInput(event) {
@@ -8529,16 +8694,6 @@ class NoInlineStyle extends Rule {
8529
8694
  }
8530
8695
  }
8531
8696
 
8532
- const ARIA = [
8533
- { property: "aria-activedescendant", isList: false },
8534
- { property: "aria-controls", isList: true },
8535
- { property: "aria-describedby", isList: true },
8536
- { property: "aria-details", isList: false },
8537
- { property: "aria-errormessage", isList: false },
8538
- { property: "aria-flowto", isList: true },
8539
- { property: "aria-labelledby", isList: true },
8540
- { property: "aria-owns", isList: true }
8541
- ];
8542
8697
  function idMissing(document, id) {
8543
8698
  const nodes = document.querySelectorAll(generateIdSelector(id));
8544
8699
  return nodes.length === 0;
@@ -8553,20 +8708,18 @@ class NoMissingReferences extends Rule {
8553
8708
  setup() {
8554
8709
  this.on("dom:ready", (event) => {
8555
8710
  const document = event.document;
8556
- for (const node of document.querySelectorAll("label[for]")) {
8557
- const attr = node.getAttribute("for");
8558
- this.validateReference(document, node, attr, false);
8559
- }
8560
- for (const node of document.querySelectorAll("input[list]")) {
8561
- const attr = node.getAttribute("list");
8562
- this.validateReference(document, node, attr, false);
8563
- }
8564
- for (const { property, isList } of ARIA) {
8565
- for (const node of document.querySelectorAll(`[${property}]`)) {
8566
- const attr = node.getAttribute(property);
8567
- this.validateReference(document, node, attr, isList);
8711
+ walk.depthFirst(document, (node) => {
8712
+ const meta = node.meta;
8713
+ if (!meta?.attributes) {
8714
+ return;
8568
8715
  }
8569
- }
8716
+ for (const attr of node.attributes) {
8717
+ const attrMeta = meta.attributes[attr.key];
8718
+ if (attrMeta?.reference === "id") {
8719
+ this.validateReference(document, node, attr, attrMeta.list ?? false);
8720
+ }
8721
+ }
8722
+ });
8570
8723
  });
8571
8724
  }
8572
8725
  validateReference(document, node, attr, isList) {
@@ -8968,18 +9121,7 @@ class NoTrailingWhitespace extends Rule {
8968
9121
  }
8969
9122
  }
8970
9123
 
8971
- const skipPatterns = [
8972
- /^data-/i,
8973
- /^aria-/i,
8974
- /^on[a-z]/i,
8975
- /^xml(ns)?:/i,
8976
- /^:/,
8977
- /^@/,
8978
- /^ng-/i,
8979
- /^v-/i,
8980
- /^x-/i,
8981
- /^\[/
8982
- ];
9124
+ const skipPatterns = [/^:/, /^@/, /^ng-/i, /^v-/i, /^x-/i, /^\[/, /^#/];
8983
9125
  function isKnownDynamicAttr(attr) {
8984
9126
  return skipPatterns.some((pattern) => pattern.test(attr));
8985
9127
  }
@@ -9001,6 +9143,9 @@ class NoUnknownAttributes extends Rule {
9001
9143
  if (attr in meta.attributes) {
9002
9144
  return;
9003
9145
  }
9146
+ if (isPatternAttribute(attr, meta.patternAttributes)) {
9147
+ return;
9148
+ }
9004
9149
  if (isKnownDynamicAttr(attr)) {
9005
9150
  return;
9006
9151
  }
@@ -12639,7 +12784,7 @@ class EventHandler {
12639
12784
  }
12640
12785
 
12641
12786
  const name = "html-validate";
12642
- const version = "11.2.0";
12787
+ const version = "11.4.0";
12643
12788
  const bugs = "https://gitlab.com/html-validate/html-validate/issues/new";
12644
12789
 
12645
12790
  function freeze(src) {