html-validate 8.20.1 → 8.22.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.
Files changed (44) hide show
  1. package/dist/cjs/browser.js +1 -1
  2. package/dist/cjs/cli.js.map +1 -1
  3. package/dist/cjs/core-nodejs.js.map +1 -1
  4. package/dist/cjs/core.js +1781 -1613
  5. package/dist/cjs/core.js.map +1 -1
  6. package/dist/cjs/elements.js +4 -6
  7. package/dist/cjs/elements.js.map +1 -1
  8. package/dist/cjs/html-validate.js +0 -1
  9. package/dist/cjs/html-validate.js.map +1 -1
  10. package/dist/cjs/index.js +1 -1
  11. package/dist/cjs/jest-diff.js.map +1 -1
  12. package/dist/cjs/jest.js +0 -1
  13. package/dist/cjs/jest.js.map +1 -1
  14. package/dist/cjs/matcher-utils.js.map +1 -1
  15. package/dist/cjs/matchers.js.map +1 -1
  16. package/dist/cjs/tsdoc-metadata.json +1 -1
  17. package/dist/cjs/vitest.js +0 -1
  18. package/dist/cjs/vitest.js.map +1 -1
  19. package/dist/es/browser.js +1 -2
  20. package/dist/es/browser.js.map +1 -1
  21. package/dist/es/cli.js.map +1 -1
  22. package/dist/es/core-browser.js +1 -1
  23. package/dist/es/core-nodejs.js +1 -1
  24. package/dist/es/core-nodejs.js.map +1 -1
  25. package/dist/es/core.js +1781 -1614
  26. package/dist/es/core.js.map +1 -1
  27. package/dist/es/elements.js +4 -6
  28. package/dist/es/elements.js.map +1 -1
  29. package/dist/es/html-validate.js +1 -2
  30. package/dist/es/html-validate.js.map +1 -1
  31. package/dist/es/index.js +1 -2
  32. package/dist/es/index.js.map +1 -1
  33. package/dist/es/jest-diff.js.map +1 -1
  34. package/dist/es/jest.js +0 -1
  35. package/dist/es/jest.js.map +1 -1
  36. package/dist/es/matcher-utils.js.map +1 -1
  37. package/dist/es/matchers-jestonly.js +1 -1
  38. package/dist/es/matchers.js.map +1 -1
  39. package/dist/es/vitest.js +0 -1
  40. package/dist/es/vitest.js.map +1 -1
  41. package/dist/tsdoc-metadata.json +1 -1
  42. package/dist/types/browser.d.ts +65 -18
  43. package/dist/types/index.d.ts +65 -18
  44. package/package.json +23 -27
package/dist/cjs/core.js CHANGED
@@ -5,7 +5,6 @@ var elements = require('./elements.js');
5
5
  var betterAjvErrors = require('@sidvind/better-ajv-errors');
6
6
  var utils_naturalJoin = require('./utils/natural-join.js');
7
7
  var fs = require('fs');
8
- var codeFrame = require('@babel/code-frame');
9
8
  var kleur = require('kleur');
10
9
  var stylish$1 = require('@html-validate/stylish');
11
10
  var semver = require('semver');
@@ -1279,14 +1278,12 @@ class MetaTable {
1279
1278
  * @returns A shallow copy of metadata.
1280
1279
  */
1281
1280
  getMetaFor(tagName) {
1282
- tagName = tagName.toLowerCase();
1283
- if (this.elements[tagName]) {
1284
- return { ...this.elements[tagName] };
1285
- }
1286
- if (this.elements["*"]) {
1287
- return { ...this.elements["*"] };
1281
+ const meta = this.elements[tagName.toLowerCase()] ?? this.elements["*"];
1282
+ if (meta) {
1283
+ return { ...meta };
1284
+ } else {
1285
+ return null;
1288
1286
  }
1289
- return null;
1290
1287
  }
1291
1288
  /**
1292
1289
  * Find all tags which has enabled given property.
@@ -1294,7 +1291,7 @@ class MetaTable {
1294
1291
  * @public
1295
1292
  */
1296
1293
  getTagsWithProperty(propName) {
1297
- return Object.entries(this.elements).filter(([, entry]) => entry[propName]).map(([tagName]) => tagName);
1294
+ return this.entries.filter(([, entry]) => entry[propName]).map(([tagName]) => tagName);
1298
1295
  }
1299
1296
  /**
1300
1297
  * Find tag matching tagName or inheriting from it.
@@ -1302,10 +1299,10 @@ class MetaTable {
1302
1299
  * @public
1303
1300
  */
1304
1301
  getTagsDerivedFrom(tagName) {
1305
- return Object.entries(this.elements).filter(([key, entry]) => key === tagName || entry.inherit === tagName).map(([tagName2]) => tagName2);
1302
+ return this.entries.filter(([key, entry]) => key === tagName || entry.inherit === tagName).map(([tagName2]) => tagName2);
1306
1303
  }
1307
1304
  addEntry(tagName, entry) {
1308
- let parent = this.elements[tagName] || {};
1305
+ let parent = this.elements[tagName];
1309
1306
  if (entry.inherit) {
1310
1307
  const name = entry.inherit;
1311
1308
  parent = this.elements[name];
@@ -1316,7 +1313,7 @@ class MetaTable {
1316
1313
  });
1317
1314
  }
1318
1315
  }
1319
- const expanded = this.mergeElement(parent, { ...entry, tagName });
1316
+ const expanded = this.mergeElement(parent ?? {}, { ...entry, tagName });
1320
1317
  expandRegex(expanded);
1321
1318
  this.elements[tagName] = expanded;
1322
1319
  }
@@ -1345,6 +1342,12 @@ class MetaTable {
1345
1342
  getJSONSchema() {
1346
1343
  return this.schema;
1347
1344
  }
1345
+ /**
1346
+ * @internal
1347
+ */
1348
+ get entries() {
1349
+ return Object.entries(this.elements);
1350
+ }
1348
1351
  /**
1349
1352
  * Finds the global element definition and merges each known element with the
1350
1353
  * global, e.g. to assign global attributes.
@@ -1356,7 +1359,7 @@ class MetaTable {
1356
1359
  delete this.elements["*"];
1357
1360
  delete global.tagName;
1358
1361
  delete global.void;
1359
- for (const [tagName, entry] of Object.entries(this.elements)) {
1362
+ for (const [tagName, entry] of this.entries) {
1360
1363
  this.elements[tagName] = this.mergeElement(global, entry);
1361
1364
  }
1362
1365
  }
@@ -1677,6 +1680,7 @@ class DOMNode {
1677
1680
  /**
1678
1681
  * Create a new DOMNode.
1679
1682
  *
1683
+ * @internal
1680
1684
  * @param nodeType - What node type to create.
1681
1685
  * @param nodeName - What node name to use. For `HtmlElement` this corresponds
1682
1686
  * to the tagName but other node types have specific predefined values.
@@ -2144,7 +2148,7 @@ class AttrMatcher extends Matcher {
2144
2148
  this.value = value;
2145
2149
  }
2146
2150
  match(node) {
2147
- const attr = node.getAttribute(this.key, true) || [];
2151
+ const attr = node.getAttribute(this.key, true);
2148
2152
  return attr.some((cur) => {
2149
2153
  switch (this.op) {
2150
2154
  case void 0:
@@ -2345,8 +2349,15 @@ function createAdapter(node) {
2345
2349
  };
2346
2350
  }
2347
2351
  class HtmlElement extends DOMNode {
2348
- constructor(tagName, parent, closed, meta, location) {
2349
- const nodeType = tagName ? NodeType.ELEMENT_NODE : NodeType.DOCUMENT_NODE;
2352
+ constructor(details) {
2353
+ const {
2354
+ nodeType,
2355
+ tagName,
2356
+ parent = null,
2357
+ closed = 1 /* EndTag */,
2358
+ meta = null,
2359
+ location
2360
+ } = details;
2350
2361
  super(nodeType, tagName, location);
2351
2362
  if (isInvalidTagName(tagName)) {
2352
2363
  throw new Error(`The tag name provided ("${tagName}") is not a valid name`);
@@ -2369,11 +2380,39 @@ class HtmlElement extends DOMNode {
2369
2380
  }
2370
2381
  }
2371
2382
  }
2383
+ /**
2384
+ * Manually create a new element. This is primary useful for test-cases. While
2385
+ * the API is public it is not meant for general consumption and is not
2386
+ * guaranteed to be stable across versions.
2387
+ *
2388
+ * Use at your own risk. Prefer to use [[Parser]] to parse a string of markup
2389
+ * instead.
2390
+ *
2391
+ * @public
2392
+ * @since 8.22.0
2393
+ * @param tagName - Element tagname.
2394
+ * @param location - Element location.
2395
+ * @param details - Additional element details.
2396
+ */
2397
+ static createElement(tagName, location, details = {}) {
2398
+ const { closed = 1 /* EndTag */, meta = null, parent = null } = details;
2399
+ return new HtmlElement({
2400
+ nodeType: NodeType.ELEMENT_NODE,
2401
+ tagName,
2402
+ parent,
2403
+ closed,
2404
+ meta,
2405
+ location
2406
+ });
2407
+ }
2372
2408
  /**
2373
2409
  * @internal
2374
2410
  */
2375
2411
  static rootNode(location) {
2376
- const root = new HtmlElement(void 0, null, 1 /* EndTag */, null, location);
2412
+ const root = new HtmlElement({
2413
+ nodeType: NodeType.DOCUMENT_NODE,
2414
+ location
2415
+ });
2377
2416
  root.setAnnotation("#document");
2378
2417
  return root;
2379
2418
  }
@@ -2392,7 +2431,14 @@ class HtmlElement extends DOMNode {
2392
2431
  const open = startToken.data[1] !== "/";
2393
2432
  const closed = isClosed(endToken, meta);
2394
2433
  const location = sliceLocation(startToken.location, 1);
2395
- return new HtmlElement(tagName, open ? parent : null, closed, meta, location);
2434
+ return new HtmlElement({
2435
+ nodeType: NodeType.ELEMENT_NODE,
2436
+ tagName,
2437
+ parent: open ? parent : null,
2438
+ closed,
2439
+ meta,
2440
+ location
2441
+ });
2396
2442
  }
2397
2443
  /**
2398
2444
  * Returns annotated name if set or defaults to `<tagName>`.
@@ -2590,10 +2636,13 @@ class HtmlElement extends DOMNode {
2590
2636
  */
2591
2637
  setAttribute(key, value, keyLocation, valueLocation, originalAttribute) {
2592
2638
  key = key.toLowerCase();
2593
- if (!this.attr[key]) {
2594
- this.attr[key] = [];
2639
+ const attr = new Attribute(key, value, keyLocation, valueLocation, originalAttribute);
2640
+ const list = this.attr[key];
2641
+ if (list) {
2642
+ list.push(attr);
2643
+ } else {
2644
+ this.attr[key] = [attr];
2595
2645
  }
2596
- this.attr[key].push(new Attribute(key, value, keyLocation, valueLocation, originalAttribute));
2597
2646
  }
2598
2647
  /**
2599
2648
  * Get parsed tabindex for this element.
@@ -2650,7 +2699,7 @@ class HtmlElement extends DOMNode {
2650
2699
  const matches = this.attr[key];
2651
2700
  return all ? matches : matches[0];
2652
2701
  } else {
2653
- return null;
2702
+ return all ? [] : null;
2654
2703
  }
2655
2704
  }
2656
2705
  /**
@@ -2756,20 +2805,6 @@ class HtmlElement extends DOMNode {
2756
2805
  yield* pattern.match(this);
2757
2806
  }
2758
2807
  }
2759
- /**
2760
- * Visit all nodes from this node and down. Depth first.
2761
- *
2762
- * @internal
2763
- */
2764
- visitDepthFirst(callback) {
2765
- function visit(node) {
2766
- node.childElements.forEach(visit);
2767
- if (!node.isRootElement()) {
2768
- callback(node);
2769
- }
2770
- }
2771
- visit(this);
2772
- }
2773
2808
  /**
2774
2809
  * Evaluates callbackk on all descendants, returning true if any are true.
2775
2810
  *
@@ -2833,738 +2868,309 @@ function isClosed(endToken, meta) {
2833
2868
  return closed;
2834
2869
  }
2835
2870
 
2836
- class DOMTree {
2837
- constructor(location) {
2838
- this.root = HtmlElement.rootNode(location);
2839
- this.active = this.root;
2840
- this.doctype = null;
2841
- }
2842
- pushActive(node) {
2843
- this.active = node;
2844
- }
2845
- popActive() {
2846
- if (this.active.isRootElement()) {
2847
- return;
2871
+ function dumpTree(root) {
2872
+ const lines = [];
2873
+ function decoration(node) {
2874
+ let output = "";
2875
+ if (node.id) {
2876
+ output += `#${node.id}`;
2848
2877
  }
2849
- this.active = this.active.parent ?? this.root;
2850
- }
2851
- getActive() {
2852
- return this.active;
2878
+ if (node.hasAttribute("class")) {
2879
+ output += `.${node.classList.join(".")}`;
2880
+ }
2881
+ return output;
2853
2882
  }
2854
- /**
2855
- * Resolve dynamic meta expressions.
2856
- */
2857
- resolveMeta(table) {
2858
- this.visitDepthFirst((node) => {
2859
- table.resolve(node);
2883
+ function writeNode(node, level, sibling) {
2884
+ if (node.parent) {
2885
+ const indent = " ".repeat(level - 1);
2886
+ const l = node.childElements.length > 0 ? "\u252C" : "\u2500";
2887
+ const b = sibling < node.parent.childElements.length - 1 ? "\u251C" : "\u2514";
2888
+ lines.push(`${indent}${b}\u2500${l} ${node.tagName}${decoration(node)}`);
2889
+ } else {
2890
+ lines.push("(root)");
2891
+ }
2892
+ node.childElements.forEach((child, index) => {
2893
+ writeNode(child, level + 1, index);
2860
2894
  });
2861
2895
  }
2862
- getElementsByTagName(tagName) {
2863
- return this.root.getElementsByTagName(tagName);
2896
+ writeNode(root, 0, 0);
2897
+ return lines;
2898
+ }
2899
+
2900
+ function escape(value) {
2901
+ return JSON.stringify(value);
2902
+ }
2903
+ function format(value, quote = false) {
2904
+ if (value === null) {
2905
+ return "null";
2864
2906
  }
2865
- visitDepthFirst(callback) {
2866
- this.root.visitDepthFirst(callback);
2907
+ if (typeof value === "number") {
2908
+ return value.toString();
2867
2909
  }
2868
- find(callback) {
2869
- return this.root.find(callback);
2910
+ if (typeof value === "string") {
2911
+ return quote ? escape(value) : value;
2870
2912
  }
2871
- querySelector(selector) {
2872
- return this.root.querySelector(selector);
2913
+ if (Array.isArray(value)) {
2914
+ const content = value.map((it) => format(it, true)).join(", ");
2915
+ return `[ ${content} ]`;
2873
2916
  }
2874
- querySelectorAll(selector) {
2875
- return this.root.querySelectorAll(selector);
2917
+ if (typeof value === "object") {
2918
+ const content = Object.entries(value).map(([key, nested]) => `${key}: ${format(nested, true)}`).join(", ");
2919
+ return `{ ${content} }`;
2876
2920
  }
2921
+ return String(value);
2922
+ }
2923
+ function interpolate(text, data) {
2924
+ return text.replace(/{{\s*([^\s{}]+)\s*}}/g, (match, key) => {
2925
+ return typeof data[key] !== "undefined" ? format(data[key]) : match;
2926
+ });
2877
2927
  }
2878
2928
 
2879
- const allowedKeys = ["exclude"];
2880
- class Validator {
2881
- /**
2882
- * Test if element is used in a proper context.
2883
- *
2884
- * @param node - Element to test.
2885
- * @param rules - List of rules.
2886
- * @returns `true` if element passes all tests.
2887
- */
2888
- static validatePermitted(node, rules) {
2889
- if (!rules) {
2890
- return true;
2891
- }
2892
- return rules.some((rule) => {
2893
- return Validator.validatePermittedRule(node, rule);
2894
- });
2929
+ function isThenable(value) {
2930
+ return value && typeof value === "object" && "then" in value && typeof value.then === "function";
2931
+ }
2932
+
2933
+ var Severity = /* @__PURE__ */ ((Severity2) => {
2934
+ Severity2[Severity2["DISABLED"] = 0] = "DISABLED";
2935
+ Severity2[Severity2["WARN"] = 1] = "WARN";
2936
+ Severity2[Severity2["ERROR"] = 2] = "ERROR";
2937
+ return Severity2;
2938
+ })(Severity || {});
2939
+ function parseSeverity(value) {
2940
+ switch (value) {
2941
+ case 0:
2942
+ case "off":
2943
+ return 0 /* DISABLED */;
2944
+ case 1:
2945
+ case "warn":
2946
+ return 1 /* WARN */;
2947
+ case 2:
2948
+ case "error":
2949
+ return 2 /* ERROR */;
2950
+ default:
2951
+ throw new Error(`Invalid severity "${String(value)}"`);
2895
2952
  }
2896
- /**
2897
- * Test if an element is used the correct amount of times.
2898
- *
2899
- * For instance, a `<table>` element can only contain a single `<tbody>`
2900
- * child. If multiple `<tbody>` exists this test will fail both nodes.
2901
- * Note that this is called on the parent but will fail the children violating
2902
- * the rule.
2903
- *
2904
- * @param children - Array of children to validate.
2905
- * @param rules - List of rules of the parent element.
2906
- * @returns `true` if the parent element of the children passes the test.
2907
- */
2908
- static validateOccurrences(children, rules, cb) {
2909
- if (!rules) {
2910
- return true;
2911
- }
2912
- let valid = true;
2913
- for (const rule of rules) {
2914
- if (typeof rule !== "string") {
2915
- return false;
2916
- }
2917
- const [, category, quantifier] = rule.match(/^(@?.*?)([?*]?)$/);
2918
- const limit = category && quantifier && parseQuantifier(quantifier);
2919
- if (limit) {
2920
- const siblings = children.filter(
2921
- (cur) => Validator.validatePermittedCategory(cur, rule, true)
2922
- );
2923
- if (siblings.length > limit) {
2924
- for (const child of siblings.slice(limit)) {
2925
- cb(child, category);
2926
- }
2927
- valid = false;
2928
- }
2929
- }
2930
- }
2931
- return valid;
2953
+ }
2954
+
2955
+ const cacheKey = Symbol("aria-naming");
2956
+ const defaultValue = "allowed";
2957
+ const prohibitedRoles = [
2958
+ "caption",
2959
+ "code",
2960
+ "deletion",
2961
+ "emphasis",
2962
+ "generic",
2963
+ "insertion",
2964
+ "paragraph",
2965
+ "presentation",
2966
+ "strong",
2967
+ "subscript",
2968
+ "superscript"
2969
+ ];
2970
+ function byRole(role) {
2971
+ return prohibitedRoles.includes(role) ? "prohibited" : "allowed";
2972
+ }
2973
+ function byMeta(element, meta) {
2974
+ return meta.aria.naming(element._adapter);
2975
+ }
2976
+ function ariaNaming(element) {
2977
+ var _a;
2978
+ const cached = element.cacheGet(cacheKey);
2979
+ if (cached) {
2980
+ return cached;
2932
2981
  }
2933
- /**
2934
- * Validate elements order.
2935
- *
2936
- * Given a parent element with children and metadata containing permitted
2937
- * order it will validate each children and ensure each one exists in the
2938
- * specified order.
2939
- *
2940
- * For instance, for a `<table>` element the `<caption>` element must come
2941
- * before a `<thead>` which must come before `<tbody>`.
2942
- *
2943
- * @param children - Array of children to validate.
2944
- */
2945
- static validateOrder(children, rules, cb) {
2946
- if (!rules) {
2947
- return true;
2948
- }
2949
- let i = 0;
2950
- let prev = null;
2951
- for (const node of children) {
2952
- const old = i;
2953
- while (rules[i] && !Validator.validatePermittedCategory(node, rules[i], true)) {
2954
- i++;
2955
- }
2956
- if (i >= rules.length) {
2957
- const orderSpecified = rules.find(
2958
- (cur) => Validator.validatePermittedCategory(node, cur, true)
2959
- );
2960
- if (orderSpecified) {
2961
- cb(node, prev);
2962
- return false;
2963
- }
2964
- i = old;
2965
- }
2966
- prev = node;
2982
+ const role = (_a = element.getAttribute("role")) == null ? void 0 : _a.value;
2983
+ if (role) {
2984
+ if (role instanceof DynamicValue) {
2985
+ return element.cacheSet(cacheKey, defaultValue);
2986
+ } else {
2987
+ return element.cacheSet(cacheKey, byRole(role));
2967
2988
  }
2968
- return true;
2969
2989
  }
2970
- /**
2971
- * Validate element ancestors.
2972
- *
2973
- * Check if an element has the required set of elements. At least one of the
2974
- * selectors must match.
2975
- */
2976
- static validateAncestors(node, rules) {
2977
- if (!rules || rules.length === 0) {
2978
- return true;
2979
- }
2980
- return rules.some((rule) => node.closest(rule));
2990
+ const meta = element.meta;
2991
+ if (!meta) {
2992
+ return element.cacheSet(cacheKey, defaultValue);
2981
2993
  }
2982
- /**
2983
- * Validate element required content.
2984
- *
2985
- * Check if an element has the required set of elements. At least one of the
2986
- * selectors must match.
2987
- *
2988
- * Returns `[]` when valid or a list of required but missing tagnames or
2989
- * categories.
2990
- */
2991
- static validateRequiredContent(node, rules) {
2992
- if (!rules || rules.length === 0) {
2993
- return [];
2994
- }
2995
- return rules.filter((tagName) => {
2996
- const haveMatchingChild = node.childElements.some(
2997
- (child) => Validator.validatePermittedCategory(child, tagName, false)
2998
- );
2999
- return !haveMatchingChild;
3000
- });
2994
+ return element.cacheSet(cacheKey, byMeta(element, meta));
2995
+ }
2996
+
2997
+ const patternCache = /* @__PURE__ */ new Map();
2998
+ function compileStringPattern(pattern) {
2999
+ const regexp = pattern.replace(/[*]+/g, ".+");
3000
+ return new RegExp(`^${regexp}$`);
3001
+ }
3002
+ function compileRegExpPattern(pattern) {
3003
+ return new RegExp(`^${pattern}$`);
3004
+ }
3005
+ function compilePattern(pattern) {
3006
+ const cached = patternCache.get(pattern);
3007
+ if (cached) {
3008
+ return cached;
3001
3009
  }
3002
- /**
3003
- * Test if an attribute has an allowed value and/or format.
3004
- *
3005
- * @param attr - Attribute to test.
3006
- * @param rules - Element attribute metadta.
3007
- * @returns `true` if attribute passes all tests.
3008
- */
3009
- static validateAttribute(attr, rules) {
3010
- const rule = rules[attr.key];
3011
- if (!rule) {
3012
- return true;
3013
- }
3014
- const value = attr.value;
3015
- if (value instanceof DynamicValue) {
3016
- return true;
3017
- }
3018
- const empty = value === null || value === "";
3019
- if (rule.boolean) {
3020
- return empty || value === attr.key;
3021
- }
3022
- if (rule.omit && empty) {
3010
+ const match = pattern.match(/^\/(.*)\/$/);
3011
+ const regexp = match ? compileRegExpPattern(match[1]) : compileStringPattern(pattern);
3012
+ patternCache.set(pattern, regexp);
3013
+ return regexp;
3014
+ }
3015
+ function keywordPatternMatcher(list, keyword) {
3016
+ for (const pattern of list) {
3017
+ const regexp = compilePattern(pattern);
3018
+ if (regexp.test(keyword)) {
3023
3019
  return true;
3024
3020
  }
3025
- if (rule.list) {
3026
- const tokens = new DOMTokenList(value, attr.valueLocation);
3027
- return tokens.every((token) => {
3028
- return this.validateAttributeValue(token, rule);
3029
- });
3030
- }
3031
- return this.validateAttributeValue(value, rule);
3032
3021
  }
3033
- static validateAttributeValue(value, rule) {
3034
- if (!rule.enum) {
3035
- return true;
3036
- }
3037
- if (value === null || value === void 0) {
3038
- return false;
3039
- }
3040
- const caseInsensitiveValue = value.toLowerCase();
3041
- return rule.enum.some((entry) => {
3042
- if (entry instanceof RegExp) {
3043
- return !!value.match(entry);
3044
- } else {
3045
- return caseInsensitiveValue === entry;
3046
- }
3047
- });
3022
+ return false;
3023
+ }
3024
+ function isKeywordIgnored(options, keyword, matcher = (list, it) => list.includes(it)) {
3025
+ const { include, exclude } = options;
3026
+ if (include && !matcher(include, keyword)) {
3027
+ return true;
3048
3028
  }
3049
- static validatePermittedRule(node, rule, isExclude = false) {
3050
- if (typeof rule === "string") {
3051
- return Validator.validatePermittedCategory(node, rule, !isExclude);
3052
- } else if (Array.isArray(rule)) {
3053
- return rule.every((inner) => {
3054
- return Validator.validatePermittedRule(node, inner, isExclude);
3055
- });
3056
- } else {
3057
- validateKeys(rule);
3058
- if (rule.exclude) {
3059
- if (Array.isArray(rule.exclude)) {
3060
- return !rule.exclude.some((inner) => {
3061
- return Validator.validatePermittedRule(node, inner, true);
3062
- });
3063
- } else {
3064
- return !Validator.validatePermittedRule(node, rule.exclude, true);
3065
- }
3066
- } else {
3067
- return true;
3068
- }
3069
- }
3029
+ if (exclude && matcher(exclude, keyword)) {
3030
+ return true;
3070
3031
  }
3071
- /**
3072
- * Validate node against a content category.
3073
- *
3074
- * When matching parent nodes against permitted parents use the superset
3075
- * parameter to also match for `@flow`. E.g. if a node expects a `@phrasing`
3076
- * parent it should also allow `@flow` parent since `@phrasing` is a subset of
3077
- * `@flow`.
3078
- *
3079
- * @param node - The node to test against
3080
- * @param category - Name of category with `@` prefix or tag name.
3081
- * @param defaultMatch - The default return value when node categories is not known.
3082
- */
3083
- /* eslint-disable-next-line complexity -- rule does not like switch */
3084
- static validatePermittedCategory(node, category, defaultMatch) {
3085
- const [, rawCategory] = category.match(/^(@?.*?)([?*]?)$/);
3086
- if (!rawCategory.startsWith("@")) {
3087
- return node.tagName === rawCategory;
3088
- }
3089
- if (!node.meta) {
3090
- return defaultMatch;
3091
- }
3092
- switch (rawCategory) {
3093
- case "@meta":
3094
- return node.meta.metadata;
3095
- case "@flow":
3096
- return node.meta.flow;
3097
- case "@sectioning":
3098
- return node.meta.sectioning;
3099
- case "@heading":
3100
- return node.meta.heading;
3101
- case "@phrasing":
3102
- return node.meta.phrasing;
3103
- case "@embedded":
3104
- return node.meta.embedded;
3105
- case "@interactive":
3106
- return node.meta.interactive;
3107
- case "@script":
3108
- return Boolean(node.meta.scriptSupporting);
3109
- case "@form":
3110
- return Boolean(node.meta.form);
3111
- default:
3112
- throw new Error(`Invalid content category "${category}"`);
3113
- }
3032
+ return false;
3033
+ }
3034
+
3035
+ const ARIA_HIDDEN_CACHE = Symbol(isAriaHidden.name);
3036
+ const HTML_HIDDEN_CACHE = Symbol(isHTMLHidden.name);
3037
+ const INERT_CACHE = Symbol(isInert.name);
3038
+ const ROLE_PRESENTATION_CACHE = Symbol(isPresentation.name);
3039
+ const STYLE_HIDDEN_CACHE = Symbol(isStyleHidden.name);
3040
+ function inAccessibilityTree(node) {
3041
+ if (isAriaHidden(node)) {
3042
+ return false;
3114
3043
  }
3044
+ if (isPresentation(node)) {
3045
+ return false;
3046
+ }
3047
+ if (isHTMLHidden(node)) {
3048
+ return false;
3049
+ }
3050
+ if (isInert(node)) {
3051
+ return false;
3052
+ }
3053
+ if (isStyleHidden(node)) {
3054
+ return false;
3055
+ }
3056
+ return true;
3115
3057
  }
3116
- function validateKeys(rule) {
3117
- for (const key of Object.keys(rule)) {
3118
- if (!allowedKeys.includes(key)) {
3119
- const str = JSON.stringify(rule);
3120
- throw new Error(`Permitted rule "${str}" contains unknown property "${key}"`);
3121
- }
3058
+ function isAriaHiddenImpl(node) {
3059
+ const getAriaHiddenAttr = (node2) => {
3060
+ const ariaHidden = node2.getAttribute("aria-hidden");
3061
+ return Boolean(ariaHidden && ariaHidden.value === "true");
3062
+ };
3063
+ return {
3064
+ byParent: node.parent ? isAriaHidden(node.parent) : false,
3065
+ bySelf: getAriaHiddenAttr(node)
3066
+ };
3067
+ }
3068
+ function isAriaHidden(node, details) {
3069
+ const cached = node.cacheGet(ARIA_HIDDEN_CACHE);
3070
+ if (cached) {
3071
+ return details ? cached : cached.byParent || cached.bySelf;
3122
3072
  }
3073
+ const result = node.cacheSet(ARIA_HIDDEN_CACHE, isAriaHiddenImpl(node));
3074
+ return details ? result : result.byParent || result.bySelf;
3123
3075
  }
3124
- function parseQuantifier(quantifier) {
3125
- switch (quantifier) {
3126
- case "?":
3127
- return 1;
3128
- case "*":
3129
- return null;
3130
- default:
3131
- throw new Error(`Invalid quantifier "${quantifier}" used`);
3076
+ function isHTMLHiddenImpl(node) {
3077
+ const getHiddenAttr = (node2) => {
3078
+ const hidden = node2.getAttribute("hidden");
3079
+ return Boolean(hidden == null ? void 0 : hidden.isStatic);
3080
+ };
3081
+ return {
3082
+ byParent: node.parent ? isHTMLHidden(node.parent) : false,
3083
+ bySelf: getHiddenAttr(node)
3084
+ };
3085
+ }
3086
+ function isHTMLHidden(node, details) {
3087
+ const cached = node.cacheGet(HTML_HIDDEN_CACHE);
3088
+ if (cached) {
3089
+ return details ? cached : cached.byParent || cached.bySelf;
3132
3090
  }
3091
+ const result = node.cacheSet(HTML_HIDDEN_CACHE, isHTMLHiddenImpl(node));
3092
+ return details ? result : result.byParent || result.bySelf;
3133
3093
  }
3134
-
3135
- const $schema = "http://json-schema.org/draft-06/schema#";
3136
- const $id = "https://html-validate.org/schemas/config.json";
3137
- const type = "object";
3138
- const additionalProperties = false;
3139
- const properties = {
3140
- $schema: {
3141
- type: "string"
3142
- },
3143
- root: {
3144
- type: "boolean",
3145
- title: "Mark as root configuration",
3146
- description: "If this is set to true no further configurations will be searched.",
3147
- "default": false
3148
- },
3149
- "extends": {
3150
- type: "array",
3151
- items: {
3152
- type: "string"
3153
- },
3154
- title: "Configurations to extend",
3155
- description: "Array of shareable or builtin configurations to extend."
3156
- },
3157
- elements: {
3158
- type: "array",
3159
- items: {
3160
- anyOf: [
3161
- {
3162
- type: "string"
3163
- },
3164
- {
3165
- type: "object"
3166
- }
3167
- ]
3168
- },
3169
- title: "Element metadata to load",
3170
- description: "Array of modules, plugins or files to load element metadata from. Use <rootDir> to refer to the folder with the package.json file.",
3171
- examples: [
3172
- [
3173
- "html-validate:recommended",
3174
- "plugin:recommended",
3175
- "module",
3176
- "./local-file.json"
3177
- ]
3178
- ]
3179
- },
3180
- plugins: {
3181
- type: "array",
3182
- items: {
3183
- anyOf: [
3184
- {
3185
- type: "string"
3186
- },
3187
- {
3188
- type: "object"
3189
- }
3190
- ]
3191
- },
3192
- title: "Plugins to load",
3193
- description: "Array of plugins load. Use <rootDir> to refer to the folder with the package.json file.",
3194
- examples: [
3195
- [
3196
- "my-plugin",
3197
- "./local-plugin"
3198
- ]
3199
- ]
3200
- },
3201
- transform: {
3202
- type: "object",
3203
- additionalProperties: {
3204
- type: "string"
3205
- },
3206
- title: "File transformations to use.",
3207
- description: "Object where key is regular expression to match filename and value is name of transformer.",
3208
- examples: [
3209
- {
3210
- "^.*\\.foo$": "my-transformer",
3211
- "^.*\\.bar$": "my-plugin",
3212
- "^.*\\.baz$": "my-plugin:named"
3213
- }
3214
- ]
3215
- },
3216
- rules: {
3217
- type: "object",
3218
- patternProperties: {
3219
- ".*": {
3220
- anyOf: [
3221
- {
3222
- "enum": [
3223
- 0,
3224
- 1,
3225
- 2,
3226
- "off",
3227
- "warn",
3228
- "error"
3229
- ]
3230
- },
3231
- {
3232
- type: "array",
3233
- minItems: 1,
3234
- maxItems: 1,
3235
- items: [
3236
- {
3237
- "enum": [
3238
- 0,
3239
- 1,
3240
- 2,
3241
- "off",
3242
- "warn",
3243
- "error"
3244
- ]
3245
- }
3246
- ]
3247
- },
3248
- {
3249
- type: "array",
3250
- minItems: 2,
3251
- maxItems: 2,
3252
- items: [
3253
- {
3254
- "enum": [
3255
- 0,
3256
- 1,
3257
- 2,
3258
- "off",
3259
- "warn",
3260
- "error"
3261
- ]
3262
- },
3263
- {
3264
- }
3265
- ]
3266
- }
3267
- ]
3268
- }
3269
- },
3270
- title: "Rule configuration.",
3271
- description: "Enable/disable rules, set severity. Some rules have additional configuration like style or patterns to use.",
3272
- examples: [
3273
- {
3274
- foo: "error",
3275
- bar: "off",
3276
- baz: [
3277
- "error",
3278
- {
3279
- style: "camelcase"
3280
- }
3281
- ]
3282
- }
3283
- ]
3284
- }
3285
- };
3286
- var configurationSchema = {
3287
- $schema: $schema,
3288
- $id: $id,
3289
- type: type,
3290
- additionalProperties: additionalProperties,
3291
- properties: properties
3292
- };
3293
-
3294
- const TRANSFORMER_API = {
3295
- VERSION: 1
3296
- };
3297
-
3298
- var Severity = /* @__PURE__ */ ((Severity2) => {
3299
- Severity2[Severity2["DISABLED"] = 0] = "DISABLED";
3300
- Severity2[Severity2["WARN"] = 1] = "WARN";
3301
- Severity2[Severity2["ERROR"] = 2] = "ERROR";
3302
- return Severity2;
3303
- })(Severity || {});
3304
- function parseSeverity(value) {
3305
- switch (value) {
3306
- case 0:
3307
- case "off":
3308
- return 0 /* DISABLED */;
3309
- case 1:
3310
- case "warn":
3311
- return 1 /* WARN */;
3312
- case 2:
3313
- case "error":
3314
- return 2 /* ERROR */;
3315
- default:
3316
- throw new Error(`Invalid severity "${String(value)}"`);
3094
+ function isInertImpl(node) {
3095
+ const getInertAttr = (node2) => {
3096
+ const inert = node2.getAttribute("inert");
3097
+ return Boolean(inert == null ? void 0 : inert.isStatic);
3098
+ };
3099
+ return {
3100
+ byParent: node.parent ? isInert(node.parent) : false,
3101
+ bySelf: getInertAttr(node)
3102
+ };
3103
+ }
3104
+ function isInert(node, details) {
3105
+ const cached = node.cacheGet(INERT_CACHE);
3106
+ if (cached) {
3107
+ return details ? cached : cached.byParent || cached.bySelf;
3317
3108
  }
3109
+ const result = node.cacheSet(INERT_CACHE, isInertImpl(node));
3110
+ return details ? result : result.byParent || result.bySelf;
3318
3111
  }
3319
-
3320
- function escape(value) {
3321
- return JSON.stringify(value);
3112
+ function isStyleHiddenImpl(node) {
3113
+ const getStyleAttr = (node2) => {
3114
+ const style = node2.getAttribute("style");
3115
+ const { display, visibility } = parseCssDeclaration(style == null ? void 0 : style.value);
3116
+ return display === "none" || visibility === "hidden";
3117
+ };
3118
+ const byParent = node.parent ? isStyleHidden(node.parent) : false;
3119
+ const bySelf = getStyleAttr(node);
3120
+ return byParent || bySelf;
3322
3121
  }
3323
- function format(value, quote = false) {
3324
- if (value === null) {
3325
- return "null";
3122
+ function isStyleHidden(node) {
3123
+ const cached = node.cacheGet(STYLE_HIDDEN_CACHE);
3124
+ if (cached) {
3125
+ return cached;
3326
3126
  }
3327
- if (typeof value === "number") {
3328
- return value.toString();
3127
+ return node.cacheSet(STYLE_HIDDEN_CACHE, isStyleHiddenImpl(node));
3128
+ }
3129
+ function isPresentation(node) {
3130
+ if (node.cacheExists(ROLE_PRESENTATION_CACHE)) {
3131
+ return Boolean(node.cacheGet(ROLE_PRESENTATION_CACHE));
3329
3132
  }
3330
- if (typeof value === "string") {
3331
- return quote ? escape(value) : value;
3133
+ const meta = node.meta;
3134
+ if (meta == null ? void 0 : meta.interactive) {
3135
+ return node.cacheSet(ROLE_PRESENTATION_CACHE, false);
3332
3136
  }
3333
- if (Array.isArray(value)) {
3334
- const content = value.map((it) => format(it, true)).join(", ");
3335
- return `[ ${content} ]`;
3137
+ const tabindex = node.getAttribute("tabindex");
3138
+ if (tabindex) {
3139
+ return node.cacheSet(ROLE_PRESENTATION_CACHE, false);
3336
3140
  }
3337
- if (typeof value === "object") {
3338
- const content = Object.entries(value).map(([key, nested]) => `${key}: ${format(nested, true)}`).join(", ");
3339
- return `{ ${content} }`;
3141
+ const role = node.getAttribute("role");
3142
+ if (role && (role.value === "presentation" || role.value === "none")) {
3143
+ return node.cacheSet(ROLE_PRESENTATION_CACHE, true);
3144
+ } else {
3145
+ return node.cacheSet(ROLE_PRESENTATION_CACHE, false);
3340
3146
  }
3341
- return String(value);
3342
- }
3343
- function interpolate(text, data) {
3344
- return text.replace(/{{\s*([^\s{}]+)\s*}}/g, (match, key) => {
3345
- return typeof data[key] !== "undefined" ? format(data[key]) : match;
3346
- });
3347
3147
  }
3348
3148
 
3349
- const cacheKey = Symbol("aria-naming");
3350
- const defaultValue = "allowed";
3351
- const prohibitedRoles = [
3352
- "caption",
3353
- "code",
3354
- "deletion",
3355
- "emphasis",
3356
- "generic",
3357
- "insertion",
3358
- "paragraph",
3359
- "presentation",
3360
- "strong",
3361
- "subscript",
3362
- "superscript"
3363
- ];
3364
- function byRole(role) {
3365
- return prohibitedRoles.includes(role) ? "prohibited" : "allowed";
3149
+ const cachePrefix = classifyNodeText.name;
3150
+ const HTML_CACHE_KEY = Symbol(`${cachePrefix}|html`);
3151
+ const A11Y_CACHE_KEY = Symbol(`${cachePrefix}|a11y`);
3152
+ const IGNORE_HIDDEN_ROOT_HTML_CACHE_KEY = Symbol(`${cachePrefix}|html|ignore-hidden-root`);
3153
+ const IGNORE_HIDDEN_ROOT_A11Y_CACHE_KEY = Symbol(`${cachePrefix}|a11y|ignore-hidden-root`);
3154
+ var TextClassification = /* @__PURE__ */ ((TextClassification2) => {
3155
+ TextClassification2[TextClassification2["EMPTY_TEXT"] = 0] = "EMPTY_TEXT";
3156
+ TextClassification2[TextClassification2["DYNAMIC_TEXT"] = 1] = "DYNAMIC_TEXT";
3157
+ TextClassification2[TextClassification2["STATIC_TEXT"] = 2] = "STATIC_TEXT";
3158
+ return TextClassification2;
3159
+ })(TextClassification || {});
3160
+ function getCachekey(options) {
3161
+ const { accessible = false, ignoreHiddenRoot = false } = options;
3162
+ if (accessible && ignoreHiddenRoot) {
3163
+ return IGNORE_HIDDEN_ROOT_A11Y_CACHE_KEY;
3164
+ } else if (ignoreHiddenRoot) {
3165
+ return IGNORE_HIDDEN_ROOT_HTML_CACHE_KEY;
3166
+ } else if (accessible) {
3167
+ return A11Y_CACHE_KEY;
3168
+ } else {
3169
+ return HTML_CACHE_KEY;
3170
+ }
3366
3171
  }
3367
- function byMeta(element, meta) {
3368
- return meta.aria.naming(element._adapter);
3369
- }
3370
- function ariaNaming(element) {
3371
- var _a;
3372
- const cached = element.cacheGet(cacheKey);
3373
- if (cached) {
3374
- return cached;
3375
- }
3376
- const role = (_a = element.getAttribute("role")) == null ? void 0 : _a.value;
3377
- if (role) {
3378
- if (role instanceof DynamicValue) {
3379
- return element.cacheSet(cacheKey, defaultValue);
3380
- } else {
3381
- return element.cacheSet(cacheKey, byRole(role));
3382
- }
3383
- }
3384
- const meta = element.meta;
3385
- if (!meta) {
3386
- return element.cacheSet(cacheKey, defaultValue);
3387
- }
3388
- return element.cacheSet(cacheKey, byMeta(element, meta));
3389
- }
3390
-
3391
- const patternCache = /* @__PURE__ */ new Map();
3392
- function compileStringPattern(pattern) {
3393
- const regexp = pattern.replace(/[*]+/g, ".+");
3394
- return new RegExp(`^${regexp}$`);
3395
- }
3396
- function compileRegExpPattern(pattern) {
3397
- return new RegExp(`^${pattern}$`);
3398
- }
3399
- function compilePattern(pattern) {
3400
- const cached = patternCache.get(pattern);
3401
- if (cached) {
3402
- return cached;
3403
- }
3404
- const match = pattern.match(/^\/(.*)\/$/);
3405
- const regexp = match ? compileRegExpPattern(match[1]) : compileStringPattern(pattern);
3406
- patternCache.set(pattern, regexp);
3407
- return regexp;
3408
- }
3409
- function keywordPatternMatcher(list, keyword) {
3410
- for (const pattern of list) {
3411
- const regexp = compilePattern(pattern);
3412
- if (regexp.test(keyword)) {
3413
- return true;
3414
- }
3415
- }
3416
- return false;
3417
- }
3418
- function isKeywordIgnored(options, keyword, matcher = (list, it) => list.includes(it)) {
3419
- const { include, exclude } = options;
3420
- if (include && !matcher(include, keyword)) {
3421
- return true;
3422
- }
3423
- if (exclude && matcher(exclude, keyword)) {
3424
- return true;
3425
- }
3426
- return false;
3427
- }
3428
-
3429
- const ARIA_HIDDEN_CACHE = Symbol(isAriaHidden.name);
3430
- const HTML_HIDDEN_CACHE = Symbol(isHTMLHidden.name);
3431
- const INERT_CACHE = Symbol(isInert.name);
3432
- const ROLE_PRESENTATION_CACHE = Symbol(isPresentation.name);
3433
- const STYLE_HIDDEN_CACHE = Symbol(isStyleHidden.name);
3434
- function inAccessibilityTree(node) {
3435
- if (isAriaHidden(node)) {
3436
- return false;
3437
- }
3438
- if (isPresentation(node)) {
3439
- return false;
3440
- }
3441
- if (isHTMLHidden(node)) {
3442
- return false;
3443
- }
3444
- if (isInert(node)) {
3445
- return false;
3446
- }
3447
- if (isStyleHidden(node)) {
3448
- return false;
3449
- }
3450
- return true;
3451
- }
3452
- function isAriaHiddenImpl(node) {
3453
- const getAriaHiddenAttr = (node2) => {
3454
- const ariaHidden = node2.getAttribute("aria-hidden");
3455
- return Boolean(ariaHidden && ariaHidden.value === "true");
3456
- };
3457
- return {
3458
- byParent: node.parent ? isAriaHidden(node.parent) : false,
3459
- bySelf: getAriaHiddenAttr(node)
3460
- };
3461
- }
3462
- function isAriaHidden(node, details) {
3463
- const cached = node.cacheGet(ARIA_HIDDEN_CACHE);
3464
- if (cached) {
3465
- return details ? cached : cached.byParent || cached.bySelf;
3466
- }
3467
- const result = node.cacheSet(ARIA_HIDDEN_CACHE, isAriaHiddenImpl(node));
3468
- return details ? result : result.byParent || result.bySelf;
3469
- }
3470
- function isHTMLHiddenImpl(node) {
3471
- const getHiddenAttr = (node2) => {
3472
- const hidden = node2.getAttribute("hidden");
3473
- return Boolean(hidden == null ? void 0 : hidden.isStatic);
3474
- };
3475
- return {
3476
- byParent: node.parent ? isHTMLHidden(node.parent) : false,
3477
- bySelf: getHiddenAttr(node)
3478
- };
3479
- }
3480
- function isHTMLHidden(node, details) {
3481
- const cached = node.cacheGet(HTML_HIDDEN_CACHE);
3482
- if (cached) {
3483
- return details ? cached : cached.byParent || cached.bySelf;
3484
- }
3485
- const result = node.cacheSet(HTML_HIDDEN_CACHE, isHTMLHiddenImpl(node));
3486
- return details ? result : result.byParent || result.bySelf;
3487
- }
3488
- function isInertImpl(node) {
3489
- const getInertAttr = (node2) => {
3490
- const inert = node2.getAttribute("inert");
3491
- return Boolean(inert == null ? void 0 : inert.isStatic);
3492
- };
3493
- return {
3494
- byParent: node.parent ? isInert(node.parent) : false,
3495
- bySelf: getInertAttr(node)
3496
- };
3497
- }
3498
- function isInert(node, details) {
3499
- const cached = node.cacheGet(INERT_CACHE);
3500
- if (cached) {
3501
- return details ? cached : cached.byParent || cached.bySelf;
3502
- }
3503
- const result = node.cacheSet(INERT_CACHE, isInertImpl(node));
3504
- return details ? result : result.byParent || result.bySelf;
3505
- }
3506
- function isStyleHiddenImpl(node) {
3507
- const getStyleAttr = (node2) => {
3508
- const style = node2.getAttribute("style");
3509
- const { display, visibility } = parseCssDeclaration(style == null ? void 0 : style.value);
3510
- return display === "none" || visibility === "hidden";
3511
- };
3512
- const byParent = node.parent ? isStyleHidden(node.parent) : false;
3513
- const bySelf = getStyleAttr(node);
3514
- return byParent || bySelf;
3515
- }
3516
- function isStyleHidden(node) {
3517
- const cached = node.cacheGet(STYLE_HIDDEN_CACHE);
3518
- if (cached) {
3519
- return cached;
3520
- }
3521
- return node.cacheSet(STYLE_HIDDEN_CACHE, isStyleHiddenImpl(node));
3522
- }
3523
- function isPresentation(node) {
3524
- if (node.cacheExists(ROLE_PRESENTATION_CACHE)) {
3525
- return Boolean(node.cacheGet(ROLE_PRESENTATION_CACHE));
3526
- }
3527
- const meta = node.meta;
3528
- if (meta == null ? void 0 : meta.interactive) {
3529
- return node.cacheSet(ROLE_PRESENTATION_CACHE, false);
3530
- }
3531
- const tabindex = node.getAttribute("tabindex");
3532
- if (tabindex) {
3533
- return node.cacheSet(ROLE_PRESENTATION_CACHE, false);
3534
- }
3535
- const role = node.getAttribute("role");
3536
- if (role && (role.value === "presentation" || role.value === "none")) {
3537
- return node.cacheSet(ROLE_PRESENTATION_CACHE, true);
3538
- } else {
3539
- return node.cacheSet(ROLE_PRESENTATION_CACHE, false);
3540
- }
3541
- }
3542
-
3543
- const cachePrefix = classifyNodeText.name;
3544
- const HTML_CACHE_KEY = Symbol(`${cachePrefix}|html`);
3545
- const A11Y_CACHE_KEY = Symbol(`${cachePrefix}|a11y`);
3546
- const IGNORE_HIDDEN_ROOT_HTML_CACHE_KEY = Symbol(`${cachePrefix}|html|ignore-hidden-root`);
3547
- const IGNORE_HIDDEN_ROOT_A11Y_CACHE_KEY = Symbol(`${cachePrefix}|a11y|ignore-hidden-root`);
3548
- var TextClassification = /* @__PURE__ */ ((TextClassification2) => {
3549
- TextClassification2[TextClassification2["EMPTY_TEXT"] = 0] = "EMPTY_TEXT";
3550
- TextClassification2[TextClassification2["DYNAMIC_TEXT"] = 1] = "DYNAMIC_TEXT";
3551
- TextClassification2[TextClassification2["STATIC_TEXT"] = 2] = "STATIC_TEXT";
3552
- return TextClassification2;
3553
- })(TextClassification || {});
3554
- function getCachekey(options) {
3555
- const { accessible = false, ignoreHiddenRoot = false } = options;
3556
- if (accessible && ignoreHiddenRoot) {
3557
- return IGNORE_HIDDEN_ROOT_A11Y_CACHE_KEY;
3558
- } else if (ignoreHiddenRoot) {
3559
- return IGNORE_HIDDEN_ROOT_HTML_CACHE_KEY;
3560
- } else if (accessible) {
3561
- return A11Y_CACHE_KEY;
3562
- } else {
3563
- return HTML_CACHE_KEY;
3564
- }
3565
- }
3566
- function isSpecialEmpty(node) {
3567
- return node.is("select") || node.is("textarea");
3172
+ function isSpecialEmpty(node) {
3173
+ return node.is("select") || node.is("textarea");
3568
3174
  }
3569
3175
  function classifyNodeText(node, options = {}) {
3570
3176
  const { accessible = false, ignoreHiddenRoot = false } = options;
@@ -5022,7 +4628,7 @@ class AttributeAllowedValues extends Rule {
5022
4628
  setup() {
5023
4629
  this.on("dom:ready", (event) => {
5024
4630
  const doc = event.document;
5025
- doc.visitDepthFirst((node) => {
4631
+ walk.depthFirst(doc, (node) => {
5026
4632
  const meta = node.meta;
5027
4633
  if (!(meta == null ? void 0 : meta.attributes))
5028
4634
  return;
@@ -5082,7 +4688,7 @@ class AttributeBooleanStyle extends Rule {
5082
4688
  setup() {
5083
4689
  this.on("dom:ready", (event) => {
5084
4690
  const doc = event.document;
5085
- doc.visitDepthFirst((node) => {
4691
+ walk.depthFirst(doc, (node) => {
5086
4692
  const meta = node.meta;
5087
4693
  if (!(meta == null ? void 0 : meta.attributes))
5088
4694
  return;
@@ -5154,7 +4760,7 @@ class AttributeEmptyStyle extends Rule {
5154
4760
  setup() {
5155
4761
  this.on("dom:ready", (event) => {
5156
4762
  const doc = event.document;
5157
- doc.visitDepthFirst((node) => {
4763
+ walk.depthFirst(doc, (node) => {
5158
4764
  const meta = node.meta;
5159
4765
  if (!(meta == null ? void 0 : meta.attributes))
5160
4766
  return;
@@ -5806,7 +5412,7 @@ class ElementPermittedContent extends Rule {
5806
5412
  setup() {
5807
5413
  this.on("dom:ready", (event) => {
5808
5414
  const doc = event.document;
5809
- doc.visitDepthFirst((node) => {
5415
+ walk.depthFirst(doc, (node) => {
5810
5416
  const parent = node.parent;
5811
5417
  if (!parent) {
5812
5418
  return;
@@ -5885,7 +5491,7 @@ class ElementPermittedOccurrences extends Rule {
5885
5491
  setup() {
5886
5492
  this.on("dom:ready", (event) => {
5887
5493
  const doc = event.document;
5888
- doc.visitDepthFirst((node) => {
5494
+ walk.depthFirst(doc, (node) => {
5889
5495
  if (!node.meta) {
5890
5496
  return;
5891
5497
  }
@@ -5918,7 +5524,7 @@ class ElementPermittedOrder extends Rule {
5918
5524
  setup() {
5919
5525
  this.on("dom:ready", (event) => {
5920
5526
  const doc = event.document;
5921
- doc.visitDepthFirst((node) => {
5527
+ walk.depthFirst(doc, (node) => {
5922
5528
  if (!node.meta) {
5923
5529
  return;
5924
5530
  }
@@ -5988,7 +5594,7 @@ class ElementPermittedParent extends Rule {
5988
5594
  setup() {
5989
5595
  this.on("dom:ready", (event) => {
5990
5596
  const doc = event.document;
5991
- doc.visitDepthFirst((node) => {
5597
+ walk.depthFirst(doc, (node) => {
5992
5598
  var _a;
5993
5599
  const parent = node.parent;
5994
5600
  if (!parent) {
@@ -6036,7 +5642,7 @@ class ElementRequiredAncestor extends Rule {
6036
5642
  setup() {
6037
5643
  this.on("dom:ready", (event) => {
6038
5644
  const doc = event.document;
6039
- doc.visitDepthFirst((node) => {
5645
+ walk.depthFirst(doc, (node) => {
6040
5646
  const parent = node.parent;
6041
5647
  if (!parent) {
6042
5648
  return;
@@ -6120,7 +5726,7 @@ class ElementRequiredContent extends Rule {
6120
5726
  setup() {
6121
5727
  this.on("dom:ready", (event) => {
6122
5728
  const doc = event.document;
6123
- doc.visitDepthFirst((node) => {
5729
+ walk.depthFirst(doc, (node) => {
6124
5730
  if (!node.meta) {
6125
5731
  return;
6126
5732
  }
@@ -6640,7 +6246,7 @@ function isFocusableImpl(element) {
6640
6246
  if (isDisabled(element, meta)) {
6641
6247
  return false;
6642
6248
  }
6643
- return Boolean(meta == null ? void 0 : meta.focusable);
6249
+ return Boolean(meta.focusable);
6644
6250
  }
6645
6251
  function isFocusable(element) {
6646
6252
  const cached = element.cacheGet(FOCUSABLE_CACHE);
@@ -9382,946 +8988,1459 @@ function matchFieldNames2(token) {
9382
8988
  function matchWebauthn(token) {
9383
8989
  return token === "webauthn";
9384
8990
  }
9385
- function matchToken(token) {
9386
- if (matchSection(token)) {
9387
- return "section";
8991
+ function matchToken(token) {
8992
+ if (matchSection(token)) {
8993
+ return "section";
8994
+ }
8995
+ if (matchHint(token)) {
8996
+ return "hint";
8997
+ }
8998
+ if (matchFieldNames1(token)) {
8999
+ return "field1";
9000
+ }
9001
+ if (matchFieldNames2(token)) {
9002
+ return "field2";
9003
+ }
9004
+ if (matchContact(token)) {
9005
+ return "contact";
9006
+ }
9007
+ if (matchWebauthn(token)) {
9008
+ return "webauthn";
9009
+ }
9010
+ return null;
9011
+ }
9012
+ function getControlGroups(type) {
9013
+ const allGroups = [
9014
+ "text",
9015
+ "multiline",
9016
+ "password",
9017
+ "url",
9018
+ "username",
9019
+ "tel",
9020
+ "numeric",
9021
+ "month",
9022
+ "date"
9023
+ ];
9024
+ const mapping = {
9025
+ hidden: allGroups,
9026
+ text: allGroups.filter((it) => it !== "multiline"),
9027
+ search: allGroups.filter((it) => it !== "multiline"),
9028
+ password: ["password"],
9029
+ url: ["url"],
9030
+ email: ["username"],
9031
+ tel: ["tel"],
9032
+ number: ["numeric"],
9033
+ month: ["month"],
9034
+ date: ["date"]
9035
+ };
9036
+ return mapping[type] ?? [];
9037
+ }
9038
+ function isDisallowedType(node, type) {
9039
+ if (!node.is("input")) {
9040
+ return false;
9041
+ }
9042
+ return disallowedInputTypes.includes(type);
9043
+ }
9044
+ function getTerminalMessage(context) {
9045
+ switch (context.msg) {
9046
+ case 0 /* InvalidAttribute */:
9047
+ return "autocomplete attribute cannot be used on {{ what }}";
9048
+ case 1 /* InvalidValue */:
9049
+ return '"{{ value }}" cannot be used on {{ what }}';
9050
+ case 2 /* InvalidOrder */:
9051
+ return '"{{ second }}" must appear before "{{ first }}"';
9052
+ case 3 /* InvalidToken */:
9053
+ return '"{{ token }}" is not a valid autocomplete token or field name';
9054
+ case 4 /* InvalidCombination */:
9055
+ return '"{{ second }}" cannot be combined with "{{ first }}"';
9056
+ case 5 /* MissingField */:
9057
+ return "autocomplete attribute is missing field name";
9058
+ }
9059
+ }
9060
+ function getMarkdownMessage(context) {
9061
+ switch (context.msg) {
9062
+ case 0 /* InvalidAttribute */:
9063
+ return [
9064
+ `\`autocomplete\` attribute cannot be used on \`${context.what}\``,
9065
+ "",
9066
+ "The following input types cannot use the `autocomplete` attribute:",
9067
+ "",
9068
+ ...disallowedInputTypes.map((it) => `- \`${it}\``)
9069
+ ].join("\n");
9070
+ case 1 /* InvalidValue */: {
9071
+ const message = `\`"${context.value}"\` cannot be used on \`${context.what}\``;
9072
+ if (context.type === "form") {
9073
+ return [
9074
+ message,
9075
+ "",
9076
+ 'The `<form>` element can only use the values `"on"` and `"off"`.'
9077
+ ].join("\n");
9078
+ }
9079
+ if (context.type === "hidden") {
9080
+ return [
9081
+ message,
9082
+ "",
9083
+ '`<input type="hidden">` cannot use the values `"on"` and `"off"`.'
9084
+ ].join("\n");
9085
+ }
9086
+ const controlGroups = getControlGroups(context.type);
9087
+ const currentGroup = fieldNameGroup[context.value];
9088
+ return [
9089
+ message,
9090
+ "",
9091
+ `\`${context.what}\` allows autocomplete fields from the following group${controlGroups.length > 1 ? "s" : ""}:`,
9092
+ "",
9093
+ ...controlGroups.map((it) => `- ${it}`),
9094
+ "",
9095
+ `The field \`"${context.value}"\` belongs to the group /${currentGroup}/ which cannot be used with this input type.`
9096
+ ].join("\n");
9097
+ }
9098
+ case 2 /* InvalidOrder */:
9099
+ return [
9100
+ `\`"${context.second}"\` must appear before \`"${context.first}"\``,
9101
+ "",
9102
+ "The autocomplete tokens must appear in the following order:",
9103
+ "",
9104
+ "- Optional section name (`section-` prefix).",
9105
+ "- Optional `shipping` or `billing` token.",
9106
+ "- Optional `home`, `work`, `mobile`, `fax` or `pager` token (for fields supporting it).",
9107
+ "- Field name",
9108
+ "- Optional `webauthn` token."
9109
+ ].join("\n");
9110
+ case 3 /* InvalidToken */:
9111
+ return `\`"${context.token}"\` is not a valid autocomplete token or field name`;
9112
+ case 4 /* InvalidCombination */:
9113
+ return `\`"${context.second}"\` cannot be combined with \`"${context.first}"\``;
9114
+ case 5 /* MissingField */:
9115
+ return "Autocomplete attribute is missing field name";
9116
+ }
9117
+ }
9118
+ class ValidAutocomplete extends Rule {
9119
+ documentation(context) {
9120
+ return {
9121
+ description: getMarkdownMessage(context),
9122
+ url: "https://html-validate.org/rules/valid-autocomplete.html"
9123
+ };
9124
+ }
9125
+ setup() {
9126
+ this.on("dom:ready", (event) => {
9127
+ const { document } = event;
9128
+ const elements = document.querySelectorAll("[autocomplete]");
9129
+ for (const element of elements) {
9130
+ const autocomplete = element.getAttribute("autocomplete");
9131
+ if (autocomplete.value === null || autocomplete.value instanceof DynamicValue) {
9132
+ continue;
9133
+ }
9134
+ const location = autocomplete.valueLocation;
9135
+ const value = autocomplete.value.toLowerCase();
9136
+ const tokens = new DOMTokenList(value, location);
9137
+ if (tokens.length === 0) {
9138
+ continue;
9139
+ }
9140
+ this.validate(element, value, tokens, autocomplete.keyLocation, location);
9141
+ }
9142
+ });
9143
+ }
9144
+ validate(node, value, tokens, keyLocation, valueLocation) {
9145
+ switch (node.tagName) {
9146
+ case "form":
9147
+ this.validateFormAutocomplete(node, value, valueLocation);
9148
+ break;
9149
+ case "input":
9150
+ case "textarea":
9151
+ case "select":
9152
+ this.validateControlAutocomplete(node, tokens, keyLocation);
9153
+ break;
9154
+ }
9155
+ }
9156
+ validateControlAutocomplete(node, tokens, keyLocation) {
9157
+ const type = node.getAttributeValue("type") ?? "text";
9158
+ const mantle = type !== "hidden" ? "expectation" : "anchor";
9159
+ if (isDisallowedType(node, type)) {
9160
+ const context = {
9161
+ msg: 0 /* InvalidAttribute */,
9162
+ what: `<input type="${type}">`
9163
+ };
9164
+ this.report({
9165
+ node,
9166
+ message: getTerminalMessage(context),
9167
+ location: keyLocation,
9168
+ context
9169
+ });
9170
+ return;
9171
+ }
9172
+ if (tokens.includes("on") || tokens.includes("off")) {
9173
+ this.validateOnOff(node, mantle, tokens);
9174
+ return;
9175
+ }
9176
+ this.validateTokens(node, tokens, keyLocation);
9177
+ }
9178
+ validateFormAutocomplete(node, value, location) {
9179
+ const trimmed = value.trim();
9180
+ if (["on", "off"].includes(trimmed)) {
9181
+ return;
9182
+ }
9183
+ const context = {
9184
+ msg: 1 /* InvalidValue */,
9185
+ type: "form",
9186
+ value: trimmed,
9187
+ what: "<form>"
9188
+ };
9189
+ this.report({
9190
+ node,
9191
+ message: getTerminalMessage(context),
9192
+ location,
9193
+ context
9194
+ });
9195
+ }
9196
+ validateOnOff(node, mantle, tokens) {
9197
+ const index = tokens.findIndex((it) => it === "on" || it === "off");
9198
+ const value = tokens.item(index);
9199
+ const location = tokens.location(index);
9200
+ if (tokens.length > 1) {
9201
+ const context = {
9202
+ msg: 4 /* InvalidCombination */,
9203
+ /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
9204
+ first: tokens.item(index > 0 ? 0 : 1),
9205
+ second: value
9206
+ };
9207
+ this.report({
9208
+ node,
9209
+ message: getTerminalMessage(context),
9210
+ location,
9211
+ context
9212
+ });
9213
+ }
9214
+ switch (mantle) {
9215
+ case "expectation":
9216
+ return;
9217
+ case "anchor": {
9218
+ const context = {
9219
+ msg: 1 /* InvalidValue */,
9220
+ type: "hidden",
9221
+ value,
9222
+ what: `<input type="hidden">`
9223
+ };
9224
+ this.report({
9225
+ node,
9226
+ message: getTerminalMessage(context),
9227
+ location: tokens.location(0),
9228
+ context
9229
+ });
9230
+ }
9231
+ }
9232
+ }
9233
+ validateTokens(node, tokens, keyLocation) {
9234
+ const order = [];
9235
+ for (const { item, location } of tokens.iterator()) {
9236
+ const tokenType = matchToken(item);
9237
+ if (tokenType) {
9238
+ order.push(tokenType);
9239
+ } else {
9240
+ const context = {
9241
+ msg: 3 /* InvalidToken */,
9242
+ token: item
9243
+ };
9244
+ this.report({
9245
+ node,
9246
+ message: getTerminalMessage(context),
9247
+ location,
9248
+ context
9249
+ });
9250
+ return;
9251
+ }
9252
+ }
9253
+ const fieldTokens = order.map((it) => it === "field1" || it === "field2");
9254
+ this.validateFieldPresence(node, tokens, fieldTokens, keyLocation);
9255
+ this.validateContact(node, tokens, order);
9256
+ this.validateOrder(node, tokens, order);
9257
+ this.validateControlGroup(node, tokens, fieldTokens);
9258
+ }
9259
+ /**
9260
+ * Ensure that exactly one field name is present from the two field lists.
9261
+ */
9262
+ validateFieldPresence(node, tokens, fieldTokens, keyLocation) {
9263
+ const numFields = fieldTokens.filter(Boolean).length;
9264
+ if (numFields === 0) {
9265
+ const context = {
9266
+ msg: 5 /* MissingField */
9267
+ };
9268
+ this.report({
9269
+ node,
9270
+ message: getTerminalMessage(context),
9271
+ location: keyLocation,
9272
+ context
9273
+ });
9274
+ } else if (numFields > 1) {
9275
+ const a = fieldTokens.indexOf(true);
9276
+ const b = fieldTokens.lastIndexOf(true);
9277
+ const context = {
9278
+ msg: 4 /* InvalidCombination */,
9279
+ /* eslint-disable @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
9280
+ first: tokens.item(a),
9281
+ second: tokens.item(b)
9282
+ /* eslint-enable @typescript-eslint/no-non-null-assertion */
9283
+ };
9284
+ this.report({
9285
+ node,
9286
+ message: getTerminalMessage(context),
9287
+ location: tokens.location(b),
9288
+ context
9289
+ });
9290
+ }
9291
+ }
9292
+ /**
9293
+ * Ensure contact token is only used with field names from the second list.
9294
+ */
9295
+ validateContact(node, tokens, order) {
9296
+ if (order.includes("contact") && order.includes("field1")) {
9297
+ const a = order.indexOf("field1");
9298
+ const b = order.indexOf("contact");
9299
+ const context = {
9300
+ msg: 4 /* InvalidCombination */,
9301
+ /* eslint-disable @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
9302
+ first: tokens.item(a),
9303
+ second: tokens.item(b)
9304
+ /* eslint-enable @typescript-eslint/no-non-null-assertion */
9305
+ };
9306
+ this.report({
9307
+ node,
9308
+ message: getTerminalMessage(context),
9309
+ location: tokens.location(b),
9310
+ context
9311
+ });
9312
+ }
9313
+ }
9314
+ validateOrder(node, tokens, order) {
9315
+ const indicies = order.map((it) => expectedOrder.indexOf(it));
9316
+ for (let i = 0; i < indicies.length - 1; i++) {
9317
+ if (indicies[0] > indicies[i + 1]) {
9318
+ const context = {
9319
+ msg: 2 /* InvalidOrder */,
9320
+ /* eslint-disable @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
9321
+ first: tokens.item(i),
9322
+ second: tokens.item(i + 1)
9323
+ /* eslint-enable @typescript-eslint/no-non-null-assertion */
9324
+ };
9325
+ this.report({
9326
+ node,
9327
+ message: getTerminalMessage(context),
9328
+ location: tokens.location(i + 1),
9329
+ context
9330
+ });
9331
+ }
9332
+ }
9333
+ }
9334
+ validateControlGroup(node, tokens, fieldTokens) {
9335
+ const numFields = fieldTokens.filter(Boolean).length;
9336
+ if (numFields === 0) {
9337
+ return;
9338
+ }
9339
+ if (!node.is("input")) {
9340
+ return;
9341
+ }
9342
+ const attr = node.getAttribute("type");
9343
+ const type = (attr == null ? void 0 : attr.value) ?? "text";
9344
+ if (type instanceof DynamicValue) {
9345
+ return;
9346
+ }
9347
+ const controlGroups = getControlGroups(type);
9348
+ const fieldIndex = fieldTokens.indexOf(true);
9349
+ const fieldToken = tokens.item(fieldIndex);
9350
+ const fieldGroup = fieldNameGroup[fieldToken];
9351
+ if (!controlGroups.includes(fieldGroup)) {
9352
+ const context = {
9353
+ msg: 1 /* InvalidValue */,
9354
+ type,
9355
+ value: fieldToken,
9356
+ what: `<input type="${type}">`
9357
+ };
9358
+ this.report({
9359
+ node,
9360
+ message: getTerminalMessage(context),
9361
+ location: tokens.location(fieldIndex),
9362
+ context
9363
+ });
9364
+ }
9365
+ }
9366
+ }
9367
+
9368
+ const defaults$3 = {
9369
+ relaxed: false
9370
+ };
9371
+ class ValidID extends Rule {
9372
+ constructor(options) {
9373
+ super({ ...defaults$3, ...options });
9388
9374
  }
9389
- if (matchHint(token)) {
9390
- return "hint";
9375
+ static schema() {
9376
+ return {
9377
+ relaxed: {
9378
+ type: "boolean"
9379
+ }
9380
+ };
9391
9381
  }
9392
- if (matchFieldNames1(token)) {
9393
- return "field1";
9382
+ documentation(context) {
9383
+ const { relaxed } = this.options;
9384
+ const message = this.messages[context].replace("id", "ID").replace(/^(.)/, (m) => m.toUpperCase());
9385
+ const relaxedDescription = relaxed ? [] : [
9386
+ " - ID must begin with a letter",
9387
+ " - ID must only contain letters, digits, `-` and `_`"
9388
+ ];
9389
+ return {
9390
+ description: [
9391
+ `${message}.`,
9392
+ "",
9393
+ "Under the current configuration the following rules are applied:",
9394
+ "",
9395
+ " - ID must not be empty",
9396
+ " - ID must not contain any whitespace characters",
9397
+ ...relaxedDescription
9398
+ ].join("\n"),
9399
+ url: "https://html-validate.org/rules/valid-id.html"
9400
+ };
9394
9401
  }
9395
- if (matchFieldNames2(token)) {
9396
- return "field2";
9402
+ setup() {
9403
+ this.on("attr", this.isRelevant, (event) => {
9404
+ const { value } = event;
9405
+ if (value === null || value instanceof DynamicValue) {
9406
+ return;
9407
+ }
9408
+ if (value === "") {
9409
+ const context = 1 /* EMPTY */;
9410
+ this.report(event.target, this.messages[context], event.location, context);
9411
+ return;
9412
+ }
9413
+ if (value.match(/\s/)) {
9414
+ const context = 2 /* WHITESPACE */;
9415
+ this.report(event.target, this.messages[context], event.valueLocation, context);
9416
+ return;
9417
+ }
9418
+ const { relaxed } = this.options;
9419
+ if (relaxed) {
9420
+ return;
9421
+ }
9422
+ if (value.match(/^[^\p{L}]/u)) {
9423
+ const context = 3 /* LEADING_CHARACTER */;
9424
+ this.report(event.target, this.messages[context], event.valueLocation, context);
9425
+ return;
9426
+ }
9427
+ if (value.match(/[^\p{L}\p{N}_-]/u)) {
9428
+ const context = 4 /* DISALLOWED_CHARACTER */;
9429
+ this.report(event.target, this.messages[context], event.valueLocation, context);
9430
+ }
9431
+ });
9397
9432
  }
9398
- if (matchContact(token)) {
9399
- return "contact";
9433
+ get messages() {
9434
+ return {
9435
+ [1 /* EMPTY */]: "element id must not be empty",
9436
+ [2 /* WHITESPACE */]: "element id must not contain whitespace",
9437
+ [3 /* LEADING_CHARACTER */]: "element id must begin with a letter",
9438
+ [4 /* DISALLOWED_CHARACTER */]: "element id must only contain letters, digits, dash and underscore characters"
9439
+ };
9400
9440
  }
9401
- if (matchWebauthn(token)) {
9402
- return "webauthn";
9441
+ isRelevant(event) {
9442
+ return event.key === "id";
9403
9443
  }
9404
- return null;
9405
9444
  }
9406
- function getControlGroups(type) {
9407
- const allGroups = [
9408
- "text",
9409
- "multiline",
9410
- "password",
9411
- "url",
9412
- "username",
9413
- "tel",
9414
- "numeric",
9415
- "month",
9416
- "date"
9417
- ];
9418
- const mapping = {
9419
- hidden: allGroups,
9420
- text: allGroups.filter((it) => it !== "multiline"),
9421
- search: allGroups.filter((it) => it !== "multiline"),
9422
- password: ["password"],
9423
- url: ["url"],
9424
- email: ["username"],
9425
- tel: ["tel"],
9426
- number: ["numeric"],
9427
- month: ["month"],
9428
- date: ["date"]
9429
- };
9430
- const groups = mapping[type];
9431
- if (groups) {
9432
- return groups;
9445
+
9446
+ class VoidContent extends Rule {
9447
+ documentation(tagName) {
9448
+ const doc = {
9449
+ description: "HTML void elements cannot have any content and must not have content or end tag.",
9450
+ url: "https://html-validate.org/rules/void-content.html"
9451
+ };
9452
+ if (tagName) {
9453
+ doc.description = `<${tagName}> is a void element and must not have content or end tag.`;
9454
+ }
9455
+ return doc;
9433
9456
  }
9434
- return [];
9435
- }
9436
- function isDisallowedType(node, type) {
9437
- if (!node.is("input")) {
9438
- return false;
9457
+ setup() {
9458
+ this.on("tag:end", (event) => {
9459
+ const node = event.target;
9460
+ if (!node) {
9461
+ return;
9462
+ }
9463
+ if (!node.voidElement) {
9464
+ return;
9465
+ }
9466
+ if (node.closed === NodeClosed.EndTag) {
9467
+ this.report(
9468
+ null,
9469
+ `End tag for <${node.tagName}> must be omitted`,
9470
+ node.location,
9471
+ node.tagName
9472
+ );
9473
+ }
9474
+ });
9439
9475
  }
9440
- return disallowedInputTypes.includes(type);
9441
9476
  }
9442
- function getTerminalMessage(context) {
9443
- switch (context.msg) {
9444
- case 0 /* InvalidAttribute */:
9445
- return "autocomplete attribute cannot be used on {{ what }}";
9446
- case 1 /* InvalidValue */:
9447
- return '"{{ value }}" cannot be used on {{ what }}';
9448
- case 2 /* InvalidOrder */:
9449
- return '"{{ second }}" must appear before "{{ first }}"';
9450
- case 3 /* InvalidToken */:
9451
- return '"{{ token }}" is not a valid autocomplete token or field name';
9452
- case 4 /* InvalidCombination */:
9453
- return '"{{ second }}" cannot be combined with "{{ first }}"';
9454
- case 5 /* MissingField */:
9455
- return "autocomplete attribute is missing field name";
9477
+
9478
+ const defaults$2 = {
9479
+ style: "omit"
9480
+ };
9481
+ class VoidStyle extends Rule {
9482
+ constructor(options) {
9483
+ super({ ...defaults$2, ...options });
9484
+ this.style = parseStyle(this.options.style);
9456
9485
  }
9457
- }
9458
- function getMarkdownMessage(context) {
9459
- switch (context.msg) {
9460
- case 0 /* InvalidAttribute */:
9461
- return [
9462
- `\`autocomplete\` attribute cannot be used on \`${context.what}\``,
9463
- "",
9464
- "The following input types cannot use the `autocomplete` attribute:",
9465
- "",
9466
- ...disallowedInputTypes.map((it) => `- \`${it}\``)
9467
- ].join("\n");
9468
- case 1 /* InvalidValue */: {
9469
- const message = `\`"${context.value}"\` cannot be used on \`${context.what}\``;
9470
- if (context.type === "form") {
9471
- return [
9472
- message,
9473
- "",
9474
- 'The `<form>` element can only use the values `"on"` and `"off"`.'
9475
- ].join("\n");
9476
- }
9477
- if (context.type === "hidden") {
9478
- return [
9479
- message,
9480
- "",
9481
- '`<input type="hidden">` cannot use the values `"on"` and `"off"`.'
9482
- ].join("\n");
9483
- }
9484
- const controlGroups = getControlGroups(context.type);
9485
- const currentGroup = fieldNameGroup[context.value];
9486
- return [
9487
- message,
9488
- "",
9489
- `\`${context.what}\` allows autocomplete fields from the following group${controlGroups.length > 1 ? "s" : ""}:`,
9490
- "",
9491
- ...controlGroups.map((it) => `- ${it}`),
9492
- "",
9493
- `The field \`"${context.value}"\` belongs to the group /${currentGroup}/ which cannot be used with this input type.`
9494
- ].join("\n");
9495
- }
9496
- case 2 /* InvalidOrder */:
9497
- return [
9498
- `\`"${context.second}"\` must appear before \`"${context.first}"\``,
9499
- "",
9500
- "The autocomplete tokens must appear in the following order:",
9501
- "",
9502
- "- Optional section name (`section-` prefix).",
9503
- "- Optional `shipping` or `billing` token.",
9504
- "- Optional `home`, `work`, `mobile`, `fax` or `pager` token (for fields supporting it).",
9505
- "- Field name",
9506
- "- Optional `webauthn` token."
9507
- ].join("\n");
9508
- case 3 /* InvalidToken */:
9509
- return `\`"${context.token}"\` is not a valid autocomplete token or field name`;
9510
- case 4 /* InvalidCombination */:
9511
- return `\`"${context.second}"\` cannot be combined with \`"${context.first}"\``;
9512
- case 5 /* MissingField */:
9513
- return "Autocomplete attribute is missing field name";
9486
+ static schema() {
9487
+ return {
9488
+ style: {
9489
+ enum: ["omit", "selfclose", "selfclosing"],
9490
+ type: "string"
9491
+ }
9492
+ };
9514
9493
  }
9515
- }
9516
- class ValidAutocomplete extends Rule {
9517
9494
  documentation(context) {
9495
+ const [desc, end] = styleDescription(context.style);
9518
9496
  return {
9519
- description: getMarkdownMessage(context),
9520
- url: "https://html-validate.org/rules/valid-autocomplete.html"
9497
+ description: `The current configuration requires void elements to ${desc}, use <${context.tagName}${end}> instead.`,
9498
+ url: "https://html-validate.org/rules/void-style.html"
9521
9499
  };
9522
9500
  }
9523
9501
  setup() {
9524
- this.on("dom:ready", (event) => {
9525
- const { document } = event;
9526
- const elements = document.querySelectorAll("[autocomplete]");
9527
- for (const element of elements) {
9528
- const autocomplete = element.getAttribute("autocomplete");
9529
- if (autocomplete.value === null || autocomplete.value instanceof DynamicValue) {
9530
- continue;
9531
- }
9532
- const location = autocomplete.valueLocation;
9533
- const value = autocomplete.value.toLowerCase();
9534
- const tokens = new DOMTokenList(value, location);
9535
- if (tokens.length === 0) {
9536
- continue;
9537
- }
9538
- this.validate(element, value, tokens, autocomplete.keyLocation, location);
9502
+ this.on("tag:end", (event) => {
9503
+ const active = event.previous;
9504
+ if (active.meta) {
9505
+ this.validateActive(active);
9539
9506
  }
9540
9507
  });
9541
9508
  }
9542
- validate(node, value, tokens, keyLocation, valueLocation) {
9543
- switch (node.tagName) {
9544
- case "form":
9545
- this.validateFormAutocomplete(node, value, valueLocation);
9546
- break;
9547
- case "input":
9548
- case "textarea":
9549
- case "select":
9550
- this.validateControlAutocomplete(node, tokens, keyLocation);
9551
- break;
9509
+ validateActive(node) {
9510
+ if (!node.voidElement) {
9511
+ return;
9552
9512
  }
9553
- }
9554
- validateControlAutocomplete(node, tokens, keyLocation) {
9555
- const type = node.getAttributeValue("type") ?? "text";
9556
- const mantle = type !== "hidden" ? "expectation" : "anchor";
9557
- if (isDisallowedType(node, type)) {
9558
- const context = {
9559
- msg: 0 /* InvalidAttribute */,
9560
- what: `<input type="${type}">`
9561
- };
9562
- this.report({
9513
+ if (this.shouldBeOmitted(node)) {
9514
+ this.reportError(
9563
9515
  node,
9564
- message: getTerminalMessage(context),
9565
- location: keyLocation,
9566
- context
9567
- });
9568
- return;
9516
+ `Expected omitted end tag <${node.tagName}> instead of self-closing element <${node.tagName}/>`
9517
+ );
9569
9518
  }
9570
- if (tokens.includes("on") || tokens.includes("off")) {
9571
- this.validateOnOff(node, mantle, tokens);
9572
- return;
9519
+ if (this.shouldBeSelfClosed(node)) {
9520
+ this.reportError(
9521
+ node,
9522
+ `Expected self-closing element <${node.tagName}/> instead of omitted end-tag <${node.tagName}>`
9523
+ );
9573
9524
  }
9574
- this.validateTokens(node, tokens, keyLocation);
9575
9525
  }
9576
- validateFormAutocomplete(node, value, location) {
9577
- const trimmed = value.trim();
9578
- if (["on", "off"].includes(trimmed)) {
9579
- return;
9580
- }
9526
+ reportError(node, message) {
9581
9527
  const context = {
9582
- msg: 1 /* InvalidValue */,
9583
- type: "form",
9584
- value: trimmed,
9585
- what: "<form>"
9528
+ style: this.style,
9529
+ tagName: node.tagName
9586
9530
  };
9587
- this.report({
9588
- node,
9589
- message: getTerminalMessage(context),
9590
- location,
9591
- context
9592
- });
9531
+ super.report(node, message, null, context);
9593
9532
  }
9594
- validateOnOff(node, mantle, tokens) {
9595
- const index = tokens.findIndex((it) => it === "on" || it === "off");
9596
- const value = tokens.item(index);
9597
- const location = tokens.location(index);
9598
- if (tokens.length > 1) {
9599
- const context = {
9600
- msg: 4 /* InvalidCombination */,
9601
- /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
9602
- first: tokens.item(index > 0 ? 0 : 1),
9603
- second: value
9604
- };
9605
- this.report({
9606
- node,
9607
- message: getTerminalMessage(context),
9608
- location,
9609
- context
9610
- });
9611
- }
9612
- switch (mantle) {
9613
- case "expectation":
9614
- return;
9615
- case "anchor": {
9616
- const context = {
9617
- msg: 1 /* InvalidValue */,
9618
- type: "hidden",
9619
- value,
9620
- what: `<input type="hidden">`
9621
- };
9622
- this.report({
9623
- node,
9624
- message: getTerminalMessage(context),
9625
- location: tokens.location(0),
9626
- context
9627
- });
9628
- }
9629
- }
9533
+ shouldBeOmitted(node) {
9534
+ return this.style === 1 /* AlwaysOmit */ && node.closed === NodeClosed.VoidSelfClosed;
9630
9535
  }
9631
- validateTokens(node, tokens, keyLocation) {
9632
- const order = [];
9633
- for (const { item, location } of tokens.iterator()) {
9634
- const tokenType = matchToken(item);
9635
- if (tokenType) {
9636
- order.push(tokenType);
9637
- } else {
9638
- const context = {
9639
- msg: 3 /* InvalidToken */,
9640
- token: item
9641
- };
9642
- this.report({
9643
- node,
9644
- message: getTerminalMessage(context),
9645
- location,
9646
- context
9647
- });
9648
- return;
9536
+ shouldBeSelfClosed(node) {
9537
+ return this.style === 2 /* AlwaysSelfclose */ && node.closed === NodeClosed.VoidOmitted;
9538
+ }
9539
+ }
9540
+ function parseStyle(name) {
9541
+ switch (name) {
9542
+ case "omit":
9543
+ return 1 /* AlwaysOmit */;
9544
+ case "selfclose":
9545
+ case "selfclosing":
9546
+ return 2 /* AlwaysSelfclose */;
9547
+ default:
9548
+ throw new Error(`Invalid style "${name}" for "void-style" rule`);
9549
+ }
9550
+ }
9551
+ function styleDescription(style) {
9552
+ switch (style) {
9553
+ case 1 /* AlwaysOmit */:
9554
+ return ["omit end tag", ""];
9555
+ case 2 /* AlwaysSelfclose */:
9556
+ return ["be self-closed", "/"];
9557
+ default:
9558
+ throw new Error(`Unknown style`);
9559
+ }
9560
+ }
9561
+
9562
+ class H30 extends Rule {
9563
+ documentation() {
9564
+ return {
9565
+ description: "WCAG 2.1 requires each `<a href>` anchor link to have a text describing the purpose of the link using either plain text or an `<img>` with the `alt` attribute set.",
9566
+ url: "https://html-validate.org/rules/wcag/h30.html"
9567
+ };
9568
+ }
9569
+ setup() {
9570
+ this.on("dom:ready", (event) => {
9571
+ const links = event.document.getElementsByTagName("a");
9572
+ for (const link of links) {
9573
+ if (!link.hasAttribute("href")) {
9574
+ continue;
9575
+ }
9576
+ if (!inAccessibilityTree(link)) {
9577
+ continue;
9578
+ }
9579
+ const textClassification = classifyNodeText(link, { ignoreHiddenRoot: true });
9580
+ if (textClassification !== TextClassification.EMPTY_TEXT) {
9581
+ continue;
9582
+ }
9583
+ const images = link.querySelectorAll("img");
9584
+ if (images.some((image) => hasAltText(image))) {
9585
+ continue;
9586
+ }
9587
+ const labels = link.querySelectorAll("[aria-label]");
9588
+ if (hasAriaLabel(link) || labels.some((cur) => hasAriaLabel(cur))) {
9589
+ continue;
9590
+ }
9591
+ this.report(link, "Anchor link must have a text describing its purpose");
9649
9592
  }
9650
- }
9651
- const fieldTokens = order.map((it) => it === "field1" || it === "field2");
9652
- this.validateFieldPresence(node, tokens, fieldTokens, keyLocation);
9653
- this.validateContact(node, tokens, order);
9654
- this.validateOrder(node, tokens, order);
9655
- this.validateControlGroup(node, tokens, fieldTokens);
9593
+ });
9656
9594
  }
9657
- /**
9658
- * Ensure that exactly one field name is present from the two field lists.
9659
- */
9660
- validateFieldPresence(node, tokens, fieldTokens, keyLocation) {
9661
- const numFields = fieldTokens.filter(Boolean).length;
9662
- if (numFields === 0) {
9663
- const context = {
9664
- msg: 5 /* MissingField */
9665
- };
9666
- this.report({
9667
- node,
9668
- message: getTerminalMessage(context),
9669
- location: keyLocation,
9670
- context
9671
- });
9672
- } else if (numFields > 1) {
9673
- const a = fieldTokens.indexOf(true);
9674
- const b = fieldTokens.lastIndexOf(true);
9675
- const context = {
9676
- msg: 4 /* InvalidCombination */,
9677
- /* eslint-disable @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
9678
- first: tokens.item(a),
9679
- second: tokens.item(b)
9680
- /* eslint-enable @typescript-eslint/no-non-null-assertion */
9681
- };
9682
- this.report({
9683
- node,
9684
- message: getTerminalMessage(context),
9685
- location: tokens.location(b),
9686
- context
9687
- });
9688
- }
9595
+ }
9596
+
9597
+ class H32 extends Rule {
9598
+ documentation() {
9599
+ return {
9600
+ description: "WCAG 2.1 requires each `<form>` element to have at least one submit button.",
9601
+ url: "https://html-validate.org/rules/wcag/h32.html"
9602
+ };
9603
+ }
9604
+ setup() {
9605
+ const formTags = this.getTagsWithProperty("form");
9606
+ const formSelector = formTags.join(",");
9607
+ this.on("dom:ready", (event) => {
9608
+ const { document } = event;
9609
+ const forms = document.querySelectorAll(formSelector);
9610
+ for (const form of forms) {
9611
+ if (hasNestedSubmit(form)) {
9612
+ continue;
9613
+ }
9614
+ if (hasAssociatedSubmit(document, form)) {
9615
+ continue;
9616
+ }
9617
+ this.report(form, `<${form.tagName}> element must have a submit button`);
9618
+ }
9619
+ });
9689
9620
  }
9690
- /**
9691
- * Ensure contact token is only used with field names from the second list.
9692
- */
9693
- validateContact(node, tokens, order) {
9694
- if (order.includes("contact") && order.includes("field1")) {
9695
- const a = order.indexOf("field1");
9696
- const b = order.indexOf("contact");
9697
- const context = {
9698
- msg: 4 /* InvalidCombination */,
9699
- /* eslint-disable @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
9700
- first: tokens.item(a),
9701
- second: tokens.item(b)
9702
- /* eslint-enable @typescript-eslint/no-non-null-assertion */
9703
- };
9704
- this.report({
9705
- node,
9706
- message: getTerminalMessage(context),
9707
- location: tokens.location(b),
9708
- context
9709
- });
9710
- }
9621
+ }
9622
+ function isSubmit(node) {
9623
+ const type = node.getAttribute("type");
9624
+ return Boolean(!type || type.valueMatches(/submit|image/));
9625
+ }
9626
+ function isAssociated(id, node) {
9627
+ const form = node.getAttribute("form");
9628
+ return Boolean(form == null ? void 0 : form.valueMatches(id, true));
9629
+ }
9630
+ function hasNestedSubmit(form) {
9631
+ const matches = form.querySelectorAll("button,input").filter(isSubmit).filter((node) => !node.hasAttribute("form"));
9632
+ return matches.length > 0;
9633
+ }
9634
+ function hasAssociatedSubmit(document, form) {
9635
+ const { id } = form;
9636
+ if (!id) {
9637
+ return false;
9711
9638
  }
9712
- validateOrder(node, tokens, order) {
9713
- const indicies = order.map((it) => expectedOrder.indexOf(it));
9714
- for (let i = 0; i < indicies.length - 1; i++) {
9715
- if (indicies[0] > indicies[i + 1]) {
9716
- const context = {
9717
- msg: 2 /* InvalidOrder */,
9718
- /* eslint-disable @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
9719
- first: tokens.item(i),
9720
- second: tokens.item(i + 1)
9721
- /* eslint-enable @typescript-eslint/no-non-null-assertion */
9722
- };
9639
+ const matches = document.querySelectorAll("button[form],input[form]").filter(isSubmit).filter((node) => isAssociated(id, node));
9640
+ return matches.length > 0;
9641
+ }
9642
+
9643
+ class H36 extends Rule {
9644
+ documentation() {
9645
+ return {
9646
+ description: [
9647
+ "WCAG 2.1 requires all images used as submit buttons to have a non-empty textual description using the `alt` attribute.",
9648
+ 'The alt text cannot be empty (`alt=""`).'
9649
+ ].join("\n"),
9650
+ url: "https://html-validate.org/rules/wcag/h36.html"
9651
+ };
9652
+ }
9653
+ setup() {
9654
+ this.on("tag:end", (event) => {
9655
+ const node = event.previous;
9656
+ if (node.tagName !== "input")
9657
+ return;
9658
+ if (node.getAttributeValue("type") !== "image") {
9659
+ return;
9660
+ }
9661
+ if (!inAccessibilityTree(node)) {
9662
+ return;
9663
+ }
9664
+ if (!hasAltText(node)) {
9665
+ const message = "image used as submit button must have non-empty alt text";
9666
+ const alt = node.getAttribute("alt");
9723
9667
  this.report({
9724
9668
  node,
9725
- message: getTerminalMessage(context),
9726
- location: tokens.location(i + 1),
9727
- context
9669
+ message,
9670
+ location: alt ? alt.keyLocation : node.location
9728
9671
  });
9729
9672
  }
9730
- }
9731
- }
9732
- validateControlGroup(node, tokens, fieldTokens) {
9733
- const numFields = fieldTokens.filter(Boolean).length;
9734
- if (numFields === 0) {
9735
- return;
9736
- }
9737
- if (!node.is("input")) {
9738
- return;
9739
- }
9740
- const attr = node.getAttribute("type");
9741
- const type = (attr == null ? void 0 : attr.value) ?? "text";
9742
- if (type instanceof DynamicValue) {
9743
- return;
9744
- }
9745
- const controlGroups = getControlGroups(type);
9746
- const fieldIndex = fieldTokens.indexOf(true);
9747
- const fieldToken = tokens.item(fieldIndex);
9748
- const fieldGroup = fieldNameGroup[fieldToken];
9749
- if (!controlGroups.includes(fieldGroup)) {
9750
- const context = {
9751
- msg: 1 /* InvalidValue */,
9752
- type,
9753
- value: fieldToken,
9754
- what: `<input type="${type}">`
9755
- };
9756
- this.report({
9757
- node,
9758
- message: getTerminalMessage(context),
9759
- location: tokens.location(fieldIndex),
9760
- context
9761
- });
9762
- }
9673
+ });
9763
9674
  }
9764
9675
  }
9765
9676
 
9766
- const defaults$3 = {
9767
- relaxed: false
9677
+ const defaults$1 = {
9678
+ allowEmpty: true,
9679
+ alias: []
9768
9680
  };
9769
- class ValidID extends Rule {
9681
+ class H37 extends Rule {
9770
9682
  constructor(options) {
9771
- super({ ...defaults$3, ...options });
9683
+ super({ ...defaults$1, ...options });
9684
+ if (!Array.isArray(this.options.alias)) {
9685
+ this.options.alias = [this.options.alias];
9686
+ }
9772
9687
  }
9773
9688
  static schema() {
9774
9689
  return {
9775
- relaxed: {
9690
+ alias: {
9691
+ anyOf: [
9692
+ {
9693
+ items: {
9694
+ type: "string"
9695
+ },
9696
+ type: "array"
9697
+ },
9698
+ {
9699
+ type: "string"
9700
+ }
9701
+ ]
9702
+ },
9703
+ allowEmpty: {
9776
9704
  type: "boolean"
9777
9705
  }
9778
9706
  };
9779
9707
  }
9780
- documentation(context) {
9781
- const { relaxed } = this.options;
9782
- const message = this.messages[context].replace("id", "ID").replace(/^(.)/, (m) => m.toUpperCase());
9783
- const relaxedDescription = relaxed ? [] : [
9784
- " - ID must begin with a letter",
9785
- " - ID must only contain letters, digits, `-` and `_`"
9786
- ];
9708
+ documentation() {
9787
9709
  return {
9788
- description: [
9789
- `${message}.`,
9790
- "",
9791
- "Under the current configuration the following rules are applied:",
9792
- "",
9793
- " - ID must not be empty",
9794
- " - ID must not contain any whitespace characters",
9795
- ...relaxedDescription
9796
- ].join("\n"),
9797
- url: "https://html-validate.org/rules/valid-id.html"
9710
+ description: "Both HTML5 and WCAG 2.0 requires images to have a alternative text for each image.",
9711
+ url: "https://html-validate.org/rules/wcag/h37.html"
9798
9712
  };
9799
9713
  }
9800
9714
  setup() {
9801
- this.on("attr", this.isRelevant, (event) => {
9802
- const { value } = event;
9803
- if (value === null || value instanceof DynamicValue) {
9804
- return;
9715
+ this.on("dom:ready", (event) => {
9716
+ const { document } = event;
9717
+ const nodes = document.querySelectorAll("img");
9718
+ for (const node of nodes) {
9719
+ this.validateNode(node);
9805
9720
  }
9806
- if (value === "") {
9807
- const context = 1 /* EMPTY */;
9808
- this.report(event.target, this.messages[context], event.location, context);
9721
+ });
9722
+ }
9723
+ validateNode(node) {
9724
+ if (!inAccessibilityTree(node)) {
9725
+ return;
9726
+ }
9727
+ if (Boolean(node.getAttributeValue("alt")) || Boolean(node.hasAttribute("alt") && this.options.allowEmpty)) {
9728
+ return;
9729
+ }
9730
+ for (const attr of this.options.alias) {
9731
+ if (node.getAttribute(attr)) {
9809
9732
  return;
9810
9733
  }
9811
- if (value.match(/\s/)) {
9812
- const context = 2 /* WHITESPACE */;
9813
- this.report(event.target, this.messages[context], event.valueLocation, context);
9734
+ }
9735
+ const tag = node.annotatedName;
9736
+ if (node.hasAttribute("alt")) {
9737
+ const attr = node.getAttribute("alt");
9738
+ this.report(node, `${tag} cannot have empty "alt" attribute`, attr.keyLocation);
9739
+ } else {
9740
+ this.report(node, `${tag} is missing required "alt" attribute`, node.location);
9741
+ }
9742
+ }
9743
+ }
9744
+
9745
+ var _a;
9746
+ const { enum: validScopes } = (_a = elements.html5.th.attributes) == null ? void 0 : _a.scope;
9747
+ const joinedScopes = utils_naturalJoin.naturalJoin(validScopes);
9748
+ class H63 extends Rule {
9749
+ documentation() {
9750
+ return {
9751
+ description: "H63: Using the scope attribute to associate header cells and data cells in data tables",
9752
+ url: "https://html-validate.org/rules/wcag/h63.html"
9753
+ };
9754
+ }
9755
+ setup() {
9756
+ this.on("tag:ready", (event) => {
9757
+ const node = event.target;
9758
+ if (node.tagName !== "th") {
9814
9759
  return;
9815
9760
  }
9816
- const { relaxed } = this.options;
9817
- if (relaxed) {
9761
+ const scope = node.getAttribute("scope");
9762
+ const value = scope == null ? void 0 : scope.value;
9763
+ if (value instanceof DynamicValue) {
9818
9764
  return;
9819
9765
  }
9820
- if (value.match(/^[^\p{L}]/u)) {
9821
- const context = 3 /* LEADING_CHARACTER */;
9822
- this.report(event.target, this.messages[context], event.valueLocation, context);
9766
+ if (value && validScopes.includes(value)) {
9823
9767
  return;
9824
9768
  }
9825
- if (value.match(/[^\p{L}\p{N}_-]/u)) {
9826
- const context = 4 /* DISALLOWED_CHARACTER */;
9827
- this.report(event.target, this.messages[context], event.valueLocation, context);
9828
- }
9769
+ const message = `<th> element must have a valid scope attribute: ${joinedScopes}`;
9770
+ const location = (scope == null ? void 0 : scope.valueLocation) ?? (scope == null ? void 0 : scope.keyLocation) ?? node.location;
9771
+ this.report(node, message, location);
9829
9772
  });
9830
9773
  }
9831
- get messages() {
9832
- return {
9833
- [1 /* EMPTY */]: "element id must not be empty",
9834
- [2 /* WHITESPACE */]: "element id must not contain whitespace",
9835
- [3 /* LEADING_CHARACTER */]: "element id must begin with a letter",
9836
- [4 /* DISALLOWED_CHARACTER */]: "element id must only contain letters, digits, dash and underscore characters"
9837
- };
9838
- }
9839
- isRelevant(event) {
9840
- return event.key === "id";
9841
- }
9842
- }
9843
-
9844
- class VoidContent extends Rule {
9845
- documentation(tagName) {
9846
- const doc = {
9847
- description: "HTML void elements cannot have any content and must not have content or end tag.",
9848
- url: "https://html-validate.org/rules/void-content.html"
9774
+ }
9775
+
9776
+ class H67 extends Rule {
9777
+ documentation() {
9778
+ return {
9779
+ description: "A decorative image cannot have a title attribute. Either remove `title` or add a descriptive `alt` text.",
9780
+ url: "https://html-validate.org/rules/wcag/h67.html"
9849
9781
  };
9850
- if (tagName) {
9851
- doc.description = `<${tagName}> is a void element and must not have content or end tag.`;
9852
- }
9853
- return doc;
9854
9782
  }
9855
9783
  setup() {
9856
9784
  this.on("tag:end", (event) => {
9857
9785
  const node = event.target;
9858
- if (!node) {
9786
+ if (!node || node.tagName !== "img") {
9859
9787
  return;
9860
9788
  }
9861
- if (!node.voidElement) {
9789
+ const title = node.getAttribute("title");
9790
+ if (!title || title.value === "") {
9862
9791
  return;
9863
9792
  }
9864
- if (node.closed === NodeClosed.EndTag) {
9865
- this.report(
9866
- null,
9867
- `End tag for <${node.tagName}> must be omitted`,
9868
- node.location,
9869
- node.tagName
9870
- );
9793
+ const alt = node.getAttributeValue("alt");
9794
+ if (alt && alt !== "") {
9795
+ return;
9871
9796
  }
9797
+ this.report(node, "<img> with empty alt text cannot have title attribute", title.keyLocation);
9872
9798
  });
9873
9799
  }
9874
9800
  }
9875
9801
 
9876
- const defaults$2 = {
9877
- style: "omit"
9878
- };
9879
- class VoidStyle extends Rule {
9880
- constructor(options) {
9881
- super({ ...defaults$2, ...options });
9882
- this.style = parseStyle(this.options.style);
9883
- }
9884
- static schema() {
9885
- return {
9886
- style: {
9887
- enum: ["omit", "selfclose", "selfclosing"],
9888
- type: "string"
9889
- }
9890
- };
9891
- }
9892
- documentation(context) {
9893
- const [desc, end] = styleDescription(context.style);
9802
+ class H71 extends Rule {
9803
+ documentation() {
9894
9804
  return {
9895
- description: `The current configuration requires void elements to ${desc}, use <${context.tagName}${end}> instead.`,
9896
- url: "https://html-validate.org/rules/void-style.html"
9805
+ description: "H71: Providing a description for groups of form controls using fieldset and legend elements",
9806
+ url: "https://html-validate.org/rules/wcag/h71.html"
9897
9807
  };
9898
9808
  }
9899
9809
  setup() {
9900
- this.on("tag:end", (event) => {
9901
- const active = event.previous;
9902
- if (active.meta) {
9903
- this.validateActive(active);
9810
+ this.on("dom:ready", (event) => {
9811
+ const { document } = event;
9812
+ const fieldsets = document.querySelectorAll(this.selector);
9813
+ for (const fieldset of fieldsets) {
9814
+ this.validate(fieldset);
9904
9815
  }
9905
9816
  });
9906
9817
  }
9907
- validateActive(node) {
9908
- if (!node.voidElement) {
9909
- return;
9910
- }
9911
- if (this.shouldBeOmitted(node)) {
9912
- this.reportError(
9913
- node,
9914
- `Expected omitted end tag <${node.tagName}> instead of self-closing element <${node.tagName}/>`
9915
- );
9916
- }
9917
- if (this.shouldBeSelfClosed(node)) {
9918
- this.reportError(
9919
- node,
9920
- `Expected self-closing element <${node.tagName}/> instead of omitted end-tag <${node.tagName}>`
9921
- );
9818
+ validate(fieldset) {
9819
+ const legend = fieldset.querySelectorAll("> legend");
9820
+ if (legend.length === 0) {
9821
+ this.reportNode(fieldset);
9922
9822
  }
9923
9823
  }
9924
- reportError(node, message) {
9925
- const context = {
9926
- style: this.style,
9927
- tagName: node.tagName
9928
- };
9929
- super.report(node, message, null, context);
9930
- }
9931
- shouldBeOmitted(node) {
9932
- return this.style === 1 /* AlwaysOmit */ && node.closed === NodeClosed.VoidSelfClosed;
9824
+ reportNode(node) {
9825
+ super.report(node, `${node.annotatedName} must have a <legend> as the first child`);
9933
9826
  }
9934
- shouldBeSelfClosed(node) {
9935
- return this.style === 2 /* AlwaysSelfclose */ && node.closed === NodeClosed.VoidOmitted;
9827
+ get selector() {
9828
+ return this.getTagsDerivedFrom("fieldset").join(",");
9936
9829
  }
9937
9830
  }
9938
- function parseStyle(name) {
9939
- switch (name) {
9940
- case "omit":
9941
- return 1 /* AlwaysOmit */;
9942
- case "selfclose":
9943
- case "selfclosing":
9944
- return 2 /* AlwaysSelfclose */;
9945
- default:
9946
- throw new Error(`Invalid style "${name}" for "void-style" rule`);
9947
- }
9831
+
9832
+ const bundledRules$1 = {
9833
+ "wcag/h30": H30,
9834
+ "wcag/h32": H32,
9835
+ "wcag/h36": H36,
9836
+ "wcag/h37": H37,
9837
+ "wcag/h63": H63,
9838
+ "wcag/h67": H67,
9839
+ "wcag/h71": H71
9840
+ };
9841
+
9842
+ const bundledRules = {
9843
+ "allowed-links": AllowedLinks,
9844
+ "area-alt": AreaAlt,
9845
+ "aria-hidden-body": AriaHiddenBody,
9846
+ "aria-label-misuse": AriaLabelMisuse,
9847
+ "attr-case": AttrCase,
9848
+ "attr-delimiter": AttrDelimiter,
9849
+ "attr-pattern": AttrPattern,
9850
+ "attr-quotes": AttrQuotes,
9851
+ "attr-spacing": AttrSpacing,
9852
+ "attribute-allowed-values": AttributeAllowedValues,
9853
+ "attribute-boolean-style": AttributeBooleanStyle,
9854
+ "attribute-empty-style": AttributeEmptyStyle,
9855
+ "attribute-misuse": AttributeMisuse,
9856
+ "class-pattern": ClassPattern,
9857
+ "close-attr": CloseAttr,
9858
+ "close-order": CloseOrder,
9859
+ deprecated: Deprecated,
9860
+ "deprecated-rule": DeprecatedRule,
9861
+ "doctype-html": NoStyleTag$1,
9862
+ "doctype-style": DoctypeStyle,
9863
+ "element-case": ElementCase,
9864
+ "element-name": ElementName,
9865
+ "element-permitted-content": ElementPermittedContent,
9866
+ "element-permitted-occurrences": ElementPermittedOccurrences,
9867
+ "element-permitted-order": ElementPermittedOrder,
9868
+ "element-permitted-parent": ElementPermittedParent,
9869
+ "element-required-ancestor": ElementRequiredAncestor,
9870
+ "element-required-attributes": ElementRequiredAttributes,
9871
+ "element-required-content": ElementRequiredContent,
9872
+ "empty-heading": EmptyHeading,
9873
+ "empty-title": EmptyTitle,
9874
+ "form-dup-name": FormDupName,
9875
+ "heading-level": HeadingLevel,
9876
+ "hidden-focusable": HiddenFocusable,
9877
+ "id-pattern": IdPattern,
9878
+ "input-attributes": InputAttributes,
9879
+ "input-missing-label": InputMissingLabel,
9880
+ "long-title": LongTitle,
9881
+ "map-dup-name": MapDupName,
9882
+ "map-id-name": MapIdName,
9883
+ "meta-refresh": MetaRefresh,
9884
+ "missing-doctype": MissingDoctype,
9885
+ "multiple-labeled-controls": MultipleLabeledControls,
9886
+ "name-pattern": NamePattern,
9887
+ "no-abstract-role": NoAbstractRole,
9888
+ "no-autoplay": NoAutoplay,
9889
+ "no-conditional-comment": NoConditionalComment,
9890
+ "no-deprecated-attr": NoDeprecatedAttr,
9891
+ "no-dup-attr": NoDupAttr,
9892
+ "no-dup-class": NoDupClass,
9893
+ "no-dup-id": NoDupID,
9894
+ "no-implicit-button-type": NoImplicitButtonType,
9895
+ "no-implicit-input-type": NoImplicitInputType,
9896
+ "no-implicit-close": NoImplicitClose,
9897
+ "no-inline-style": NoInlineStyle,
9898
+ "no-missing-references": NoMissingReferences,
9899
+ "no-multiple-main": NoMultipleMain,
9900
+ "no-raw-characters": NoRawCharacters,
9901
+ "no-redundant-aria-label": NoRedundantAriaLabel,
9902
+ "no-redundant-for": NoRedundantFor,
9903
+ "no-redundant-role": NoRedundantRole,
9904
+ "no-self-closing": NoSelfClosing,
9905
+ "no-style-tag": NoStyleTag,
9906
+ "no-trailing-whitespace": NoTrailingWhitespace,
9907
+ "no-unknown-elements": NoUnknownElements,
9908
+ "no-unused-disable": NoUnusedDisable,
9909
+ "no-utf8-bom": NoUtf8Bom,
9910
+ "prefer-button": PreferButton,
9911
+ "prefer-native-element": PreferNativeElement,
9912
+ "prefer-tbody": PreferTbody,
9913
+ "require-csp-nonce": RequireCSPNonce,
9914
+ "require-sri": RequireSri,
9915
+ "script-element": ScriptElement,
9916
+ "script-type": ScriptType,
9917
+ "svg-focusable": SvgFocusable,
9918
+ "tel-non-breaking": TelNonBreaking,
9919
+ "text-content": TextContent,
9920
+ "unique-landmark": UniqueLandmark,
9921
+ "unrecognized-char-ref": UnknownCharReference,
9922
+ "valid-autocomplete": ValidAutocomplete,
9923
+ "valid-id": ValidID,
9924
+ "void-content": VoidContent,
9925
+ "void-style": VoidStyle,
9926
+ ...bundledRules$1
9927
+ };
9928
+
9929
+ const ruleIds = new Set(Object.keys(bundledRules));
9930
+ function ruleExists(ruleId) {
9931
+ return ruleIds.has(ruleId);
9948
9932
  }
9949
- function styleDescription(style) {
9950
- switch (style) {
9951
- case 1 /* AlwaysOmit */:
9952
- return ["omit end tag", ""];
9953
- case 2 /* AlwaysSelfclose */:
9954
- return ["be self-closed", "/"];
9955
- default:
9956
- throw new Error(`Unknown style`);
9933
+
9934
+ function depthFirst(root, callback) {
9935
+ if (root instanceof DOMTree) {
9936
+ if (root.readyState !== "complete") {
9937
+ throw new Error(`Cannot call walk.depthFirst(..) before document is ready`);
9938
+ }
9939
+ root = root.root;
9940
+ }
9941
+ function visit(node) {
9942
+ node.childElements.forEach(visit);
9943
+ if (!node.isRootElement()) {
9944
+ callback(node);
9945
+ }
9957
9946
  }
9947
+ visit(root);
9958
9948
  }
9949
+ const walk = {
9950
+ depthFirst
9951
+ };
9959
9952
 
9960
- class H30 extends Rule {
9961
- documentation() {
9962
- return {
9963
- description: "WCAG 2.1 requires each `<a href>` anchor link to have a text describing the purpose of the link using either plain text or an `<img>` with the `alt` attribute set.",
9964
- url: "https://html-validate.org/rules/wcag/h30.html"
9965
- };
9953
+ class DOMTree {
9954
+ /**
9955
+ * @internal
9956
+ */
9957
+ constructor(location) {
9958
+ this.root = HtmlElement.rootNode(location);
9959
+ this.active = this.root;
9960
+ this.doctype = null;
9961
+ this._readyState = "loading";
9962
+ }
9963
+ /**
9964
+ * @internal
9965
+ */
9966
+ pushActive(node) {
9967
+ this.active = node;
9968
+ }
9969
+ /**
9970
+ * @internal
9971
+ */
9972
+ popActive() {
9973
+ if (this.active.isRootElement()) {
9974
+ return;
9975
+ }
9976
+ this.active = this.active.parent ?? this.root;
9977
+ }
9978
+ /**
9979
+ * @internal
9980
+ */
9981
+ getActive() {
9982
+ return this.active;
9983
+ }
9984
+ /**
9985
+ * Describes the loading state of the document.
9986
+ *
9987
+ * When `"loading"` it is still not safe to use functions such as
9988
+ * `querySelector` or presence of attributes, child nodes, etc.
9989
+ */
9990
+ get readyState() {
9991
+ return this._readyState;
9966
9992
  }
9967
- setup() {
9968
- this.on("dom:ready", (event) => {
9969
- const links = event.document.getElementsByTagName("a");
9970
- for (const link of links) {
9971
- if (!link.hasAttribute("href")) {
9972
- continue;
9973
- }
9974
- if (!inAccessibilityTree(link)) {
9975
- continue;
9976
- }
9977
- const textClassification = classifyNodeText(link, { ignoreHiddenRoot: true });
9978
- if (textClassification !== TextClassification.EMPTY_TEXT) {
9979
- continue;
9980
- }
9981
- const images = link.querySelectorAll("img");
9982
- if (images.some((image) => hasAltText(image))) {
9983
- continue;
9984
- }
9985
- const labels = link.querySelectorAll("[aria-label]");
9986
- if (hasAriaLabel(link) || labels.some((cur) => hasAriaLabel(cur))) {
9987
- continue;
9988
- }
9989
- this.report(link, "Anchor link must have a text describing its purpose");
9990
- }
9993
+ /**
9994
+ * Resolve dynamic meta expressions.
9995
+ *
9996
+ * @internal
9997
+ */
9998
+ resolveMeta(table) {
9999
+ this._readyState = "complete";
10000
+ walk.depthFirst(this, (node) => {
10001
+ table.resolve(node);
9991
10002
  });
9992
10003
  }
9993
- }
9994
-
9995
- class H32 extends Rule {
9996
- documentation() {
9997
- return {
9998
- description: "WCAG 2.1 requires each `<form>` element to have at least one submit button.",
9999
- url: "https://html-validate.org/rules/wcag/h32.html"
10000
- };
10004
+ getElementsByTagName(tagName) {
10005
+ return this.root.getElementsByTagName(tagName);
10001
10006
  }
10002
- setup() {
10003
- const formTags = this.getTagsWithProperty("form");
10004
- const formSelector = formTags.join(",");
10005
- this.on("dom:ready", (event) => {
10006
- const { document } = event;
10007
- const forms = document.querySelectorAll(formSelector);
10008
- for (const form of forms) {
10009
- if (hasNestedSubmit(form)) {
10010
- continue;
10011
- }
10012
- if (hasAssociatedSubmit(document, form)) {
10013
- continue;
10014
- }
10015
- this.report(form, `<${form.tagName}> element must have a submit button`);
10016
- }
10017
- });
10007
+ /**
10008
+ * @deprecated use utility function `walk.depthFirst(..)` instead (since 8.21.0).
10009
+ */
10010
+ visitDepthFirst(callback) {
10011
+ walk.depthFirst(this, callback);
10018
10012
  }
10019
- }
10020
- function isSubmit(node) {
10021
- const type = node.getAttribute("type");
10022
- return Boolean(!type || type.valueMatches(/submit|image/));
10023
- }
10024
- function isAssociated(id, node) {
10025
- const form = node.getAttribute("form");
10026
- return Boolean(form == null ? void 0 : form.valueMatches(id, true));
10027
- }
10028
- function hasNestedSubmit(form) {
10029
- const matches = form.querySelectorAll("button,input").filter(isSubmit).filter((node) => !node.hasAttribute("form"));
10030
- return matches.length > 0;
10031
- }
10032
- function hasAssociatedSubmit(document, form) {
10033
- const { id } = form;
10034
- if (!id) {
10035
- return false;
10013
+ /**
10014
+ * @deprecated use `querySelector(..)` instead (since 8.21.0)
10015
+ */
10016
+ find(callback) {
10017
+ return this.root.find(callback);
10036
10018
  }
10037
- const matches = document.querySelectorAll("button[form],input[form]").filter(isSubmit).filter((node) => isAssociated(id, node));
10038
- return matches.length > 0;
10039
- }
10040
-
10041
- class H36 extends Rule {
10042
- documentation() {
10043
- return {
10044
- description: [
10045
- "WCAG 2.1 requires all images used as submit buttons to have a non-empty textual description using the `alt` attribute.",
10046
- 'The alt text cannot be empty (`alt=""`).'
10047
- ].join("\n"),
10048
- url: "https://html-validate.org/rules/wcag/h36.html"
10049
- };
10019
+ querySelector(selector) {
10020
+ return this.root.querySelector(selector);
10050
10021
  }
10051
- setup() {
10052
- this.on("tag:end", (event) => {
10053
- const node = event.previous;
10054
- if (node.tagName !== "input")
10055
- return;
10056
- if (node.getAttributeValue("type") !== "image") {
10057
- return;
10058
- }
10059
- if (!inAccessibilityTree(node)) {
10060
- return;
10061
- }
10062
- if (!hasAltText(node)) {
10063
- const message = "image used as submit button must have non-empty alt text";
10064
- const alt = node.getAttribute("alt");
10065
- this.report({
10066
- node,
10067
- message,
10068
- location: alt ? alt.keyLocation : node.location
10069
- });
10070
- }
10071
- });
10022
+ querySelectorAll(selector) {
10023
+ return this.root.querySelectorAll(selector);
10072
10024
  }
10073
10025
  }
10074
10026
 
10075
- const defaults$1 = {
10076
- allowEmpty: true,
10077
- alias: []
10078
- };
10079
- class H37 extends Rule {
10080
- constructor(options) {
10081
- super({ ...defaults$1, ...options });
10082
- if (!Array.isArray(this.options.alias)) {
10083
- this.options.alias = [this.options.alias];
10027
+ const allowedKeys = ["exclude"];
10028
+ class Validator {
10029
+ /**
10030
+ * Test if element is used in a proper context.
10031
+ *
10032
+ * @param node - Element to test.
10033
+ * @param rules - List of rules.
10034
+ * @returns `true` if element passes all tests.
10035
+ */
10036
+ static validatePermitted(node, rules) {
10037
+ if (!rules) {
10038
+ return true;
10084
10039
  }
10040
+ return rules.some((rule) => {
10041
+ return Validator.validatePermittedRule(node, rule);
10042
+ });
10085
10043
  }
10086
- static schema() {
10087
- return {
10088
- alias: {
10089
- anyOf: [
10090
- {
10091
- items: {
10092
- type: "string"
10093
- },
10094
- type: "array"
10095
- },
10096
- {
10097
- type: "string"
10044
+ /**
10045
+ * Test if an element is used the correct amount of times.
10046
+ *
10047
+ * For instance, a `<table>` element can only contain a single `<tbody>`
10048
+ * child. If multiple `<tbody>` exists this test will fail both nodes.
10049
+ * Note that this is called on the parent but will fail the children violating
10050
+ * the rule.
10051
+ *
10052
+ * @param children - Array of children to validate.
10053
+ * @param rules - List of rules of the parent element.
10054
+ * @returns `true` if the parent element of the children passes the test.
10055
+ */
10056
+ static validateOccurrences(children, rules, cb) {
10057
+ if (!rules) {
10058
+ return true;
10059
+ }
10060
+ let valid = true;
10061
+ for (const rule of rules) {
10062
+ if (typeof rule !== "string") {
10063
+ return false;
10064
+ }
10065
+ const [, category, quantifier] = rule.match(/^(@?.*?)([?*]?)$/);
10066
+ const limit = category && quantifier && parseQuantifier(quantifier);
10067
+ if (limit) {
10068
+ const siblings = children.filter(
10069
+ (cur) => Validator.validatePermittedCategory(cur, rule, true)
10070
+ );
10071
+ if (siblings.length > limit) {
10072
+ for (const child of siblings.slice(limit)) {
10073
+ cb(child, category);
10098
10074
  }
10099
- ]
10100
- },
10101
- allowEmpty: {
10102
- type: "boolean"
10075
+ valid = false;
10076
+ }
10103
10077
  }
10104
- };
10105
- }
10106
- documentation() {
10107
- return {
10108
- description: "Both HTML5 and WCAG 2.0 requires images to have a alternative text for each image.",
10109
- url: "https://html-validate.org/rules/wcag/h37.html"
10110
- };
10078
+ }
10079
+ return valid;
10111
10080
  }
10112
- setup() {
10113
- this.on("dom:ready", (event) => {
10114
- const { document } = event;
10115
- const nodes = document.querySelectorAll("img");
10116
- for (const node of nodes) {
10117
- this.validateNode(node);
10081
+ /**
10082
+ * Validate elements order.
10083
+ *
10084
+ * Given a parent element with children and metadata containing permitted
10085
+ * order it will validate each children and ensure each one exists in the
10086
+ * specified order.
10087
+ *
10088
+ * For instance, for a `<table>` element the `<caption>` element must come
10089
+ * before a `<thead>` which must come before `<tbody>`.
10090
+ *
10091
+ * @param children - Array of children to validate.
10092
+ */
10093
+ static validateOrder(children, rules, cb) {
10094
+ if (!rules) {
10095
+ return true;
10096
+ }
10097
+ let i = 0;
10098
+ let prev = null;
10099
+ for (const node of children) {
10100
+ const old = i;
10101
+ while (rules[i] && !Validator.validatePermittedCategory(node, rules[i], true)) {
10102
+ i++;
10103
+ }
10104
+ if (i >= rules.length) {
10105
+ const orderSpecified = rules.find(
10106
+ (cur) => Validator.validatePermittedCategory(node, cur, true)
10107
+ );
10108
+ if (orderSpecified) {
10109
+ cb(node, prev);
10110
+ return false;
10111
+ }
10112
+ i = old;
10118
10113
  }
10114
+ prev = node;
10115
+ }
10116
+ return true;
10117
+ }
10118
+ /**
10119
+ * Validate element ancestors.
10120
+ *
10121
+ * Check if an element has the required set of elements. At least one of the
10122
+ * selectors must match.
10123
+ */
10124
+ static validateAncestors(node, rules) {
10125
+ if (!rules || rules.length === 0) {
10126
+ return true;
10127
+ }
10128
+ return rules.some((rule) => node.closest(rule));
10129
+ }
10130
+ /**
10131
+ * Validate element required content.
10132
+ *
10133
+ * Check if an element has the required set of elements. At least one of the
10134
+ * selectors must match.
10135
+ *
10136
+ * Returns `[]` when valid or a list of required but missing tagnames or
10137
+ * categories.
10138
+ */
10139
+ static validateRequiredContent(node, rules) {
10140
+ if (!rules || rules.length === 0) {
10141
+ return [];
10142
+ }
10143
+ return rules.filter((tagName) => {
10144
+ const haveMatchingChild = node.childElements.some(
10145
+ (child) => Validator.validatePermittedCategory(child, tagName, false)
10146
+ );
10147
+ return !haveMatchingChild;
10119
10148
  });
10120
10149
  }
10121
- validateNode(node) {
10122
- if (!inAccessibilityTree(node)) {
10123
- return;
10150
+ /**
10151
+ * Test if an attribute has an allowed value and/or format.
10152
+ *
10153
+ * @param attr - Attribute to test.
10154
+ * @param rules - Element attribute metadta.
10155
+ * @returns `true` if attribute passes all tests.
10156
+ */
10157
+ static validateAttribute(attr, rules) {
10158
+ const rule = rules[attr.key];
10159
+ if (!rule) {
10160
+ return true;
10124
10161
  }
10125
- if (Boolean(node.getAttributeValue("alt")) || Boolean(node.hasAttribute("alt") && this.options.allowEmpty)) {
10126
- return;
10162
+ const value = attr.value;
10163
+ if (value instanceof DynamicValue) {
10164
+ return true;
10127
10165
  }
10128
- for (const attr of this.options.alias) {
10129
- if (node.getAttribute(attr)) {
10130
- return;
10131
- }
10166
+ const empty = value === null || value === "";
10167
+ if (rule.boolean) {
10168
+ return empty || value === attr.key;
10132
10169
  }
10133
- const tag = node.annotatedName;
10134
- if (node.hasAttribute("alt")) {
10135
- const attr = node.getAttribute("alt");
10136
- this.report(node, `${tag} cannot have empty "alt" attribute`, attr.keyLocation);
10137
- } else {
10138
- this.report(node, `${tag} is missing required "alt" attribute`, node.location);
10170
+ if (rule.omit && empty) {
10171
+ return true;
10139
10172
  }
10173
+ if (rule.list) {
10174
+ const tokens = new DOMTokenList(value, attr.valueLocation);
10175
+ return tokens.every((token) => {
10176
+ return this.validateAttributeValue(token, rule);
10177
+ });
10178
+ }
10179
+ return this.validateAttributeValue(value, rule);
10140
10180
  }
10141
- }
10142
-
10143
- var _a;
10144
- const { enum: validScopes } = (_a = elements.html5.th.attributes) == null ? void 0 : _a.scope;
10145
- const joinedScopes = utils_naturalJoin.naturalJoin(validScopes);
10146
- class H63 extends Rule {
10147
- documentation() {
10148
- return {
10149
- description: "H63: Using the scope attribute to associate header cells and data cells in data tables",
10150
- url: "https://html-validate.org/rules/wcag/h63.html"
10151
- };
10152
- }
10153
- setup() {
10154
- this.on("tag:ready", (event) => {
10155
- const node = event.target;
10156
- if (node.tagName !== "th") {
10157
- return;
10158
- }
10159
- const scope = node.getAttribute("scope");
10160
- const value = scope == null ? void 0 : scope.value;
10161
- if (value instanceof DynamicValue) {
10162
- return;
10163
- }
10164
- if (value && validScopes.includes(value)) {
10165
- return;
10166
- }
10167
- const message = `<th> element must have a valid scope attribute: ${joinedScopes}`;
10168
- const location = (scope == null ? void 0 : scope.valueLocation) ?? (scope == null ? void 0 : scope.keyLocation) ?? node.location;
10169
- this.report(node, message, location);
10170
- });
10171
- }
10172
- }
10173
-
10174
- class H67 extends Rule {
10175
- documentation() {
10176
- return {
10177
- description: "A decorative image cannot have a title attribute. Either remove `title` or add a descriptive `alt` text.",
10178
- url: "https://html-validate.org/rules/wcag/h67.html"
10179
- };
10180
- }
10181
- setup() {
10182
- this.on("tag:end", (event) => {
10183
- const node = event.target;
10184
- if (!node || node.tagName !== "img") {
10185
- return;
10186
- }
10187
- const title = node.getAttribute("title");
10188
- if (!title || title.value === "") {
10189
- return;
10190
- }
10191
- const alt = node.getAttributeValue("alt");
10192
- if (alt && alt !== "") {
10193
- return;
10181
+ static validateAttributeValue(value, rule) {
10182
+ if (!rule.enum) {
10183
+ return true;
10184
+ }
10185
+ if (value === null) {
10186
+ return false;
10187
+ }
10188
+ const caseInsensitiveValue = value.toLowerCase();
10189
+ return rule.enum.some((entry) => {
10190
+ if (entry instanceof RegExp) {
10191
+ return !!value.match(entry);
10192
+ } else {
10193
+ return caseInsensitiveValue === entry;
10194
10194
  }
10195
- this.report(node, "<img> with empty alt text cannot have title attribute", title.keyLocation);
10196
10195
  });
10197
10196
  }
10198
- }
10199
-
10200
- class H71 extends Rule {
10201
- documentation() {
10202
- return {
10203
- description: "H71: Providing a description for groups of form controls using fieldset and legend elements",
10204
- url: "https://html-validate.org/rules/wcag/h71.html"
10205
- };
10206
- }
10207
- setup() {
10208
- this.on("dom:ready", (event) => {
10209
- const { document } = event;
10210
- const fieldsets = document.querySelectorAll(this.selector);
10211
- for (const fieldset of fieldsets) {
10212
- this.validate(fieldset);
10197
+ static validatePermittedRule(node, rule, isExclude = false) {
10198
+ if (typeof rule === "string") {
10199
+ return Validator.validatePermittedCategory(node, rule, !isExclude);
10200
+ } else if (Array.isArray(rule)) {
10201
+ return rule.every((inner) => {
10202
+ return Validator.validatePermittedRule(node, inner, isExclude);
10203
+ });
10204
+ } else {
10205
+ validateKeys(rule);
10206
+ if (rule.exclude) {
10207
+ if (Array.isArray(rule.exclude)) {
10208
+ return !rule.exclude.some((inner) => {
10209
+ return Validator.validatePermittedRule(node, inner, true);
10210
+ });
10211
+ } else {
10212
+ return !Validator.validatePermittedRule(node, rule.exclude, true);
10213
+ }
10214
+ } else {
10215
+ return true;
10213
10216
  }
10214
- });
10217
+ }
10215
10218
  }
10216
- validate(fieldset) {
10217
- const legend = fieldset.querySelectorAll("> legend");
10218
- if (legend.length === 0) {
10219
- this.reportNode(fieldset);
10219
+ /**
10220
+ * Validate node against a content category.
10221
+ *
10222
+ * When matching parent nodes against permitted parents use the superset
10223
+ * parameter to also match for `@flow`. E.g. if a node expects a `@phrasing`
10224
+ * parent it should also allow `@flow` parent since `@phrasing` is a subset of
10225
+ * `@flow`.
10226
+ *
10227
+ * @param node - The node to test against
10228
+ * @param category - Name of category with `@` prefix or tag name.
10229
+ * @param defaultMatch - The default return value when node categories is not known.
10230
+ */
10231
+ /* eslint-disable-next-line complexity -- rule does not like switch */
10232
+ static validatePermittedCategory(node, category, defaultMatch) {
10233
+ const [, rawCategory] = category.match(/^(@?.*?)([?*]?)$/);
10234
+ if (!rawCategory.startsWith("@")) {
10235
+ return node.tagName === rawCategory;
10236
+ }
10237
+ if (!node.meta) {
10238
+ return defaultMatch;
10239
+ }
10240
+ switch (rawCategory) {
10241
+ case "@meta":
10242
+ return node.meta.metadata;
10243
+ case "@flow":
10244
+ return node.meta.flow;
10245
+ case "@sectioning":
10246
+ return node.meta.sectioning;
10247
+ case "@heading":
10248
+ return node.meta.heading;
10249
+ case "@phrasing":
10250
+ return node.meta.phrasing;
10251
+ case "@embedded":
10252
+ return node.meta.embedded;
10253
+ case "@interactive":
10254
+ return node.meta.interactive;
10255
+ case "@script":
10256
+ return Boolean(node.meta.scriptSupporting);
10257
+ case "@form":
10258
+ return Boolean(node.meta.form);
10259
+ default:
10260
+ throw new Error(`Invalid content category "${category}"`);
10220
10261
  }
10221
10262
  }
10222
- reportNode(node) {
10223
- super.report(node, `${node.annotatedName} must have a <legend> as the first child`);
10263
+ }
10264
+ function validateKeys(rule) {
10265
+ for (const key of Object.keys(rule)) {
10266
+ if (!allowedKeys.includes(key)) {
10267
+ const str = JSON.stringify(rule);
10268
+ throw new Error(`Permitted rule "${str}" contains unknown property "${key}"`);
10269
+ }
10224
10270
  }
10225
- get selector() {
10226
- return this.getTagsDerivedFrom("fieldset").join(",");
10271
+ }
10272
+ function parseQuantifier(quantifier) {
10273
+ switch (quantifier) {
10274
+ case "?":
10275
+ return 1;
10276
+ case "*":
10277
+ return null;
10278
+ default:
10279
+ throw new Error(`Invalid quantifier "${quantifier}" used`);
10227
10280
  }
10228
10281
  }
10229
10282
 
10230
- const bundledRules$1 = {
10231
- "wcag/h30": H30,
10232
- "wcag/h32": H32,
10233
- "wcag/h36": H36,
10234
- "wcag/h37": H37,
10235
- "wcag/h63": H63,
10236
- "wcag/h67": H67,
10237
- "wcag/h71": H71
10283
+ const $schema = "http://json-schema.org/draft-06/schema#";
10284
+ const $id = "https://html-validate.org/schemas/config.json";
10285
+ const type = "object";
10286
+ const additionalProperties = false;
10287
+ const properties = {
10288
+ $schema: {
10289
+ type: "string"
10290
+ },
10291
+ root: {
10292
+ type: "boolean",
10293
+ title: "Mark as root configuration",
10294
+ description: "If this is set to true no further configurations will be searched.",
10295
+ "default": false
10296
+ },
10297
+ "extends": {
10298
+ type: "array",
10299
+ items: {
10300
+ type: "string"
10301
+ },
10302
+ title: "Configurations to extend",
10303
+ description: "Array of shareable or builtin configurations to extend."
10304
+ },
10305
+ elements: {
10306
+ type: "array",
10307
+ items: {
10308
+ anyOf: [
10309
+ {
10310
+ type: "string"
10311
+ },
10312
+ {
10313
+ type: "object"
10314
+ }
10315
+ ]
10316
+ },
10317
+ title: "Element metadata to load",
10318
+ description: "Array of modules, plugins or files to load element metadata from. Use <rootDir> to refer to the folder with the package.json file.",
10319
+ examples: [
10320
+ [
10321
+ "html-validate:recommended",
10322
+ "plugin:recommended",
10323
+ "module",
10324
+ "./local-file.json"
10325
+ ]
10326
+ ]
10327
+ },
10328
+ plugins: {
10329
+ type: "array",
10330
+ items: {
10331
+ anyOf: [
10332
+ {
10333
+ type: "string"
10334
+ },
10335
+ {
10336
+ type: "object"
10337
+ }
10338
+ ]
10339
+ },
10340
+ title: "Plugins to load",
10341
+ description: "Array of plugins load. Use <rootDir> to refer to the folder with the package.json file.",
10342
+ examples: [
10343
+ [
10344
+ "my-plugin",
10345
+ "./local-plugin"
10346
+ ]
10347
+ ]
10348
+ },
10349
+ transform: {
10350
+ type: "object",
10351
+ additionalProperties: {
10352
+ type: "string"
10353
+ },
10354
+ title: "File transformations to use.",
10355
+ description: "Object where key is regular expression to match filename and value is name of transformer.",
10356
+ examples: [
10357
+ {
10358
+ "^.*\\.foo$": "my-transformer",
10359
+ "^.*\\.bar$": "my-plugin",
10360
+ "^.*\\.baz$": "my-plugin:named"
10361
+ }
10362
+ ]
10363
+ },
10364
+ rules: {
10365
+ type: "object",
10366
+ patternProperties: {
10367
+ ".*": {
10368
+ anyOf: [
10369
+ {
10370
+ "enum": [
10371
+ 0,
10372
+ 1,
10373
+ 2,
10374
+ "off",
10375
+ "warn",
10376
+ "error"
10377
+ ]
10378
+ },
10379
+ {
10380
+ type: "array",
10381
+ minItems: 1,
10382
+ maxItems: 1,
10383
+ items: [
10384
+ {
10385
+ "enum": [
10386
+ 0,
10387
+ 1,
10388
+ 2,
10389
+ "off",
10390
+ "warn",
10391
+ "error"
10392
+ ]
10393
+ }
10394
+ ]
10395
+ },
10396
+ {
10397
+ type: "array",
10398
+ minItems: 2,
10399
+ maxItems: 2,
10400
+ items: [
10401
+ {
10402
+ "enum": [
10403
+ 0,
10404
+ 1,
10405
+ 2,
10406
+ "off",
10407
+ "warn",
10408
+ "error"
10409
+ ]
10410
+ },
10411
+ {
10412
+ }
10413
+ ]
10414
+ }
10415
+ ]
10416
+ }
10417
+ },
10418
+ title: "Rule configuration.",
10419
+ description: "Enable/disable rules, set severity. Some rules have additional configuration like style or patterns to use.",
10420
+ examples: [
10421
+ {
10422
+ foo: "error",
10423
+ bar: "off",
10424
+ baz: [
10425
+ "error",
10426
+ {
10427
+ style: "camelcase"
10428
+ }
10429
+ ]
10430
+ }
10431
+ ]
10432
+ }
10433
+ };
10434
+ var configurationSchema = {
10435
+ $schema: $schema,
10436
+ $id: $id,
10437
+ type: type,
10438
+ additionalProperties: additionalProperties,
10439
+ properties: properties
10238
10440
  };
10239
10441
 
10240
- const bundledRules = {
10241
- "allowed-links": AllowedLinks,
10242
- "area-alt": AreaAlt,
10243
- "aria-hidden-body": AriaHiddenBody,
10244
- "aria-label-misuse": AriaLabelMisuse,
10245
- "attr-case": AttrCase,
10246
- "attr-delimiter": AttrDelimiter,
10247
- "attr-pattern": AttrPattern,
10248
- "attr-quotes": AttrQuotes,
10249
- "attr-spacing": AttrSpacing,
10250
- "attribute-allowed-values": AttributeAllowedValues,
10251
- "attribute-boolean-style": AttributeBooleanStyle,
10252
- "attribute-empty-style": AttributeEmptyStyle,
10253
- "attribute-misuse": AttributeMisuse,
10254
- "class-pattern": ClassPattern,
10255
- "close-attr": CloseAttr,
10256
- "close-order": CloseOrder,
10257
- deprecated: Deprecated,
10258
- "deprecated-rule": DeprecatedRule,
10259
- "doctype-html": NoStyleTag$1,
10260
- "doctype-style": DoctypeStyle,
10261
- "element-case": ElementCase,
10262
- "element-name": ElementName,
10263
- "element-permitted-content": ElementPermittedContent,
10264
- "element-permitted-occurrences": ElementPermittedOccurrences,
10265
- "element-permitted-order": ElementPermittedOrder,
10266
- "element-permitted-parent": ElementPermittedParent,
10267
- "element-required-ancestor": ElementRequiredAncestor,
10268
- "element-required-attributes": ElementRequiredAttributes,
10269
- "element-required-content": ElementRequiredContent,
10270
- "empty-heading": EmptyHeading,
10271
- "empty-title": EmptyTitle,
10272
- "form-dup-name": FormDupName,
10273
- "heading-level": HeadingLevel,
10274
- "hidden-focusable": HiddenFocusable,
10275
- "id-pattern": IdPattern,
10276
- "input-attributes": InputAttributes,
10277
- "input-missing-label": InputMissingLabel,
10278
- "long-title": LongTitle,
10279
- "map-dup-name": MapDupName,
10280
- "map-id-name": MapIdName,
10281
- "meta-refresh": MetaRefresh,
10282
- "missing-doctype": MissingDoctype,
10283
- "multiple-labeled-controls": MultipleLabeledControls,
10284
- "name-pattern": NamePattern,
10285
- "no-abstract-role": NoAbstractRole,
10286
- "no-autoplay": NoAutoplay,
10287
- "no-conditional-comment": NoConditionalComment,
10288
- "no-deprecated-attr": NoDeprecatedAttr,
10289
- "no-dup-attr": NoDupAttr,
10290
- "no-dup-class": NoDupClass,
10291
- "no-dup-id": NoDupID,
10292
- "no-implicit-button-type": NoImplicitButtonType,
10293
- "no-implicit-input-type": NoImplicitInputType,
10294
- "no-implicit-close": NoImplicitClose,
10295
- "no-inline-style": NoInlineStyle,
10296
- "no-missing-references": NoMissingReferences,
10297
- "no-multiple-main": NoMultipleMain,
10298
- "no-raw-characters": NoRawCharacters,
10299
- "no-redundant-aria-label": NoRedundantAriaLabel,
10300
- "no-redundant-for": NoRedundantFor,
10301
- "no-redundant-role": NoRedundantRole,
10302
- "no-self-closing": NoSelfClosing,
10303
- "no-style-tag": NoStyleTag,
10304
- "no-trailing-whitespace": NoTrailingWhitespace,
10305
- "no-unknown-elements": NoUnknownElements,
10306
- "no-unused-disable": NoUnusedDisable,
10307
- "no-utf8-bom": NoUtf8Bom,
10308
- "prefer-button": PreferButton,
10309
- "prefer-native-element": PreferNativeElement,
10310
- "prefer-tbody": PreferTbody,
10311
- "require-csp-nonce": RequireCSPNonce,
10312
- "require-sri": RequireSri,
10313
- "script-element": ScriptElement,
10314
- "script-type": ScriptType,
10315
- "svg-focusable": SvgFocusable,
10316
- "tel-non-breaking": TelNonBreaking,
10317
- "text-content": TextContent,
10318
- "unique-landmark": UniqueLandmark,
10319
- "unrecognized-char-ref": UnknownCharReference,
10320
- "valid-autocomplete": ValidAutocomplete,
10321
- "valid-id": ValidID,
10322
- "void-content": VoidContent,
10323
- "void-style": VoidStyle,
10324
- ...bundledRules$1
10442
+ const TRANSFORMER_API = {
10443
+ VERSION: 1
10325
10444
  };
10326
10445
 
10327
10446
  var defaultConfig = {};
@@ -11449,7 +11568,7 @@ class Parser {
11449
11568
  location
11450
11569
  };
11451
11570
  this.trigger("tag:end", event);
11452
- if (active && !active.isRootElement()) {
11571
+ if (!active.isRootElement()) {
11453
11572
  this.trigger("element:ready", {
11454
11573
  target: active,
11455
11574
  location: active.location
@@ -11801,15 +11920,6 @@ class Parser {
11801
11920
  }
11802
11921
  }
11803
11922
 
11804
- function isThenable(value) {
11805
- return value && typeof value === "object" && "then" in value && typeof value.then === "function";
11806
- }
11807
-
11808
- const ruleIds = new Set(Object.keys(bundledRules));
11809
- function ruleExists(ruleId) {
11810
- return ruleIds.has(ruleId);
11811
- }
11812
-
11813
11923
  function freeze(src) {
11814
11924
  return {
11815
11925
  ...src,
@@ -11980,7 +12090,7 @@ class Engine {
11980
12090
  const directiveContext = {
11981
12091
  rules,
11982
12092
  reportUnused(rules2, unused, options, location2) {
11983
- if (noUnusedDisable && !rules2.has(noUnusedDisable.name)) {
12093
+ if (!rules2.has(noUnusedDisable.name)) {
11984
12094
  noUnusedDisable.reportUnused(unused, options, location2);
11985
12095
  }
11986
12096
  }
@@ -12057,33 +12167,8 @@ class Engine {
12057
12167
  }
12058
12168
  dumpTree(source) {
12059
12169
  const parser = this.instantiateParser();
12060
- const document = parser.parseHtml(source[0]);
12061
- const lines = [];
12062
- function decoration(node) {
12063
- let output = "";
12064
- if (node.id) {
12065
- output += `#${node.id}`;
12066
- }
12067
- if (node.hasAttribute("class")) {
12068
- output += `.${node.classList.join(".")}`;
12069
- }
12070
- return output;
12071
- }
12072
- function writeNode(node, level, sibling) {
12073
- if (node.parent) {
12074
- const indent = " ".repeat(level - 1);
12075
- const l = node.childElements.length > 0 ? "\u252C" : "\u2500";
12076
- const b = sibling < node.parent.childElements.length - 1 ? "\u251C" : "\u2514";
12077
- lines.push(`${indent}${b}\u2500${l} ${node.tagName}${decoration(node)}`);
12078
- } else {
12079
- lines.push("(root)");
12080
- }
12081
- node.childElements.forEach((child, index) => {
12082
- writeNode(child, level + 1, index);
12083
- });
12084
- }
12085
- writeNode(document, 0, 0);
12086
- return lines;
12170
+ const root = parser.parseHtml(source[0]);
12171
+ return dumpTree(root);
12087
12172
  }
12088
12173
  /**
12089
12174
  * Get rule documentation.
@@ -12111,7 +12196,9 @@ class Engine {
12111
12196
  return new this.ParserClass(this.config);
12112
12197
  }
12113
12198
  processDirective(event, parser, context) {
12114
- const rules = event.data.split(",").map((name) => name.trim()).map((name) => context.rules[name]).filter((rule) => rule);
12199
+ const rules = event.data.split(",").map((name) => name.trim()).map((name) => context.rules[name]).filter((rule) => {
12200
+ return Boolean(rule);
12201
+ });
12115
12202
  const location = event.optionsLocation ?? event.location;
12116
12203
  switch (event.action) {
12117
12204
  case "enable":
@@ -12135,7 +12222,7 @@ class Engine {
12135
12222
  rule.setServerity(Severity.ERROR);
12136
12223
  }
12137
12224
  }
12138
- parser.on("tag:start", (event, data) => {
12225
+ parser.on("tag:start", (_event, data) => {
12139
12226
  data.target.enableRules(rules.map((rule) => rule.name));
12140
12227
  });
12141
12228
  }
@@ -12143,7 +12230,7 @@ class Engine {
12143
12230
  for (const rule of rules) {
12144
12231
  rule.setEnabled(false);
12145
12232
  }
12146
- parser.on("tag:start", (event, data) => {
12233
+ parser.on("tag:start", (_event, data) => {
12147
12234
  data.target.disableRules(rules.map((rule) => rule.name));
12148
12235
  });
12149
12236
  }
@@ -12155,14 +12242,14 @@ class Engine {
12155
12242
  for (const rule of rules) {
12156
12243
  rule.block(blocker);
12157
12244
  }
12158
- const unregisterOpen = parser.on("tag:start", (event, data) => {
12245
+ const unregisterOpen = parser.on("tag:start", (_event, data) => {
12159
12246
  var _a;
12160
12247
  if (directiveBlock === null) {
12161
12248
  directiveBlock = ((_a = data.target.parent) == null ? void 0 : _a.unique) ?? null;
12162
12249
  }
12163
12250
  data.target.blockRules(ruleIds, blocker);
12164
12251
  });
12165
- const unregisterClose = parser.on("tag:end", (event, data) => {
12252
+ const unregisterClose = parser.on("tag:end", (_event, data) => {
12166
12253
  const lastNode = directiveBlock === null;
12167
12254
  const parentClosed = directiveBlock === data.previous.unique;
12168
12255
  if (lastNode || parentClosed) {
@@ -12173,7 +12260,7 @@ class Engine {
12173
12260
  }
12174
12261
  }
12175
12262
  });
12176
- parser.on("rule:error", (event, data) => {
12263
+ parser.on("rule:error", (_event, data) => {
12177
12264
  if (data.blockers.includes(blocker)) {
12178
12265
  unused.delete(data.ruleId);
12179
12266
  }
@@ -12189,10 +12276,10 @@ class Engine {
12189
12276
  for (const rule of rules) {
12190
12277
  rule.block(blocker);
12191
12278
  }
12192
- const unregister = parser.on("tag:start", (event, data) => {
12279
+ const unregister = parser.on("tag:start", (_event, data) => {
12193
12280
  data.target.blockRules(ruleIds, blocker);
12194
12281
  });
12195
- parser.on("rule:error", (event, data) => {
12282
+ parser.on("rule:error", (_event, data) => {
12196
12283
  if (data.blockers.includes(blocker)) {
12197
12284
  unused.delete(data.ruleId);
12198
12285
  }
@@ -12756,7 +12843,7 @@ class HtmlValidate {
12756
12843
  }
12757
12844
 
12758
12845
  const name = "html-validate";
12759
- const version = "8.20.1";
12846
+ const version = "8.22.0";
12760
12847
  const bugs = "https://gitlab.com/html-validate/html-validate/issues/new";
12761
12848
 
12762
12849
  function definePlugin(plugin) {
@@ -12844,17 +12931,26 @@ const REPLACERS = [
12844
12931
  [
12845
12932
  // (a\ ) -> (a )
12846
12933
  // (a ) -> (a)
12934
+ // (a ) -> (a)
12847
12935
  // (a \ ) -> (a )
12848
- /\\?\s+$/,
12849
- match => match.indexOf('\\') === 0
12850
- ? SPACE
12851
- : EMPTY
12936
+ /((?:\\\\)*?)(\\?\s+)$/,
12937
+ (_, m1, m2) => m1 + (
12938
+ m2.indexOf('\\') === 0
12939
+ ? SPACE
12940
+ : EMPTY
12941
+ )
12852
12942
  ],
12853
12943
 
12854
12944
  // replace (\ ) with ' '
12945
+ // (\ ) -> ' '
12946
+ // (\\ ) -> '\\ '
12947
+ // (\\\ ) -> '\\ '
12855
12948
  [
12856
- /\\\s/g,
12857
- () => SPACE
12949
+ /(\\+?)\s/g,
12950
+ (_, m1) => {
12951
+ const {length} = m1;
12952
+ return m1.slice(0, length - length % 2) + SPACE
12953
+ }
12858
12954
  ],
12859
12955
 
12860
12956
  // Escape metacharacters
@@ -13082,7 +13178,8 @@ const makeRegex = (pattern, ignoreCase) => {
13082
13178
 
13083
13179
  if (!source) {
13084
13180
  source = REPLACERS.reduce(
13085
- (prev, current) => prev.replace(current[0], current[1].bind(pattern)),
13181
+ (prev, [matcher, replacer]) =>
13182
+ prev.replace(matcher, replacer.bind(pattern)),
13086
13183
  pattern
13087
13184
  );
13088
13185
  regexCache[pattern] = source;
@@ -13448,6 +13545,81 @@ const defaults = {
13448
13545
  showSummary: true,
13449
13546
  showSelector: false
13450
13547
  };
13548
+ const NEWLINE = /\r\n|[\n\r\u2028\u2029]/;
13549
+ function getMarkerLines(loc, source) {
13550
+ const startLoc = {
13551
+ ...loc.start
13552
+ };
13553
+ const endLoc = {
13554
+ ...startLoc,
13555
+ ...loc.end
13556
+ };
13557
+ const linesAbove = 2;
13558
+ const linesBelow = 3;
13559
+ const startLine = startLoc.line;
13560
+ const startColumn = startLoc.column;
13561
+ const endLine = endLoc.line;
13562
+ const endColumn = endLoc.column;
13563
+ const start = Math.max(startLine - (linesAbove + 1), 0);
13564
+ const end = Math.min(source.length, endLine + linesBelow);
13565
+ const lineDiff = endLine - startLine;
13566
+ const markerLines = {};
13567
+ if (lineDiff) {
13568
+ for (let i = 0; i <= lineDiff; i++) {
13569
+ const lineNumber = i + startLine;
13570
+ if (!startColumn) {
13571
+ markerLines[lineNumber] = true;
13572
+ } else if (i === 0) {
13573
+ const sourceLength = source[lineNumber - 1].length;
13574
+ markerLines[lineNumber] = [startColumn, sourceLength - startColumn + 1];
13575
+ } else if (i === lineDiff) {
13576
+ markerLines[lineNumber] = [0, endColumn];
13577
+ } else {
13578
+ const sourceLength = source[lineNumber - i].length;
13579
+ markerLines[lineNumber] = [0, sourceLength];
13580
+ }
13581
+ }
13582
+ } else {
13583
+ if (startColumn === endColumn) {
13584
+ if (startColumn) {
13585
+ markerLines[startLine] = [startColumn, 0];
13586
+ } else {
13587
+ markerLines[startLine] = true;
13588
+ }
13589
+ } else {
13590
+ markerLines[startLine] = [startColumn, endColumn - startColumn];
13591
+ }
13592
+ }
13593
+ return { start, end, markerLines };
13594
+ }
13595
+ function codeFrameColumns(rawLines, loc) {
13596
+ const lines = rawLines.split(NEWLINE);
13597
+ const { start, end, markerLines } = getMarkerLines(loc, lines);
13598
+ const numberMaxWidth = String(end).length;
13599
+ return rawLines.split(NEWLINE, end).slice(start, end).map((line, index) => {
13600
+ const number = start + 1 + index;
13601
+ const paddedNumber = ` ${String(number)}`.slice(-numberMaxWidth);
13602
+ const gutter = ` ${paddedNumber} |`;
13603
+ const hasMarker = markerLines[number];
13604
+ if (hasMarker) {
13605
+ let markerLine = "";
13606
+ if (Array.isArray(hasMarker)) {
13607
+ const markerSpacing = line.slice(0, Math.max(hasMarker[0] - 1, 0)).replace(/[^\t]/g, " ");
13608
+ const numberOfMarkers = hasMarker[1] || 1;
13609
+ markerLine = [
13610
+ "\n ",
13611
+ gutter.replace(/\d/g, " "),
13612
+ " ",
13613
+ markerSpacing,
13614
+ "^".repeat(numberOfMarkers)
13615
+ ].join("");
13616
+ }
13617
+ return [">", gutter, line.length > 0 ? ` ${line}` : "", markerLine].join("");
13618
+ } else {
13619
+ return [" ", gutter, line.length > 0 ? ` ${line}` : ""].join("");
13620
+ }
13621
+ }).join("\n");
13622
+ }
13451
13623
  function pluralize(word, count) {
13452
13624
  return count === 1 ? word : `${word}s`;
13453
13625
  }
@@ -13490,16 +13662,11 @@ function formatMessage(message, parentResult, options) {
13490
13662
  ].filter(String).join(" ");
13491
13663
  const result = [firstLine];
13492
13664
  if (sourceCode) {
13493
- result.push(
13494
- codeFrame.codeFrameColumns(
13495
- sourceCode,
13496
- {
13497
- start: getStartLocation(message),
13498
- end: getEndLocation(message, sourceCode)
13499
- },
13500
- { highlightCode: false }
13501
- )
13502
- );
13665
+ const output = codeFrameColumns(sourceCode, {
13666
+ start: getStartLocation(message),
13667
+ end: getEndLocation(message, sourceCode)
13668
+ });
13669
+ result.push(output);
13503
13670
  }
13504
13671
  if (options.showSelector) {
13505
13672
  result.push(`${kleur__default.default.bold("Selector:")} ${message.selector ?? "-"}`);
@@ -13678,4 +13845,5 @@ exports.ruleExists = ruleExists;
13678
13845
  exports.sliceLocation = sliceLocation;
13679
13846
  exports.staticResolver = staticResolver;
13680
13847
  exports.version = version;
13848
+ exports.walk = walk;
13681
13849
  //# sourceMappingURL=core.js.map