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/es/core.js CHANGED
@@ -3,7 +3,6 @@ import { e as entities$1, h as html5, b as bundledElements } from './elements.js
3
3
  import betterAjvErrors from '@sidvind/better-ajv-errors';
4
4
  import { n as naturalJoin } from './utils/natural-join.js';
5
5
  import fs from 'fs';
6
- import { codeFrameColumns } from '@babel/code-frame';
7
6
  import kleur from 'kleur';
8
7
  import { stylish as stylish$1 } from '@html-validate/stylish';
9
8
  import semver from 'semver';
@@ -1269,14 +1268,12 @@ class MetaTable {
1269
1268
  * @returns A shallow copy of metadata.
1270
1269
  */
1271
1270
  getMetaFor(tagName) {
1272
- tagName = tagName.toLowerCase();
1273
- if (this.elements[tagName]) {
1274
- return { ...this.elements[tagName] };
1275
- }
1276
- if (this.elements["*"]) {
1277
- return { ...this.elements["*"] };
1271
+ const meta = this.elements[tagName.toLowerCase()] ?? this.elements["*"];
1272
+ if (meta) {
1273
+ return { ...meta };
1274
+ } else {
1275
+ return null;
1278
1276
  }
1279
- return null;
1280
1277
  }
1281
1278
  /**
1282
1279
  * Find all tags which has enabled given property.
@@ -1284,7 +1281,7 @@ class MetaTable {
1284
1281
  * @public
1285
1282
  */
1286
1283
  getTagsWithProperty(propName) {
1287
- return Object.entries(this.elements).filter(([, entry]) => entry[propName]).map(([tagName]) => tagName);
1284
+ return this.entries.filter(([, entry]) => entry[propName]).map(([tagName]) => tagName);
1288
1285
  }
1289
1286
  /**
1290
1287
  * Find tag matching tagName or inheriting from it.
@@ -1292,10 +1289,10 @@ class MetaTable {
1292
1289
  * @public
1293
1290
  */
1294
1291
  getTagsDerivedFrom(tagName) {
1295
- return Object.entries(this.elements).filter(([key, entry]) => key === tagName || entry.inherit === tagName).map(([tagName2]) => tagName2);
1292
+ return this.entries.filter(([key, entry]) => key === tagName || entry.inherit === tagName).map(([tagName2]) => tagName2);
1296
1293
  }
1297
1294
  addEntry(tagName, entry) {
1298
- let parent = this.elements[tagName] || {};
1295
+ let parent = this.elements[tagName];
1299
1296
  if (entry.inherit) {
1300
1297
  const name = entry.inherit;
1301
1298
  parent = this.elements[name];
@@ -1306,7 +1303,7 @@ class MetaTable {
1306
1303
  });
1307
1304
  }
1308
1305
  }
1309
- const expanded = this.mergeElement(parent, { ...entry, tagName });
1306
+ const expanded = this.mergeElement(parent ?? {}, { ...entry, tagName });
1310
1307
  expandRegex(expanded);
1311
1308
  this.elements[tagName] = expanded;
1312
1309
  }
@@ -1335,6 +1332,12 @@ class MetaTable {
1335
1332
  getJSONSchema() {
1336
1333
  return this.schema;
1337
1334
  }
1335
+ /**
1336
+ * @internal
1337
+ */
1338
+ get entries() {
1339
+ return Object.entries(this.elements);
1340
+ }
1338
1341
  /**
1339
1342
  * Finds the global element definition and merges each known element with the
1340
1343
  * global, e.g. to assign global attributes.
@@ -1346,7 +1349,7 @@ class MetaTable {
1346
1349
  delete this.elements["*"];
1347
1350
  delete global.tagName;
1348
1351
  delete global.void;
1349
- for (const [tagName, entry] of Object.entries(this.elements)) {
1352
+ for (const [tagName, entry] of this.entries) {
1350
1353
  this.elements[tagName] = this.mergeElement(global, entry);
1351
1354
  }
1352
1355
  }
@@ -1667,6 +1670,7 @@ class DOMNode {
1667
1670
  /**
1668
1671
  * Create a new DOMNode.
1669
1672
  *
1673
+ * @internal
1670
1674
  * @param nodeType - What node type to create.
1671
1675
  * @param nodeName - What node name to use. For `HtmlElement` this corresponds
1672
1676
  * to the tagName but other node types have specific predefined values.
@@ -2134,7 +2138,7 @@ class AttrMatcher extends Matcher {
2134
2138
  this.value = value;
2135
2139
  }
2136
2140
  match(node) {
2137
- const attr = node.getAttribute(this.key, true) || [];
2141
+ const attr = node.getAttribute(this.key, true);
2138
2142
  return attr.some((cur) => {
2139
2143
  switch (this.op) {
2140
2144
  case void 0:
@@ -2335,8 +2339,15 @@ function createAdapter(node) {
2335
2339
  };
2336
2340
  }
2337
2341
  class HtmlElement extends DOMNode {
2338
- constructor(tagName, parent, closed, meta, location) {
2339
- const nodeType = tagName ? NodeType.ELEMENT_NODE : NodeType.DOCUMENT_NODE;
2342
+ constructor(details) {
2343
+ const {
2344
+ nodeType,
2345
+ tagName,
2346
+ parent = null,
2347
+ closed = 1 /* EndTag */,
2348
+ meta = null,
2349
+ location
2350
+ } = details;
2340
2351
  super(nodeType, tagName, location);
2341
2352
  if (isInvalidTagName(tagName)) {
2342
2353
  throw new Error(`The tag name provided ("${tagName}") is not a valid name`);
@@ -2359,11 +2370,39 @@ class HtmlElement extends DOMNode {
2359
2370
  }
2360
2371
  }
2361
2372
  }
2373
+ /**
2374
+ * Manually create a new element. This is primary useful for test-cases. While
2375
+ * the API is public it is not meant for general consumption and is not
2376
+ * guaranteed to be stable across versions.
2377
+ *
2378
+ * Use at your own risk. Prefer to use [[Parser]] to parse a string of markup
2379
+ * instead.
2380
+ *
2381
+ * @public
2382
+ * @since 8.22.0
2383
+ * @param tagName - Element tagname.
2384
+ * @param location - Element location.
2385
+ * @param details - Additional element details.
2386
+ */
2387
+ static createElement(tagName, location, details = {}) {
2388
+ const { closed = 1 /* EndTag */, meta = null, parent = null } = details;
2389
+ return new HtmlElement({
2390
+ nodeType: NodeType.ELEMENT_NODE,
2391
+ tagName,
2392
+ parent,
2393
+ closed,
2394
+ meta,
2395
+ location
2396
+ });
2397
+ }
2362
2398
  /**
2363
2399
  * @internal
2364
2400
  */
2365
2401
  static rootNode(location) {
2366
- const root = new HtmlElement(void 0, null, 1 /* EndTag */, null, location);
2402
+ const root = new HtmlElement({
2403
+ nodeType: NodeType.DOCUMENT_NODE,
2404
+ location
2405
+ });
2367
2406
  root.setAnnotation("#document");
2368
2407
  return root;
2369
2408
  }
@@ -2382,7 +2421,14 @@ class HtmlElement extends DOMNode {
2382
2421
  const open = startToken.data[1] !== "/";
2383
2422
  const closed = isClosed(endToken, meta);
2384
2423
  const location = sliceLocation(startToken.location, 1);
2385
- return new HtmlElement(tagName, open ? parent : null, closed, meta, location);
2424
+ return new HtmlElement({
2425
+ nodeType: NodeType.ELEMENT_NODE,
2426
+ tagName,
2427
+ parent: open ? parent : null,
2428
+ closed,
2429
+ meta,
2430
+ location
2431
+ });
2386
2432
  }
2387
2433
  /**
2388
2434
  * Returns annotated name if set or defaults to `<tagName>`.
@@ -2580,10 +2626,13 @@ class HtmlElement extends DOMNode {
2580
2626
  */
2581
2627
  setAttribute(key, value, keyLocation, valueLocation, originalAttribute) {
2582
2628
  key = key.toLowerCase();
2583
- if (!this.attr[key]) {
2584
- this.attr[key] = [];
2629
+ const attr = new Attribute(key, value, keyLocation, valueLocation, originalAttribute);
2630
+ const list = this.attr[key];
2631
+ if (list) {
2632
+ list.push(attr);
2633
+ } else {
2634
+ this.attr[key] = [attr];
2585
2635
  }
2586
- this.attr[key].push(new Attribute(key, value, keyLocation, valueLocation, originalAttribute));
2587
2636
  }
2588
2637
  /**
2589
2638
  * Get parsed tabindex for this element.
@@ -2640,7 +2689,7 @@ class HtmlElement extends DOMNode {
2640
2689
  const matches = this.attr[key];
2641
2690
  return all ? matches : matches[0];
2642
2691
  } else {
2643
- return null;
2692
+ return all ? [] : null;
2644
2693
  }
2645
2694
  }
2646
2695
  /**
@@ -2746,20 +2795,6 @@ class HtmlElement extends DOMNode {
2746
2795
  yield* pattern.match(this);
2747
2796
  }
2748
2797
  }
2749
- /**
2750
- * Visit all nodes from this node and down. Depth first.
2751
- *
2752
- * @internal
2753
- */
2754
- visitDepthFirst(callback) {
2755
- function visit(node) {
2756
- node.childElements.forEach(visit);
2757
- if (!node.isRootElement()) {
2758
- callback(node);
2759
- }
2760
- }
2761
- visit(this);
2762
- }
2763
2798
  /**
2764
2799
  * Evaluates callbackk on all descendants, returning true if any are true.
2765
2800
  *
@@ -2823,738 +2858,309 @@ function isClosed(endToken, meta) {
2823
2858
  return closed;
2824
2859
  }
2825
2860
 
2826
- class DOMTree {
2827
- constructor(location) {
2828
- this.root = HtmlElement.rootNode(location);
2829
- this.active = this.root;
2830
- this.doctype = null;
2831
- }
2832
- pushActive(node) {
2833
- this.active = node;
2834
- }
2835
- popActive() {
2836
- if (this.active.isRootElement()) {
2837
- return;
2861
+ function dumpTree(root) {
2862
+ const lines = [];
2863
+ function decoration(node) {
2864
+ let output = "";
2865
+ if (node.id) {
2866
+ output += `#${node.id}`;
2838
2867
  }
2839
- this.active = this.active.parent ?? this.root;
2840
- }
2841
- getActive() {
2842
- return this.active;
2868
+ if (node.hasAttribute("class")) {
2869
+ output += `.${node.classList.join(".")}`;
2870
+ }
2871
+ return output;
2843
2872
  }
2844
- /**
2845
- * Resolve dynamic meta expressions.
2846
- */
2847
- resolveMeta(table) {
2848
- this.visitDepthFirst((node) => {
2849
- table.resolve(node);
2873
+ function writeNode(node, level, sibling) {
2874
+ if (node.parent) {
2875
+ const indent = " ".repeat(level - 1);
2876
+ const l = node.childElements.length > 0 ? "\u252C" : "\u2500";
2877
+ const b = sibling < node.parent.childElements.length - 1 ? "\u251C" : "\u2514";
2878
+ lines.push(`${indent}${b}\u2500${l} ${node.tagName}${decoration(node)}`);
2879
+ } else {
2880
+ lines.push("(root)");
2881
+ }
2882
+ node.childElements.forEach((child, index) => {
2883
+ writeNode(child, level + 1, index);
2850
2884
  });
2851
2885
  }
2852
- getElementsByTagName(tagName) {
2853
- return this.root.getElementsByTagName(tagName);
2886
+ writeNode(root, 0, 0);
2887
+ return lines;
2888
+ }
2889
+
2890
+ function escape(value) {
2891
+ return JSON.stringify(value);
2892
+ }
2893
+ function format(value, quote = false) {
2894
+ if (value === null) {
2895
+ return "null";
2854
2896
  }
2855
- visitDepthFirst(callback) {
2856
- this.root.visitDepthFirst(callback);
2897
+ if (typeof value === "number") {
2898
+ return value.toString();
2857
2899
  }
2858
- find(callback) {
2859
- return this.root.find(callback);
2900
+ if (typeof value === "string") {
2901
+ return quote ? escape(value) : value;
2860
2902
  }
2861
- querySelector(selector) {
2862
- return this.root.querySelector(selector);
2903
+ if (Array.isArray(value)) {
2904
+ const content = value.map((it) => format(it, true)).join(", ");
2905
+ return `[ ${content} ]`;
2863
2906
  }
2864
- querySelectorAll(selector) {
2865
- return this.root.querySelectorAll(selector);
2907
+ if (typeof value === "object") {
2908
+ const content = Object.entries(value).map(([key, nested]) => `${key}: ${format(nested, true)}`).join(", ");
2909
+ return `{ ${content} }`;
2866
2910
  }
2911
+ return String(value);
2912
+ }
2913
+ function interpolate(text, data) {
2914
+ return text.replace(/{{\s*([^\s{}]+)\s*}}/g, (match, key) => {
2915
+ return typeof data[key] !== "undefined" ? format(data[key]) : match;
2916
+ });
2867
2917
  }
2868
2918
 
2869
- const allowedKeys = ["exclude"];
2870
- class Validator {
2871
- /**
2872
- * Test if element is used in a proper context.
2873
- *
2874
- * @param node - Element to test.
2875
- * @param rules - List of rules.
2876
- * @returns `true` if element passes all tests.
2877
- */
2878
- static validatePermitted(node, rules) {
2879
- if (!rules) {
2880
- return true;
2881
- }
2882
- return rules.some((rule) => {
2883
- return Validator.validatePermittedRule(node, rule);
2884
- });
2919
+ function isThenable(value) {
2920
+ return value && typeof value === "object" && "then" in value && typeof value.then === "function";
2921
+ }
2922
+
2923
+ var Severity = /* @__PURE__ */ ((Severity2) => {
2924
+ Severity2[Severity2["DISABLED"] = 0] = "DISABLED";
2925
+ Severity2[Severity2["WARN"] = 1] = "WARN";
2926
+ Severity2[Severity2["ERROR"] = 2] = "ERROR";
2927
+ return Severity2;
2928
+ })(Severity || {});
2929
+ function parseSeverity(value) {
2930
+ switch (value) {
2931
+ case 0:
2932
+ case "off":
2933
+ return 0 /* DISABLED */;
2934
+ case 1:
2935
+ case "warn":
2936
+ return 1 /* WARN */;
2937
+ case 2:
2938
+ case "error":
2939
+ return 2 /* ERROR */;
2940
+ default:
2941
+ throw new Error(`Invalid severity "${String(value)}"`);
2885
2942
  }
2886
- /**
2887
- * Test if an element is used the correct amount of times.
2888
- *
2889
- * For instance, a `<table>` element can only contain a single `<tbody>`
2890
- * child. If multiple `<tbody>` exists this test will fail both nodes.
2891
- * Note that this is called on the parent but will fail the children violating
2892
- * the rule.
2893
- *
2894
- * @param children - Array of children to validate.
2895
- * @param rules - List of rules of the parent element.
2896
- * @returns `true` if the parent element of the children passes the test.
2897
- */
2898
- static validateOccurrences(children, rules, cb) {
2899
- if (!rules) {
2900
- return true;
2901
- }
2902
- let valid = true;
2903
- for (const rule of rules) {
2904
- if (typeof rule !== "string") {
2905
- return false;
2906
- }
2907
- const [, category, quantifier] = rule.match(/^(@?.*?)([?*]?)$/);
2908
- const limit = category && quantifier && parseQuantifier(quantifier);
2909
- if (limit) {
2910
- const siblings = children.filter(
2911
- (cur) => Validator.validatePermittedCategory(cur, rule, true)
2912
- );
2913
- if (siblings.length > limit) {
2914
- for (const child of siblings.slice(limit)) {
2915
- cb(child, category);
2916
- }
2917
- valid = false;
2918
- }
2919
- }
2920
- }
2921
- return valid;
2943
+ }
2944
+
2945
+ const cacheKey = Symbol("aria-naming");
2946
+ const defaultValue = "allowed";
2947
+ const prohibitedRoles = [
2948
+ "caption",
2949
+ "code",
2950
+ "deletion",
2951
+ "emphasis",
2952
+ "generic",
2953
+ "insertion",
2954
+ "paragraph",
2955
+ "presentation",
2956
+ "strong",
2957
+ "subscript",
2958
+ "superscript"
2959
+ ];
2960
+ function byRole(role) {
2961
+ return prohibitedRoles.includes(role) ? "prohibited" : "allowed";
2962
+ }
2963
+ function byMeta(element, meta) {
2964
+ return meta.aria.naming(element._adapter);
2965
+ }
2966
+ function ariaNaming(element) {
2967
+ var _a;
2968
+ const cached = element.cacheGet(cacheKey);
2969
+ if (cached) {
2970
+ return cached;
2922
2971
  }
2923
- /**
2924
- * Validate elements order.
2925
- *
2926
- * Given a parent element with children and metadata containing permitted
2927
- * order it will validate each children and ensure each one exists in the
2928
- * specified order.
2929
- *
2930
- * For instance, for a `<table>` element the `<caption>` element must come
2931
- * before a `<thead>` which must come before `<tbody>`.
2932
- *
2933
- * @param children - Array of children to validate.
2934
- */
2935
- static validateOrder(children, rules, cb) {
2936
- if (!rules) {
2937
- return true;
2938
- }
2939
- let i = 0;
2940
- let prev = null;
2941
- for (const node of children) {
2942
- const old = i;
2943
- while (rules[i] && !Validator.validatePermittedCategory(node, rules[i], true)) {
2944
- i++;
2945
- }
2946
- if (i >= rules.length) {
2947
- const orderSpecified = rules.find(
2948
- (cur) => Validator.validatePermittedCategory(node, cur, true)
2949
- );
2950
- if (orderSpecified) {
2951
- cb(node, prev);
2952
- return false;
2953
- }
2954
- i = old;
2955
- }
2956
- prev = node;
2972
+ const role = (_a = element.getAttribute("role")) == null ? void 0 : _a.value;
2973
+ if (role) {
2974
+ if (role instanceof DynamicValue) {
2975
+ return element.cacheSet(cacheKey, defaultValue);
2976
+ } else {
2977
+ return element.cacheSet(cacheKey, byRole(role));
2957
2978
  }
2958
- return true;
2959
2979
  }
2960
- /**
2961
- * Validate element ancestors.
2962
- *
2963
- * Check if an element has the required set of elements. At least one of the
2964
- * selectors must match.
2965
- */
2966
- static validateAncestors(node, rules) {
2967
- if (!rules || rules.length === 0) {
2968
- return true;
2969
- }
2970
- return rules.some((rule) => node.closest(rule));
2980
+ const meta = element.meta;
2981
+ if (!meta) {
2982
+ return element.cacheSet(cacheKey, defaultValue);
2971
2983
  }
2972
- /**
2973
- * Validate element required content.
2974
- *
2975
- * Check if an element has the required set of elements. At least one of the
2976
- * selectors must match.
2977
- *
2978
- * Returns `[]` when valid or a list of required but missing tagnames or
2979
- * categories.
2980
- */
2981
- static validateRequiredContent(node, rules) {
2982
- if (!rules || rules.length === 0) {
2983
- return [];
2984
- }
2985
- return rules.filter((tagName) => {
2986
- const haveMatchingChild = node.childElements.some(
2987
- (child) => Validator.validatePermittedCategory(child, tagName, false)
2988
- );
2989
- return !haveMatchingChild;
2990
- });
2984
+ return element.cacheSet(cacheKey, byMeta(element, meta));
2985
+ }
2986
+
2987
+ const patternCache = /* @__PURE__ */ new Map();
2988
+ function compileStringPattern(pattern) {
2989
+ const regexp = pattern.replace(/[*]+/g, ".+");
2990
+ return new RegExp(`^${regexp}$`);
2991
+ }
2992
+ function compileRegExpPattern(pattern) {
2993
+ return new RegExp(`^${pattern}$`);
2994
+ }
2995
+ function compilePattern(pattern) {
2996
+ const cached = patternCache.get(pattern);
2997
+ if (cached) {
2998
+ return cached;
2991
2999
  }
2992
- /**
2993
- * Test if an attribute has an allowed value and/or format.
2994
- *
2995
- * @param attr - Attribute to test.
2996
- * @param rules - Element attribute metadta.
2997
- * @returns `true` if attribute passes all tests.
2998
- */
2999
- static validateAttribute(attr, rules) {
3000
- const rule = rules[attr.key];
3001
- if (!rule) {
3002
- return true;
3003
- }
3004
- const value = attr.value;
3005
- if (value instanceof DynamicValue) {
3006
- return true;
3007
- }
3008
- const empty = value === null || value === "";
3009
- if (rule.boolean) {
3010
- return empty || value === attr.key;
3011
- }
3012
- if (rule.omit && empty) {
3000
+ const match = pattern.match(/^\/(.*)\/$/);
3001
+ const regexp = match ? compileRegExpPattern(match[1]) : compileStringPattern(pattern);
3002
+ patternCache.set(pattern, regexp);
3003
+ return regexp;
3004
+ }
3005
+ function keywordPatternMatcher(list, keyword) {
3006
+ for (const pattern of list) {
3007
+ const regexp = compilePattern(pattern);
3008
+ if (regexp.test(keyword)) {
3013
3009
  return true;
3014
3010
  }
3015
- if (rule.list) {
3016
- const tokens = new DOMTokenList(value, attr.valueLocation);
3017
- return tokens.every((token) => {
3018
- return this.validateAttributeValue(token, rule);
3019
- });
3020
- }
3021
- return this.validateAttributeValue(value, rule);
3022
3011
  }
3023
- static validateAttributeValue(value, rule) {
3024
- if (!rule.enum) {
3025
- return true;
3026
- }
3027
- if (value === null || value === void 0) {
3028
- return false;
3029
- }
3030
- const caseInsensitiveValue = value.toLowerCase();
3031
- return rule.enum.some((entry) => {
3032
- if (entry instanceof RegExp) {
3033
- return !!value.match(entry);
3034
- } else {
3035
- return caseInsensitiveValue === entry;
3036
- }
3037
- });
3012
+ return false;
3013
+ }
3014
+ function isKeywordIgnored(options, keyword, matcher = (list, it) => list.includes(it)) {
3015
+ const { include, exclude } = options;
3016
+ if (include && !matcher(include, keyword)) {
3017
+ return true;
3038
3018
  }
3039
- static validatePermittedRule(node, rule, isExclude = false) {
3040
- if (typeof rule === "string") {
3041
- return Validator.validatePermittedCategory(node, rule, !isExclude);
3042
- } else if (Array.isArray(rule)) {
3043
- return rule.every((inner) => {
3044
- return Validator.validatePermittedRule(node, inner, isExclude);
3045
- });
3046
- } else {
3047
- validateKeys(rule);
3048
- if (rule.exclude) {
3049
- if (Array.isArray(rule.exclude)) {
3050
- return !rule.exclude.some((inner) => {
3051
- return Validator.validatePermittedRule(node, inner, true);
3052
- });
3053
- } else {
3054
- return !Validator.validatePermittedRule(node, rule.exclude, true);
3055
- }
3056
- } else {
3057
- return true;
3058
- }
3059
- }
3019
+ if (exclude && matcher(exclude, keyword)) {
3020
+ return true;
3060
3021
  }
3061
- /**
3062
- * Validate node against a content category.
3063
- *
3064
- * When matching parent nodes against permitted parents use the superset
3065
- * parameter to also match for `@flow`. E.g. if a node expects a `@phrasing`
3066
- * parent it should also allow `@flow` parent since `@phrasing` is a subset of
3067
- * `@flow`.
3068
- *
3069
- * @param node - The node to test against
3070
- * @param category - Name of category with `@` prefix or tag name.
3071
- * @param defaultMatch - The default return value when node categories is not known.
3072
- */
3073
- /* eslint-disable-next-line complexity -- rule does not like switch */
3074
- static validatePermittedCategory(node, category, defaultMatch) {
3075
- const [, rawCategory] = category.match(/^(@?.*?)([?*]?)$/);
3076
- if (!rawCategory.startsWith("@")) {
3077
- return node.tagName === rawCategory;
3078
- }
3079
- if (!node.meta) {
3080
- return defaultMatch;
3081
- }
3082
- switch (rawCategory) {
3083
- case "@meta":
3084
- return node.meta.metadata;
3085
- case "@flow":
3086
- return node.meta.flow;
3087
- case "@sectioning":
3088
- return node.meta.sectioning;
3089
- case "@heading":
3090
- return node.meta.heading;
3091
- case "@phrasing":
3092
- return node.meta.phrasing;
3093
- case "@embedded":
3094
- return node.meta.embedded;
3095
- case "@interactive":
3096
- return node.meta.interactive;
3097
- case "@script":
3098
- return Boolean(node.meta.scriptSupporting);
3099
- case "@form":
3100
- return Boolean(node.meta.form);
3101
- default:
3102
- throw new Error(`Invalid content category "${category}"`);
3103
- }
3022
+ return false;
3023
+ }
3024
+
3025
+ const ARIA_HIDDEN_CACHE = Symbol(isAriaHidden.name);
3026
+ const HTML_HIDDEN_CACHE = Symbol(isHTMLHidden.name);
3027
+ const INERT_CACHE = Symbol(isInert.name);
3028
+ const ROLE_PRESENTATION_CACHE = Symbol(isPresentation.name);
3029
+ const STYLE_HIDDEN_CACHE = Symbol(isStyleHidden.name);
3030
+ function inAccessibilityTree(node) {
3031
+ if (isAriaHidden(node)) {
3032
+ return false;
3104
3033
  }
3034
+ if (isPresentation(node)) {
3035
+ return false;
3036
+ }
3037
+ if (isHTMLHidden(node)) {
3038
+ return false;
3039
+ }
3040
+ if (isInert(node)) {
3041
+ return false;
3042
+ }
3043
+ if (isStyleHidden(node)) {
3044
+ return false;
3045
+ }
3046
+ return true;
3105
3047
  }
3106
- function validateKeys(rule) {
3107
- for (const key of Object.keys(rule)) {
3108
- if (!allowedKeys.includes(key)) {
3109
- const str = JSON.stringify(rule);
3110
- throw new Error(`Permitted rule "${str}" contains unknown property "${key}"`);
3111
- }
3048
+ function isAriaHiddenImpl(node) {
3049
+ const getAriaHiddenAttr = (node2) => {
3050
+ const ariaHidden = node2.getAttribute("aria-hidden");
3051
+ return Boolean(ariaHidden && ariaHidden.value === "true");
3052
+ };
3053
+ return {
3054
+ byParent: node.parent ? isAriaHidden(node.parent) : false,
3055
+ bySelf: getAriaHiddenAttr(node)
3056
+ };
3057
+ }
3058
+ function isAriaHidden(node, details) {
3059
+ const cached = node.cacheGet(ARIA_HIDDEN_CACHE);
3060
+ if (cached) {
3061
+ return details ? cached : cached.byParent || cached.bySelf;
3112
3062
  }
3063
+ const result = node.cacheSet(ARIA_HIDDEN_CACHE, isAriaHiddenImpl(node));
3064
+ return details ? result : result.byParent || result.bySelf;
3113
3065
  }
3114
- function parseQuantifier(quantifier) {
3115
- switch (quantifier) {
3116
- case "?":
3117
- return 1;
3118
- case "*":
3119
- return null;
3120
- default:
3121
- throw new Error(`Invalid quantifier "${quantifier}" used`);
3066
+ function isHTMLHiddenImpl(node) {
3067
+ const getHiddenAttr = (node2) => {
3068
+ const hidden = node2.getAttribute("hidden");
3069
+ return Boolean(hidden == null ? void 0 : hidden.isStatic);
3070
+ };
3071
+ return {
3072
+ byParent: node.parent ? isHTMLHidden(node.parent) : false,
3073
+ bySelf: getHiddenAttr(node)
3074
+ };
3075
+ }
3076
+ function isHTMLHidden(node, details) {
3077
+ const cached = node.cacheGet(HTML_HIDDEN_CACHE);
3078
+ if (cached) {
3079
+ return details ? cached : cached.byParent || cached.bySelf;
3122
3080
  }
3081
+ const result = node.cacheSet(HTML_HIDDEN_CACHE, isHTMLHiddenImpl(node));
3082
+ return details ? result : result.byParent || result.bySelf;
3123
3083
  }
3124
-
3125
- const $schema = "http://json-schema.org/draft-06/schema#";
3126
- const $id = "https://html-validate.org/schemas/config.json";
3127
- const type = "object";
3128
- const additionalProperties = false;
3129
- const properties = {
3130
- $schema: {
3131
- type: "string"
3132
- },
3133
- root: {
3134
- type: "boolean",
3135
- title: "Mark as root configuration",
3136
- description: "If this is set to true no further configurations will be searched.",
3137
- "default": false
3138
- },
3139
- "extends": {
3140
- type: "array",
3141
- items: {
3142
- type: "string"
3143
- },
3144
- title: "Configurations to extend",
3145
- description: "Array of shareable or builtin configurations to extend."
3146
- },
3147
- elements: {
3148
- type: "array",
3149
- items: {
3150
- anyOf: [
3151
- {
3152
- type: "string"
3153
- },
3154
- {
3155
- type: "object"
3156
- }
3157
- ]
3158
- },
3159
- title: "Element metadata to load",
3160
- description: "Array of modules, plugins or files to load element metadata from. Use <rootDir> to refer to the folder with the package.json file.",
3161
- examples: [
3162
- [
3163
- "html-validate:recommended",
3164
- "plugin:recommended",
3165
- "module",
3166
- "./local-file.json"
3167
- ]
3168
- ]
3169
- },
3170
- plugins: {
3171
- type: "array",
3172
- items: {
3173
- anyOf: [
3174
- {
3175
- type: "string"
3176
- },
3177
- {
3178
- type: "object"
3179
- }
3180
- ]
3181
- },
3182
- title: "Plugins to load",
3183
- description: "Array of plugins load. Use <rootDir> to refer to the folder with the package.json file.",
3184
- examples: [
3185
- [
3186
- "my-plugin",
3187
- "./local-plugin"
3188
- ]
3189
- ]
3190
- },
3191
- transform: {
3192
- type: "object",
3193
- additionalProperties: {
3194
- type: "string"
3195
- },
3196
- title: "File transformations to use.",
3197
- description: "Object where key is regular expression to match filename and value is name of transformer.",
3198
- examples: [
3199
- {
3200
- "^.*\\.foo$": "my-transformer",
3201
- "^.*\\.bar$": "my-plugin",
3202
- "^.*\\.baz$": "my-plugin:named"
3203
- }
3204
- ]
3205
- },
3206
- rules: {
3207
- type: "object",
3208
- patternProperties: {
3209
- ".*": {
3210
- anyOf: [
3211
- {
3212
- "enum": [
3213
- 0,
3214
- 1,
3215
- 2,
3216
- "off",
3217
- "warn",
3218
- "error"
3219
- ]
3220
- },
3221
- {
3222
- type: "array",
3223
- minItems: 1,
3224
- maxItems: 1,
3225
- items: [
3226
- {
3227
- "enum": [
3228
- 0,
3229
- 1,
3230
- 2,
3231
- "off",
3232
- "warn",
3233
- "error"
3234
- ]
3235
- }
3236
- ]
3237
- },
3238
- {
3239
- type: "array",
3240
- minItems: 2,
3241
- maxItems: 2,
3242
- items: [
3243
- {
3244
- "enum": [
3245
- 0,
3246
- 1,
3247
- 2,
3248
- "off",
3249
- "warn",
3250
- "error"
3251
- ]
3252
- },
3253
- {
3254
- }
3255
- ]
3256
- }
3257
- ]
3258
- }
3259
- },
3260
- title: "Rule configuration.",
3261
- description: "Enable/disable rules, set severity. Some rules have additional configuration like style or patterns to use.",
3262
- examples: [
3263
- {
3264
- foo: "error",
3265
- bar: "off",
3266
- baz: [
3267
- "error",
3268
- {
3269
- style: "camelcase"
3270
- }
3271
- ]
3272
- }
3273
- ]
3274
- }
3275
- };
3276
- var configurationSchema = {
3277
- $schema: $schema,
3278
- $id: $id,
3279
- type: type,
3280
- additionalProperties: additionalProperties,
3281
- properties: properties
3282
- };
3283
-
3284
- const TRANSFORMER_API = {
3285
- VERSION: 1
3286
- };
3287
-
3288
- var Severity = /* @__PURE__ */ ((Severity2) => {
3289
- Severity2[Severity2["DISABLED"] = 0] = "DISABLED";
3290
- Severity2[Severity2["WARN"] = 1] = "WARN";
3291
- Severity2[Severity2["ERROR"] = 2] = "ERROR";
3292
- return Severity2;
3293
- })(Severity || {});
3294
- function parseSeverity(value) {
3295
- switch (value) {
3296
- case 0:
3297
- case "off":
3298
- return 0 /* DISABLED */;
3299
- case 1:
3300
- case "warn":
3301
- return 1 /* WARN */;
3302
- case 2:
3303
- case "error":
3304
- return 2 /* ERROR */;
3305
- default:
3306
- throw new Error(`Invalid severity "${String(value)}"`);
3084
+ function isInertImpl(node) {
3085
+ const getInertAttr = (node2) => {
3086
+ const inert = node2.getAttribute("inert");
3087
+ return Boolean(inert == null ? void 0 : inert.isStatic);
3088
+ };
3089
+ return {
3090
+ byParent: node.parent ? isInert(node.parent) : false,
3091
+ bySelf: getInertAttr(node)
3092
+ };
3093
+ }
3094
+ function isInert(node, details) {
3095
+ const cached = node.cacheGet(INERT_CACHE);
3096
+ if (cached) {
3097
+ return details ? cached : cached.byParent || cached.bySelf;
3307
3098
  }
3099
+ const result = node.cacheSet(INERT_CACHE, isInertImpl(node));
3100
+ return details ? result : result.byParent || result.bySelf;
3308
3101
  }
3309
-
3310
- function escape(value) {
3311
- return JSON.stringify(value);
3102
+ function isStyleHiddenImpl(node) {
3103
+ const getStyleAttr = (node2) => {
3104
+ const style = node2.getAttribute("style");
3105
+ const { display, visibility } = parseCssDeclaration(style == null ? void 0 : style.value);
3106
+ return display === "none" || visibility === "hidden";
3107
+ };
3108
+ const byParent = node.parent ? isStyleHidden(node.parent) : false;
3109
+ const bySelf = getStyleAttr(node);
3110
+ return byParent || bySelf;
3312
3111
  }
3313
- function format(value, quote = false) {
3314
- if (value === null) {
3315
- return "null";
3112
+ function isStyleHidden(node) {
3113
+ const cached = node.cacheGet(STYLE_HIDDEN_CACHE);
3114
+ if (cached) {
3115
+ return cached;
3316
3116
  }
3317
- if (typeof value === "number") {
3318
- return value.toString();
3117
+ return node.cacheSet(STYLE_HIDDEN_CACHE, isStyleHiddenImpl(node));
3118
+ }
3119
+ function isPresentation(node) {
3120
+ if (node.cacheExists(ROLE_PRESENTATION_CACHE)) {
3121
+ return Boolean(node.cacheGet(ROLE_PRESENTATION_CACHE));
3319
3122
  }
3320
- if (typeof value === "string") {
3321
- return quote ? escape(value) : value;
3123
+ const meta = node.meta;
3124
+ if (meta == null ? void 0 : meta.interactive) {
3125
+ return node.cacheSet(ROLE_PRESENTATION_CACHE, false);
3322
3126
  }
3323
- if (Array.isArray(value)) {
3324
- const content = value.map((it) => format(it, true)).join(", ");
3325
- return `[ ${content} ]`;
3127
+ const tabindex = node.getAttribute("tabindex");
3128
+ if (tabindex) {
3129
+ return node.cacheSet(ROLE_PRESENTATION_CACHE, false);
3326
3130
  }
3327
- if (typeof value === "object") {
3328
- const content = Object.entries(value).map(([key, nested]) => `${key}: ${format(nested, true)}`).join(", ");
3329
- return `{ ${content} }`;
3131
+ const role = node.getAttribute("role");
3132
+ if (role && (role.value === "presentation" || role.value === "none")) {
3133
+ return node.cacheSet(ROLE_PRESENTATION_CACHE, true);
3134
+ } else {
3135
+ return node.cacheSet(ROLE_PRESENTATION_CACHE, false);
3330
3136
  }
3331
- return String(value);
3332
- }
3333
- function interpolate(text, data) {
3334
- return text.replace(/{{\s*([^\s{}]+)\s*}}/g, (match, key) => {
3335
- return typeof data[key] !== "undefined" ? format(data[key]) : match;
3336
- });
3337
3137
  }
3338
3138
 
3339
- const cacheKey = Symbol("aria-naming");
3340
- const defaultValue = "allowed";
3341
- const prohibitedRoles = [
3342
- "caption",
3343
- "code",
3344
- "deletion",
3345
- "emphasis",
3346
- "generic",
3347
- "insertion",
3348
- "paragraph",
3349
- "presentation",
3350
- "strong",
3351
- "subscript",
3352
- "superscript"
3353
- ];
3354
- function byRole(role) {
3355
- return prohibitedRoles.includes(role) ? "prohibited" : "allowed";
3139
+ const cachePrefix = classifyNodeText.name;
3140
+ const HTML_CACHE_KEY = Symbol(`${cachePrefix}|html`);
3141
+ const A11Y_CACHE_KEY = Symbol(`${cachePrefix}|a11y`);
3142
+ const IGNORE_HIDDEN_ROOT_HTML_CACHE_KEY = Symbol(`${cachePrefix}|html|ignore-hidden-root`);
3143
+ const IGNORE_HIDDEN_ROOT_A11Y_CACHE_KEY = Symbol(`${cachePrefix}|a11y|ignore-hidden-root`);
3144
+ var TextClassification = /* @__PURE__ */ ((TextClassification2) => {
3145
+ TextClassification2[TextClassification2["EMPTY_TEXT"] = 0] = "EMPTY_TEXT";
3146
+ TextClassification2[TextClassification2["DYNAMIC_TEXT"] = 1] = "DYNAMIC_TEXT";
3147
+ TextClassification2[TextClassification2["STATIC_TEXT"] = 2] = "STATIC_TEXT";
3148
+ return TextClassification2;
3149
+ })(TextClassification || {});
3150
+ function getCachekey(options) {
3151
+ const { accessible = false, ignoreHiddenRoot = false } = options;
3152
+ if (accessible && ignoreHiddenRoot) {
3153
+ return IGNORE_HIDDEN_ROOT_A11Y_CACHE_KEY;
3154
+ } else if (ignoreHiddenRoot) {
3155
+ return IGNORE_HIDDEN_ROOT_HTML_CACHE_KEY;
3156
+ } else if (accessible) {
3157
+ return A11Y_CACHE_KEY;
3158
+ } else {
3159
+ return HTML_CACHE_KEY;
3160
+ }
3356
3161
  }
3357
- function byMeta(element, meta) {
3358
- return meta.aria.naming(element._adapter);
3359
- }
3360
- function ariaNaming(element) {
3361
- var _a;
3362
- const cached = element.cacheGet(cacheKey);
3363
- if (cached) {
3364
- return cached;
3365
- }
3366
- const role = (_a = element.getAttribute("role")) == null ? void 0 : _a.value;
3367
- if (role) {
3368
- if (role instanceof DynamicValue) {
3369
- return element.cacheSet(cacheKey, defaultValue);
3370
- } else {
3371
- return element.cacheSet(cacheKey, byRole(role));
3372
- }
3373
- }
3374
- const meta = element.meta;
3375
- if (!meta) {
3376
- return element.cacheSet(cacheKey, defaultValue);
3377
- }
3378
- return element.cacheSet(cacheKey, byMeta(element, meta));
3379
- }
3380
-
3381
- const patternCache = /* @__PURE__ */ new Map();
3382
- function compileStringPattern(pattern) {
3383
- const regexp = pattern.replace(/[*]+/g, ".+");
3384
- return new RegExp(`^${regexp}$`);
3385
- }
3386
- function compileRegExpPattern(pattern) {
3387
- return new RegExp(`^${pattern}$`);
3388
- }
3389
- function compilePattern(pattern) {
3390
- const cached = patternCache.get(pattern);
3391
- if (cached) {
3392
- return cached;
3393
- }
3394
- const match = pattern.match(/^\/(.*)\/$/);
3395
- const regexp = match ? compileRegExpPattern(match[1]) : compileStringPattern(pattern);
3396
- patternCache.set(pattern, regexp);
3397
- return regexp;
3398
- }
3399
- function keywordPatternMatcher(list, keyword) {
3400
- for (const pattern of list) {
3401
- const regexp = compilePattern(pattern);
3402
- if (regexp.test(keyword)) {
3403
- return true;
3404
- }
3405
- }
3406
- return false;
3407
- }
3408
- function isKeywordIgnored(options, keyword, matcher = (list, it) => list.includes(it)) {
3409
- const { include, exclude } = options;
3410
- if (include && !matcher(include, keyword)) {
3411
- return true;
3412
- }
3413
- if (exclude && matcher(exclude, keyword)) {
3414
- return true;
3415
- }
3416
- return false;
3417
- }
3418
-
3419
- const ARIA_HIDDEN_CACHE = Symbol(isAriaHidden.name);
3420
- const HTML_HIDDEN_CACHE = Symbol(isHTMLHidden.name);
3421
- const INERT_CACHE = Symbol(isInert.name);
3422
- const ROLE_PRESENTATION_CACHE = Symbol(isPresentation.name);
3423
- const STYLE_HIDDEN_CACHE = Symbol(isStyleHidden.name);
3424
- function inAccessibilityTree(node) {
3425
- if (isAriaHidden(node)) {
3426
- return false;
3427
- }
3428
- if (isPresentation(node)) {
3429
- return false;
3430
- }
3431
- if (isHTMLHidden(node)) {
3432
- return false;
3433
- }
3434
- if (isInert(node)) {
3435
- return false;
3436
- }
3437
- if (isStyleHidden(node)) {
3438
- return false;
3439
- }
3440
- return true;
3441
- }
3442
- function isAriaHiddenImpl(node) {
3443
- const getAriaHiddenAttr = (node2) => {
3444
- const ariaHidden = node2.getAttribute("aria-hidden");
3445
- return Boolean(ariaHidden && ariaHidden.value === "true");
3446
- };
3447
- return {
3448
- byParent: node.parent ? isAriaHidden(node.parent) : false,
3449
- bySelf: getAriaHiddenAttr(node)
3450
- };
3451
- }
3452
- function isAriaHidden(node, details) {
3453
- const cached = node.cacheGet(ARIA_HIDDEN_CACHE);
3454
- if (cached) {
3455
- return details ? cached : cached.byParent || cached.bySelf;
3456
- }
3457
- const result = node.cacheSet(ARIA_HIDDEN_CACHE, isAriaHiddenImpl(node));
3458
- return details ? result : result.byParent || result.bySelf;
3459
- }
3460
- function isHTMLHiddenImpl(node) {
3461
- const getHiddenAttr = (node2) => {
3462
- const hidden = node2.getAttribute("hidden");
3463
- return Boolean(hidden == null ? void 0 : hidden.isStatic);
3464
- };
3465
- return {
3466
- byParent: node.parent ? isHTMLHidden(node.parent) : false,
3467
- bySelf: getHiddenAttr(node)
3468
- };
3469
- }
3470
- function isHTMLHidden(node, details) {
3471
- const cached = node.cacheGet(HTML_HIDDEN_CACHE);
3472
- if (cached) {
3473
- return details ? cached : cached.byParent || cached.bySelf;
3474
- }
3475
- const result = node.cacheSet(HTML_HIDDEN_CACHE, isHTMLHiddenImpl(node));
3476
- return details ? result : result.byParent || result.bySelf;
3477
- }
3478
- function isInertImpl(node) {
3479
- const getInertAttr = (node2) => {
3480
- const inert = node2.getAttribute("inert");
3481
- return Boolean(inert == null ? void 0 : inert.isStatic);
3482
- };
3483
- return {
3484
- byParent: node.parent ? isInert(node.parent) : false,
3485
- bySelf: getInertAttr(node)
3486
- };
3487
- }
3488
- function isInert(node, details) {
3489
- const cached = node.cacheGet(INERT_CACHE);
3490
- if (cached) {
3491
- return details ? cached : cached.byParent || cached.bySelf;
3492
- }
3493
- const result = node.cacheSet(INERT_CACHE, isInertImpl(node));
3494
- return details ? result : result.byParent || result.bySelf;
3495
- }
3496
- function isStyleHiddenImpl(node) {
3497
- const getStyleAttr = (node2) => {
3498
- const style = node2.getAttribute("style");
3499
- const { display, visibility } = parseCssDeclaration(style == null ? void 0 : style.value);
3500
- return display === "none" || visibility === "hidden";
3501
- };
3502
- const byParent = node.parent ? isStyleHidden(node.parent) : false;
3503
- const bySelf = getStyleAttr(node);
3504
- return byParent || bySelf;
3505
- }
3506
- function isStyleHidden(node) {
3507
- const cached = node.cacheGet(STYLE_HIDDEN_CACHE);
3508
- if (cached) {
3509
- return cached;
3510
- }
3511
- return node.cacheSet(STYLE_HIDDEN_CACHE, isStyleHiddenImpl(node));
3512
- }
3513
- function isPresentation(node) {
3514
- if (node.cacheExists(ROLE_PRESENTATION_CACHE)) {
3515
- return Boolean(node.cacheGet(ROLE_PRESENTATION_CACHE));
3516
- }
3517
- const meta = node.meta;
3518
- if (meta == null ? void 0 : meta.interactive) {
3519
- return node.cacheSet(ROLE_PRESENTATION_CACHE, false);
3520
- }
3521
- const tabindex = node.getAttribute("tabindex");
3522
- if (tabindex) {
3523
- return node.cacheSet(ROLE_PRESENTATION_CACHE, false);
3524
- }
3525
- const role = node.getAttribute("role");
3526
- if (role && (role.value === "presentation" || role.value === "none")) {
3527
- return node.cacheSet(ROLE_PRESENTATION_CACHE, true);
3528
- } else {
3529
- return node.cacheSet(ROLE_PRESENTATION_CACHE, false);
3530
- }
3531
- }
3532
-
3533
- const cachePrefix = classifyNodeText.name;
3534
- const HTML_CACHE_KEY = Symbol(`${cachePrefix}|html`);
3535
- const A11Y_CACHE_KEY = Symbol(`${cachePrefix}|a11y`);
3536
- const IGNORE_HIDDEN_ROOT_HTML_CACHE_KEY = Symbol(`${cachePrefix}|html|ignore-hidden-root`);
3537
- const IGNORE_HIDDEN_ROOT_A11Y_CACHE_KEY = Symbol(`${cachePrefix}|a11y|ignore-hidden-root`);
3538
- var TextClassification = /* @__PURE__ */ ((TextClassification2) => {
3539
- TextClassification2[TextClassification2["EMPTY_TEXT"] = 0] = "EMPTY_TEXT";
3540
- TextClassification2[TextClassification2["DYNAMIC_TEXT"] = 1] = "DYNAMIC_TEXT";
3541
- TextClassification2[TextClassification2["STATIC_TEXT"] = 2] = "STATIC_TEXT";
3542
- return TextClassification2;
3543
- })(TextClassification || {});
3544
- function getCachekey(options) {
3545
- const { accessible = false, ignoreHiddenRoot = false } = options;
3546
- if (accessible && ignoreHiddenRoot) {
3547
- return IGNORE_HIDDEN_ROOT_A11Y_CACHE_KEY;
3548
- } else if (ignoreHiddenRoot) {
3549
- return IGNORE_HIDDEN_ROOT_HTML_CACHE_KEY;
3550
- } else if (accessible) {
3551
- return A11Y_CACHE_KEY;
3552
- } else {
3553
- return HTML_CACHE_KEY;
3554
- }
3555
- }
3556
- function isSpecialEmpty(node) {
3557
- return node.is("select") || node.is("textarea");
3162
+ function isSpecialEmpty(node) {
3163
+ return node.is("select") || node.is("textarea");
3558
3164
  }
3559
3165
  function classifyNodeText(node, options = {}) {
3560
3166
  const { accessible = false, ignoreHiddenRoot = false } = options;
@@ -5012,7 +4618,7 @@ class AttributeAllowedValues extends Rule {
5012
4618
  setup() {
5013
4619
  this.on("dom:ready", (event) => {
5014
4620
  const doc = event.document;
5015
- doc.visitDepthFirst((node) => {
4621
+ walk.depthFirst(doc, (node) => {
5016
4622
  const meta = node.meta;
5017
4623
  if (!(meta == null ? void 0 : meta.attributes))
5018
4624
  return;
@@ -5072,7 +4678,7 @@ class AttributeBooleanStyle extends Rule {
5072
4678
  setup() {
5073
4679
  this.on("dom:ready", (event) => {
5074
4680
  const doc = event.document;
5075
- doc.visitDepthFirst((node) => {
4681
+ walk.depthFirst(doc, (node) => {
5076
4682
  const meta = node.meta;
5077
4683
  if (!(meta == null ? void 0 : meta.attributes))
5078
4684
  return;
@@ -5144,7 +4750,7 @@ class AttributeEmptyStyle extends Rule {
5144
4750
  setup() {
5145
4751
  this.on("dom:ready", (event) => {
5146
4752
  const doc = event.document;
5147
- doc.visitDepthFirst((node) => {
4753
+ walk.depthFirst(doc, (node) => {
5148
4754
  const meta = node.meta;
5149
4755
  if (!(meta == null ? void 0 : meta.attributes))
5150
4756
  return;
@@ -5796,7 +5402,7 @@ class ElementPermittedContent extends Rule {
5796
5402
  setup() {
5797
5403
  this.on("dom:ready", (event) => {
5798
5404
  const doc = event.document;
5799
- doc.visitDepthFirst((node) => {
5405
+ walk.depthFirst(doc, (node) => {
5800
5406
  const parent = node.parent;
5801
5407
  if (!parent) {
5802
5408
  return;
@@ -5875,7 +5481,7 @@ class ElementPermittedOccurrences extends Rule {
5875
5481
  setup() {
5876
5482
  this.on("dom:ready", (event) => {
5877
5483
  const doc = event.document;
5878
- doc.visitDepthFirst((node) => {
5484
+ walk.depthFirst(doc, (node) => {
5879
5485
  if (!node.meta) {
5880
5486
  return;
5881
5487
  }
@@ -5908,7 +5514,7 @@ class ElementPermittedOrder extends Rule {
5908
5514
  setup() {
5909
5515
  this.on("dom:ready", (event) => {
5910
5516
  const doc = event.document;
5911
- doc.visitDepthFirst((node) => {
5517
+ walk.depthFirst(doc, (node) => {
5912
5518
  if (!node.meta) {
5913
5519
  return;
5914
5520
  }
@@ -5978,7 +5584,7 @@ class ElementPermittedParent extends Rule {
5978
5584
  setup() {
5979
5585
  this.on("dom:ready", (event) => {
5980
5586
  const doc = event.document;
5981
- doc.visitDepthFirst((node) => {
5587
+ walk.depthFirst(doc, (node) => {
5982
5588
  var _a;
5983
5589
  const parent = node.parent;
5984
5590
  if (!parent) {
@@ -6026,7 +5632,7 @@ class ElementRequiredAncestor extends Rule {
6026
5632
  setup() {
6027
5633
  this.on("dom:ready", (event) => {
6028
5634
  const doc = event.document;
6029
- doc.visitDepthFirst((node) => {
5635
+ walk.depthFirst(doc, (node) => {
6030
5636
  const parent = node.parent;
6031
5637
  if (!parent) {
6032
5638
  return;
@@ -6110,7 +5716,7 @@ class ElementRequiredContent extends Rule {
6110
5716
  setup() {
6111
5717
  this.on("dom:ready", (event) => {
6112
5718
  const doc = event.document;
6113
- doc.visitDepthFirst((node) => {
5719
+ walk.depthFirst(doc, (node) => {
6114
5720
  if (!node.meta) {
6115
5721
  return;
6116
5722
  }
@@ -6630,7 +6236,7 @@ function isFocusableImpl(element) {
6630
6236
  if (isDisabled(element, meta)) {
6631
6237
  return false;
6632
6238
  }
6633
- return Boolean(meta == null ? void 0 : meta.focusable);
6239
+ return Boolean(meta.focusable);
6634
6240
  }
6635
6241
  function isFocusable(element) {
6636
6242
  const cached = element.cacheGet(FOCUSABLE_CACHE);
@@ -9372,946 +8978,1459 @@ function matchFieldNames2(token) {
9372
8978
  function matchWebauthn(token) {
9373
8979
  return token === "webauthn";
9374
8980
  }
9375
- function matchToken(token) {
9376
- if (matchSection(token)) {
9377
- return "section";
8981
+ function matchToken(token) {
8982
+ if (matchSection(token)) {
8983
+ return "section";
8984
+ }
8985
+ if (matchHint(token)) {
8986
+ return "hint";
8987
+ }
8988
+ if (matchFieldNames1(token)) {
8989
+ return "field1";
8990
+ }
8991
+ if (matchFieldNames2(token)) {
8992
+ return "field2";
8993
+ }
8994
+ if (matchContact(token)) {
8995
+ return "contact";
8996
+ }
8997
+ if (matchWebauthn(token)) {
8998
+ return "webauthn";
8999
+ }
9000
+ return null;
9001
+ }
9002
+ function getControlGroups(type) {
9003
+ const allGroups = [
9004
+ "text",
9005
+ "multiline",
9006
+ "password",
9007
+ "url",
9008
+ "username",
9009
+ "tel",
9010
+ "numeric",
9011
+ "month",
9012
+ "date"
9013
+ ];
9014
+ const mapping = {
9015
+ hidden: allGroups,
9016
+ text: allGroups.filter((it) => it !== "multiline"),
9017
+ search: allGroups.filter((it) => it !== "multiline"),
9018
+ password: ["password"],
9019
+ url: ["url"],
9020
+ email: ["username"],
9021
+ tel: ["tel"],
9022
+ number: ["numeric"],
9023
+ month: ["month"],
9024
+ date: ["date"]
9025
+ };
9026
+ return mapping[type] ?? [];
9027
+ }
9028
+ function isDisallowedType(node, type) {
9029
+ if (!node.is("input")) {
9030
+ return false;
9031
+ }
9032
+ return disallowedInputTypes.includes(type);
9033
+ }
9034
+ function getTerminalMessage(context) {
9035
+ switch (context.msg) {
9036
+ case 0 /* InvalidAttribute */:
9037
+ return "autocomplete attribute cannot be used on {{ what }}";
9038
+ case 1 /* InvalidValue */:
9039
+ return '"{{ value }}" cannot be used on {{ what }}';
9040
+ case 2 /* InvalidOrder */:
9041
+ return '"{{ second }}" must appear before "{{ first }}"';
9042
+ case 3 /* InvalidToken */:
9043
+ return '"{{ token }}" is not a valid autocomplete token or field name';
9044
+ case 4 /* InvalidCombination */:
9045
+ return '"{{ second }}" cannot be combined with "{{ first }}"';
9046
+ case 5 /* MissingField */:
9047
+ return "autocomplete attribute is missing field name";
9048
+ }
9049
+ }
9050
+ function getMarkdownMessage(context) {
9051
+ switch (context.msg) {
9052
+ case 0 /* InvalidAttribute */:
9053
+ return [
9054
+ `\`autocomplete\` attribute cannot be used on \`${context.what}\``,
9055
+ "",
9056
+ "The following input types cannot use the `autocomplete` attribute:",
9057
+ "",
9058
+ ...disallowedInputTypes.map((it) => `- \`${it}\``)
9059
+ ].join("\n");
9060
+ case 1 /* InvalidValue */: {
9061
+ const message = `\`"${context.value}"\` cannot be used on \`${context.what}\``;
9062
+ if (context.type === "form") {
9063
+ return [
9064
+ message,
9065
+ "",
9066
+ 'The `<form>` element can only use the values `"on"` and `"off"`.'
9067
+ ].join("\n");
9068
+ }
9069
+ if (context.type === "hidden") {
9070
+ return [
9071
+ message,
9072
+ "",
9073
+ '`<input type="hidden">` cannot use the values `"on"` and `"off"`.'
9074
+ ].join("\n");
9075
+ }
9076
+ const controlGroups = getControlGroups(context.type);
9077
+ const currentGroup = fieldNameGroup[context.value];
9078
+ return [
9079
+ message,
9080
+ "",
9081
+ `\`${context.what}\` allows autocomplete fields from the following group${controlGroups.length > 1 ? "s" : ""}:`,
9082
+ "",
9083
+ ...controlGroups.map((it) => `- ${it}`),
9084
+ "",
9085
+ `The field \`"${context.value}"\` belongs to the group /${currentGroup}/ which cannot be used with this input type.`
9086
+ ].join("\n");
9087
+ }
9088
+ case 2 /* InvalidOrder */:
9089
+ return [
9090
+ `\`"${context.second}"\` must appear before \`"${context.first}"\``,
9091
+ "",
9092
+ "The autocomplete tokens must appear in the following order:",
9093
+ "",
9094
+ "- Optional section name (`section-` prefix).",
9095
+ "- Optional `shipping` or `billing` token.",
9096
+ "- Optional `home`, `work`, `mobile`, `fax` or `pager` token (for fields supporting it).",
9097
+ "- Field name",
9098
+ "- Optional `webauthn` token."
9099
+ ].join("\n");
9100
+ case 3 /* InvalidToken */:
9101
+ return `\`"${context.token}"\` is not a valid autocomplete token or field name`;
9102
+ case 4 /* InvalidCombination */:
9103
+ return `\`"${context.second}"\` cannot be combined with \`"${context.first}"\``;
9104
+ case 5 /* MissingField */:
9105
+ return "Autocomplete attribute is missing field name";
9106
+ }
9107
+ }
9108
+ class ValidAutocomplete extends Rule {
9109
+ documentation(context) {
9110
+ return {
9111
+ description: getMarkdownMessage(context),
9112
+ url: "https://html-validate.org/rules/valid-autocomplete.html"
9113
+ };
9114
+ }
9115
+ setup() {
9116
+ this.on("dom:ready", (event) => {
9117
+ const { document } = event;
9118
+ const elements = document.querySelectorAll("[autocomplete]");
9119
+ for (const element of elements) {
9120
+ const autocomplete = element.getAttribute("autocomplete");
9121
+ if (autocomplete.value === null || autocomplete.value instanceof DynamicValue) {
9122
+ continue;
9123
+ }
9124
+ const location = autocomplete.valueLocation;
9125
+ const value = autocomplete.value.toLowerCase();
9126
+ const tokens = new DOMTokenList(value, location);
9127
+ if (tokens.length === 0) {
9128
+ continue;
9129
+ }
9130
+ this.validate(element, value, tokens, autocomplete.keyLocation, location);
9131
+ }
9132
+ });
9133
+ }
9134
+ validate(node, value, tokens, keyLocation, valueLocation) {
9135
+ switch (node.tagName) {
9136
+ case "form":
9137
+ this.validateFormAutocomplete(node, value, valueLocation);
9138
+ break;
9139
+ case "input":
9140
+ case "textarea":
9141
+ case "select":
9142
+ this.validateControlAutocomplete(node, tokens, keyLocation);
9143
+ break;
9144
+ }
9145
+ }
9146
+ validateControlAutocomplete(node, tokens, keyLocation) {
9147
+ const type = node.getAttributeValue("type") ?? "text";
9148
+ const mantle = type !== "hidden" ? "expectation" : "anchor";
9149
+ if (isDisallowedType(node, type)) {
9150
+ const context = {
9151
+ msg: 0 /* InvalidAttribute */,
9152
+ what: `<input type="${type}">`
9153
+ };
9154
+ this.report({
9155
+ node,
9156
+ message: getTerminalMessage(context),
9157
+ location: keyLocation,
9158
+ context
9159
+ });
9160
+ return;
9161
+ }
9162
+ if (tokens.includes("on") || tokens.includes("off")) {
9163
+ this.validateOnOff(node, mantle, tokens);
9164
+ return;
9165
+ }
9166
+ this.validateTokens(node, tokens, keyLocation);
9167
+ }
9168
+ validateFormAutocomplete(node, value, location) {
9169
+ const trimmed = value.trim();
9170
+ if (["on", "off"].includes(trimmed)) {
9171
+ return;
9172
+ }
9173
+ const context = {
9174
+ msg: 1 /* InvalidValue */,
9175
+ type: "form",
9176
+ value: trimmed,
9177
+ what: "<form>"
9178
+ };
9179
+ this.report({
9180
+ node,
9181
+ message: getTerminalMessage(context),
9182
+ location,
9183
+ context
9184
+ });
9185
+ }
9186
+ validateOnOff(node, mantle, tokens) {
9187
+ const index = tokens.findIndex((it) => it === "on" || it === "off");
9188
+ const value = tokens.item(index);
9189
+ const location = tokens.location(index);
9190
+ if (tokens.length > 1) {
9191
+ const context = {
9192
+ msg: 4 /* InvalidCombination */,
9193
+ /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
9194
+ first: tokens.item(index > 0 ? 0 : 1),
9195
+ second: value
9196
+ };
9197
+ this.report({
9198
+ node,
9199
+ message: getTerminalMessage(context),
9200
+ location,
9201
+ context
9202
+ });
9203
+ }
9204
+ switch (mantle) {
9205
+ case "expectation":
9206
+ return;
9207
+ case "anchor": {
9208
+ const context = {
9209
+ msg: 1 /* InvalidValue */,
9210
+ type: "hidden",
9211
+ value,
9212
+ what: `<input type="hidden">`
9213
+ };
9214
+ this.report({
9215
+ node,
9216
+ message: getTerminalMessage(context),
9217
+ location: tokens.location(0),
9218
+ context
9219
+ });
9220
+ }
9221
+ }
9222
+ }
9223
+ validateTokens(node, tokens, keyLocation) {
9224
+ const order = [];
9225
+ for (const { item, location } of tokens.iterator()) {
9226
+ const tokenType = matchToken(item);
9227
+ if (tokenType) {
9228
+ order.push(tokenType);
9229
+ } else {
9230
+ const context = {
9231
+ msg: 3 /* InvalidToken */,
9232
+ token: item
9233
+ };
9234
+ this.report({
9235
+ node,
9236
+ message: getTerminalMessage(context),
9237
+ location,
9238
+ context
9239
+ });
9240
+ return;
9241
+ }
9242
+ }
9243
+ const fieldTokens = order.map((it) => it === "field1" || it === "field2");
9244
+ this.validateFieldPresence(node, tokens, fieldTokens, keyLocation);
9245
+ this.validateContact(node, tokens, order);
9246
+ this.validateOrder(node, tokens, order);
9247
+ this.validateControlGroup(node, tokens, fieldTokens);
9248
+ }
9249
+ /**
9250
+ * Ensure that exactly one field name is present from the two field lists.
9251
+ */
9252
+ validateFieldPresence(node, tokens, fieldTokens, keyLocation) {
9253
+ const numFields = fieldTokens.filter(Boolean).length;
9254
+ if (numFields === 0) {
9255
+ const context = {
9256
+ msg: 5 /* MissingField */
9257
+ };
9258
+ this.report({
9259
+ node,
9260
+ message: getTerminalMessage(context),
9261
+ location: keyLocation,
9262
+ context
9263
+ });
9264
+ } else if (numFields > 1) {
9265
+ const a = fieldTokens.indexOf(true);
9266
+ const b = fieldTokens.lastIndexOf(true);
9267
+ const context = {
9268
+ msg: 4 /* InvalidCombination */,
9269
+ /* eslint-disable @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
9270
+ first: tokens.item(a),
9271
+ second: tokens.item(b)
9272
+ /* eslint-enable @typescript-eslint/no-non-null-assertion */
9273
+ };
9274
+ this.report({
9275
+ node,
9276
+ message: getTerminalMessage(context),
9277
+ location: tokens.location(b),
9278
+ context
9279
+ });
9280
+ }
9281
+ }
9282
+ /**
9283
+ * Ensure contact token is only used with field names from the second list.
9284
+ */
9285
+ validateContact(node, tokens, order) {
9286
+ if (order.includes("contact") && order.includes("field1")) {
9287
+ const a = order.indexOf("field1");
9288
+ const b = order.indexOf("contact");
9289
+ const context = {
9290
+ msg: 4 /* InvalidCombination */,
9291
+ /* eslint-disable @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
9292
+ first: tokens.item(a),
9293
+ second: tokens.item(b)
9294
+ /* eslint-enable @typescript-eslint/no-non-null-assertion */
9295
+ };
9296
+ this.report({
9297
+ node,
9298
+ message: getTerminalMessage(context),
9299
+ location: tokens.location(b),
9300
+ context
9301
+ });
9302
+ }
9303
+ }
9304
+ validateOrder(node, tokens, order) {
9305
+ const indicies = order.map((it) => expectedOrder.indexOf(it));
9306
+ for (let i = 0; i < indicies.length - 1; i++) {
9307
+ if (indicies[0] > indicies[i + 1]) {
9308
+ const context = {
9309
+ msg: 2 /* InvalidOrder */,
9310
+ /* eslint-disable @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
9311
+ first: tokens.item(i),
9312
+ second: tokens.item(i + 1)
9313
+ /* eslint-enable @typescript-eslint/no-non-null-assertion */
9314
+ };
9315
+ this.report({
9316
+ node,
9317
+ message: getTerminalMessage(context),
9318
+ location: tokens.location(i + 1),
9319
+ context
9320
+ });
9321
+ }
9322
+ }
9323
+ }
9324
+ validateControlGroup(node, tokens, fieldTokens) {
9325
+ const numFields = fieldTokens.filter(Boolean).length;
9326
+ if (numFields === 0) {
9327
+ return;
9328
+ }
9329
+ if (!node.is("input")) {
9330
+ return;
9331
+ }
9332
+ const attr = node.getAttribute("type");
9333
+ const type = (attr == null ? void 0 : attr.value) ?? "text";
9334
+ if (type instanceof DynamicValue) {
9335
+ return;
9336
+ }
9337
+ const controlGroups = getControlGroups(type);
9338
+ const fieldIndex = fieldTokens.indexOf(true);
9339
+ const fieldToken = tokens.item(fieldIndex);
9340
+ const fieldGroup = fieldNameGroup[fieldToken];
9341
+ if (!controlGroups.includes(fieldGroup)) {
9342
+ const context = {
9343
+ msg: 1 /* InvalidValue */,
9344
+ type,
9345
+ value: fieldToken,
9346
+ what: `<input type="${type}">`
9347
+ };
9348
+ this.report({
9349
+ node,
9350
+ message: getTerminalMessage(context),
9351
+ location: tokens.location(fieldIndex),
9352
+ context
9353
+ });
9354
+ }
9355
+ }
9356
+ }
9357
+
9358
+ const defaults$3 = {
9359
+ relaxed: false
9360
+ };
9361
+ class ValidID extends Rule {
9362
+ constructor(options) {
9363
+ super({ ...defaults$3, ...options });
9378
9364
  }
9379
- if (matchHint(token)) {
9380
- return "hint";
9365
+ static schema() {
9366
+ return {
9367
+ relaxed: {
9368
+ type: "boolean"
9369
+ }
9370
+ };
9381
9371
  }
9382
- if (matchFieldNames1(token)) {
9383
- return "field1";
9372
+ documentation(context) {
9373
+ const { relaxed } = this.options;
9374
+ const message = this.messages[context].replace("id", "ID").replace(/^(.)/, (m) => m.toUpperCase());
9375
+ const relaxedDescription = relaxed ? [] : [
9376
+ " - ID must begin with a letter",
9377
+ " - ID must only contain letters, digits, `-` and `_`"
9378
+ ];
9379
+ return {
9380
+ description: [
9381
+ `${message}.`,
9382
+ "",
9383
+ "Under the current configuration the following rules are applied:",
9384
+ "",
9385
+ " - ID must not be empty",
9386
+ " - ID must not contain any whitespace characters",
9387
+ ...relaxedDescription
9388
+ ].join("\n"),
9389
+ url: "https://html-validate.org/rules/valid-id.html"
9390
+ };
9384
9391
  }
9385
- if (matchFieldNames2(token)) {
9386
- return "field2";
9392
+ setup() {
9393
+ this.on("attr", this.isRelevant, (event) => {
9394
+ const { value } = event;
9395
+ if (value === null || value instanceof DynamicValue) {
9396
+ return;
9397
+ }
9398
+ if (value === "") {
9399
+ const context = 1 /* EMPTY */;
9400
+ this.report(event.target, this.messages[context], event.location, context);
9401
+ return;
9402
+ }
9403
+ if (value.match(/\s/)) {
9404
+ const context = 2 /* WHITESPACE */;
9405
+ this.report(event.target, this.messages[context], event.valueLocation, context);
9406
+ return;
9407
+ }
9408
+ const { relaxed } = this.options;
9409
+ if (relaxed) {
9410
+ return;
9411
+ }
9412
+ if (value.match(/^[^\p{L}]/u)) {
9413
+ const context = 3 /* LEADING_CHARACTER */;
9414
+ this.report(event.target, this.messages[context], event.valueLocation, context);
9415
+ return;
9416
+ }
9417
+ if (value.match(/[^\p{L}\p{N}_-]/u)) {
9418
+ const context = 4 /* DISALLOWED_CHARACTER */;
9419
+ this.report(event.target, this.messages[context], event.valueLocation, context);
9420
+ }
9421
+ });
9387
9422
  }
9388
- if (matchContact(token)) {
9389
- return "contact";
9423
+ get messages() {
9424
+ return {
9425
+ [1 /* EMPTY */]: "element id must not be empty",
9426
+ [2 /* WHITESPACE */]: "element id must not contain whitespace",
9427
+ [3 /* LEADING_CHARACTER */]: "element id must begin with a letter",
9428
+ [4 /* DISALLOWED_CHARACTER */]: "element id must only contain letters, digits, dash and underscore characters"
9429
+ };
9390
9430
  }
9391
- if (matchWebauthn(token)) {
9392
- return "webauthn";
9431
+ isRelevant(event) {
9432
+ return event.key === "id";
9393
9433
  }
9394
- return null;
9395
9434
  }
9396
- function getControlGroups(type) {
9397
- const allGroups = [
9398
- "text",
9399
- "multiline",
9400
- "password",
9401
- "url",
9402
- "username",
9403
- "tel",
9404
- "numeric",
9405
- "month",
9406
- "date"
9407
- ];
9408
- const mapping = {
9409
- hidden: allGroups,
9410
- text: allGroups.filter((it) => it !== "multiline"),
9411
- search: allGroups.filter((it) => it !== "multiline"),
9412
- password: ["password"],
9413
- url: ["url"],
9414
- email: ["username"],
9415
- tel: ["tel"],
9416
- number: ["numeric"],
9417
- month: ["month"],
9418
- date: ["date"]
9419
- };
9420
- const groups = mapping[type];
9421
- if (groups) {
9422
- return groups;
9435
+
9436
+ class VoidContent extends Rule {
9437
+ documentation(tagName) {
9438
+ const doc = {
9439
+ description: "HTML void elements cannot have any content and must not have content or end tag.",
9440
+ url: "https://html-validate.org/rules/void-content.html"
9441
+ };
9442
+ if (tagName) {
9443
+ doc.description = `<${tagName}> is a void element and must not have content or end tag.`;
9444
+ }
9445
+ return doc;
9423
9446
  }
9424
- return [];
9425
- }
9426
- function isDisallowedType(node, type) {
9427
- if (!node.is("input")) {
9428
- return false;
9447
+ setup() {
9448
+ this.on("tag:end", (event) => {
9449
+ const node = event.target;
9450
+ if (!node) {
9451
+ return;
9452
+ }
9453
+ if (!node.voidElement) {
9454
+ return;
9455
+ }
9456
+ if (node.closed === NodeClosed.EndTag) {
9457
+ this.report(
9458
+ null,
9459
+ `End tag for <${node.tagName}> must be omitted`,
9460
+ node.location,
9461
+ node.tagName
9462
+ );
9463
+ }
9464
+ });
9429
9465
  }
9430
- return disallowedInputTypes.includes(type);
9431
9466
  }
9432
- function getTerminalMessage(context) {
9433
- switch (context.msg) {
9434
- case 0 /* InvalidAttribute */:
9435
- return "autocomplete attribute cannot be used on {{ what }}";
9436
- case 1 /* InvalidValue */:
9437
- return '"{{ value }}" cannot be used on {{ what }}';
9438
- case 2 /* InvalidOrder */:
9439
- return '"{{ second }}" must appear before "{{ first }}"';
9440
- case 3 /* InvalidToken */:
9441
- return '"{{ token }}" is not a valid autocomplete token or field name';
9442
- case 4 /* InvalidCombination */:
9443
- return '"{{ second }}" cannot be combined with "{{ first }}"';
9444
- case 5 /* MissingField */:
9445
- return "autocomplete attribute is missing field name";
9467
+
9468
+ const defaults$2 = {
9469
+ style: "omit"
9470
+ };
9471
+ class VoidStyle extends Rule {
9472
+ constructor(options) {
9473
+ super({ ...defaults$2, ...options });
9474
+ this.style = parseStyle(this.options.style);
9446
9475
  }
9447
- }
9448
- function getMarkdownMessage(context) {
9449
- switch (context.msg) {
9450
- case 0 /* InvalidAttribute */:
9451
- return [
9452
- `\`autocomplete\` attribute cannot be used on \`${context.what}\``,
9453
- "",
9454
- "The following input types cannot use the `autocomplete` attribute:",
9455
- "",
9456
- ...disallowedInputTypes.map((it) => `- \`${it}\``)
9457
- ].join("\n");
9458
- case 1 /* InvalidValue */: {
9459
- const message = `\`"${context.value}"\` cannot be used on \`${context.what}\``;
9460
- if (context.type === "form") {
9461
- return [
9462
- message,
9463
- "",
9464
- 'The `<form>` element can only use the values `"on"` and `"off"`.'
9465
- ].join("\n");
9466
- }
9467
- if (context.type === "hidden") {
9468
- return [
9469
- message,
9470
- "",
9471
- '`<input type="hidden">` cannot use the values `"on"` and `"off"`.'
9472
- ].join("\n");
9473
- }
9474
- const controlGroups = getControlGroups(context.type);
9475
- const currentGroup = fieldNameGroup[context.value];
9476
- return [
9477
- message,
9478
- "",
9479
- `\`${context.what}\` allows autocomplete fields from the following group${controlGroups.length > 1 ? "s" : ""}:`,
9480
- "",
9481
- ...controlGroups.map((it) => `- ${it}`),
9482
- "",
9483
- `The field \`"${context.value}"\` belongs to the group /${currentGroup}/ which cannot be used with this input type.`
9484
- ].join("\n");
9485
- }
9486
- case 2 /* InvalidOrder */:
9487
- return [
9488
- `\`"${context.second}"\` must appear before \`"${context.first}"\``,
9489
- "",
9490
- "The autocomplete tokens must appear in the following order:",
9491
- "",
9492
- "- Optional section name (`section-` prefix).",
9493
- "- Optional `shipping` or `billing` token.",
9494
- "- Optional `home`, `work`, `mobile`, `fax` or `pager` token (for fields supporting it).",
9495
- "- Field name",
9496
- "- Optional `webauthn` token."
9497
- ].join("\n");
9498
- case 3 /* InvalidToken */:
9499
- return `\`"${context.token}"\` is not a valid autocomplete token or field name`;
9500
- case 4 /* InvalidCombination */:
9501
- return `\`"${context.second}"\` cannot be combined with \`"${context.first}"\``;
9502
- case 5 /* MissingField */:
9503
- return "Autocomplete attribute is missing field name";
9476
+ static schema() {
9477
+ return {
9478
+ style: {
9479
+ enum: ["omit", "selfclose", "selfclosing"],
9480
+ type: "string"
9481
+ }
9482
+ };
9504
9483
  }
9505
- }
9506
- class ValidAutocomplete extends Rule {
9507
9484
  documentation(context) {
9485
+ const [desc, end] = styleDescription(context.style);
9508
9486
  return {
9509
- description: getMarkdownMessage(context),
9510
- url: "https://html-validate.org/rules/valid-autocomplete.html"
9487
+ description: `The current configuration requires void elements to ${desc}, use <${context.tagName}${end}> instead.`,
9488
+ url: "https://html-validate.org/rules/void-style.html"
9511
9489
  };
9512
9490
  }
9513
9491
  setup() {
9514
- this.on("dom:ready", (event) => {
9515
- const { document } = event;
9516
- const elements = document.querySelectorAll("[autocomplete]");
9517
- for (const element of elements) {
9518
- const autocomplete = element.getAttribute("autocomplete");
9519
- if (autocomplete.value === null || autocomplete.value instanceof DynamicValue) {
9520
- continue;
9521
- }
9522
- const location = autocomplete.valueLocation;
9523
- const value = autocomplete.value.toLowerCase();
9524
- const tokens = new DOMTokenList(value, location);
9525
- if (tokens.length === 0) {
9526
- continue;
9527
- }
9528
- this.validate(element, value, tokens, autocomplete.keyLocation, location);
9492
+ this.on("tag:end", (event) => {
9493
+ const active = event.previous;
9494
+ if (active.meta) {
9495
+ this.validateActive(active);
9529
9496
  }
9530
9497
  });
9531
9498
  }
9532
- validate(node, value, tokens, keyLocation, valueLocation) {
9533
- switch (node.tagName) {
9534
- case "form":
9535
- this.validateFormAutocomplete(node, value, valueLocation);
9536
- break;
9537
- case "input":
9538
- case "textarea":
9539
- case "select":
9540
- this.validateControlAutocomplete(node, tokens, keyLocation);
9541
- break;
9499
+ validateActive(node) {
9500
+ if (!node.voidElement) {
9501
+ return;
9542
9502
  }
9543
- }
9544
- validateControlAutocomplete(node, tokens, keyLocation) {
9545
- const type = node.getAttributeValue("type") ?? "text";
9546
- const mantle = type !== "hidden" ? "expectation" : "anchor";
9547
- if (isDisallowedType(node, type)) {
9548
- const context = {
9549
- msg: 0 /* InvalidAttribute */,
9550
- what: `<input type="${type}">`
9551
- };
9552
- this.report({
9503
+ if (this.shouldBeOmitted(node)) {
9504
+ this.reportError(
9553
9505
  node,
9554
- message: getTerminalMessage(context),
9555
- location: keyLocation,
9556
- context
9557
- });
9558
- return;
9506
+ `Expected omitted end tag <${node.tagName}> instead of self-closing element <${node.tagName}/>`
9507
+ );
9559
9508
  }
9560
- if (tokens.includes("on") || tokens.includes("off")) {
9561
- this.validateOnOff(node, mantle, tokens);
9562
- return;
9509
+ if (this.shouldBeSelfClosed(node)) {
9510
+ this.reportError(
9511
+ node,
9512
+ `Expected self-closing element <${node.tagName}/> instead of omitted end-tag <${node.tagName}>`
9513
+ );
9563
9514
  }
9564
- this.validateTokens(node, tokens, keyLocation);
9565
9515
  }
9566
- validateFormAutocomplete(node, value, location) {
9567
- const trimmed = value.trim();
9568
- if (["on", "off"].includes(trimmed)) {
9569
- return;
9570
- }
9516
+ reportError(node, message) {
9571
9517
  const context = {
9572
- msg: 1 /* InvalidValue */,
9573
- type: "form",
9574
- value: trimmed,
9575
- what: "<form>"
9518
+ style: this.style,
9519
+ tagName: node.tagName
9576
9520
  };
9577
- this.report({
9578
- node,
9579
- message: getTerminalMessage(context),
9580
- location,
9581
- context
9582
- });
9521
+ super.report(node, message, null, context);
9583
9522
  }
9584
- validateOnOff(node, mantle, tokens) {
9585
- const index = tokens.findIndex((it) => it === "on" || it === "off");
9586
- const value = tokens.item(index);
9587
- const location = tokens.location(index);
9588
- if (tokens.length > 1) {
9589
- const context = {
9590
- msg: 4 /* InvalidCombination */,
9591
- /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
9592
- first: tokens.item(index > 0 ? 0 : 1),
9593
- second: value
9594
- };
9595
- this.report({
9596
- node,
9597
- message: getTerminalMessage(context),
9598
- location,
9599
- context
9600
- });
9601
- }
9602
- switch (mantle) {
9603
- case "expectation":
9604
- return;
9605
- case "anchor": {
9606
- const context = {
9607
- msg: 1 /* InvalidValue */,
9608
- type: "hidden",
9609
- value,
9610
- what: `<input type="hidden">`
9611
- };
9612
- this.report({
9613
- node,
9614
- message: getTerminalMessage(context),
9615
- location: tokens.location(0),
9616
- context
9617
- });
9618
- }
9619
- }
9523
+ shouldBeOmitted(node) {
9524
+ return this.style === 1 /* AlwaysOmit */ && node.closed === NodeClosed.VoidSelfClosed;
9620
9525
  }
9621
- validateTokens(node, tokens, keyLocation) {
9622
- const order = [];
9623
- for (const { item, location } of tokens.iterator()) {
9624
- const tokenType = matchToken(item);
9625
- if (tokenType) {
9626
- order.push(tokenType);
9627
- } else {
9628
- const context = {
9629
- msg: 3 /* InvalidToken */,
9630
- token: item
9631
- };
9632
- this.report({
9633
- node,
9634
- message: getTerminalMessage(context),
9635
- location,
9636
- context
9637
- });
9638
- return;
9526
+ shouldBeSelfClosed(node) {
9527
+ return this.style === 2 /* AlwaysSelfclose */ && node.closed === NodeClosed.VoidOmitted;
9528
+ }
9529
+ }
9530
+ function parseStyle(name) {
9531
+ switch (name) {
9532
+ case "omit":
9533
+ return 1 /* AlwaysOmit */;
9534
+ case "selfclose":
9535
+ case "selfclosing":
9536
+ return 2 /* AlwaysSelfclose */;
9537
+ default:
9538
+ throw new Error(`Invalid style "${name}" for "void-style" rule`);
9539
+ }
9540
+ }
9541
+ function styleDescription(style) {
9542
+ switch (style) {
9543
+ case 1 /* AlwaysOmit */:
9544
+ return ["omit end tag", ""];
9545
+ case 2 /* AlwaysSelfclose */:
9546
+ return ["be self-closed", "/"];
9547
+ default:
9548
+ throw new Error(`Unknown style`);
9549
+ }
9550
+ }
9551
+
9552
+ class H30 extends Rule {
9553
+ documentation() {
9554
+ return {
9555
+ 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.",
9556
+ url: "https://html-validate.org/rules/wcag/h30.html"
9557
+ };
9558
+ }
9559
+ setup() {
9560
+ this.on("dom:ready", (event) => {
9561
+ const links = event.document.getElementsByTagName("a");
9562
+ for (const link of links) {
9563
+ if (!link.hasAttribute("href")) {
9564
+ continue;
9565
+ }
9566
+ if (!inAccessibilityTree(link)) {
9567
+ continue;
9568
+ }
9569
+ const textClassification = classifyNodeText(link, { ignoreHiddenRoot: true });
9570
+ if (textClassification !== TextClassification.EMPTY_TEXT) {
9571
+ continue;
9572
+ }
9573
+ const images = link.querySelectorAll("img");
9574
+ if (images.some((image) => hasAltText(image))) {
9575
+ continue;
9576
+ }
9577
+ const labels = link.querySelectorAll("[aria-label]");
9578
+ if (hasAriaLabel(link) || labels.some((cur) => hasAriaLabel(cur))) {
9579
+ continue;
9580
+ }
9581
+ this.report(link, "Anchor link must have a text describing its purpose");
9639
9582
  }
9640
- }
9641
- const fieldTokens = order.map((it) => it === "field1" || it === "field2");
9642
- this.validateFieldPresence(node, tokens, fieldTokens, keyLocation);
9643
- this.validateContact(node, tokens, order);
9644
- this.validateOrder(node, tokens, order);
9645
- this.validateControlGroup(node, tokens, fieldTokens);
9583
+ });
9646
9584
  }
9647
- /**
9648
- * Ensure that exactly one field name is present from the two field lists.
9649
- */
9650
- validateFieldPresence(node, tokens, fieldTokens, keyLocation) {
9651
- const numFields = fieldTokens.filter(Boolean).length;
9652
- if (numFields === 0) {
9653
- const context = {
9654
- msg: 5 /* MissingField */
9655
- };
9656
- this.report({
9657
- node,
9658
- message: getTerminalMessage(context),
9659
- location: keyLocation,
9660
- context
9661
- });
9662
- } else if (numFields > 1) {
9663
- const a = fieldTokens.indexOf(true);
9664
- const b = fieldTokens.lastIndexOf(true);
9665
- const context = {
9666
- msg: 4 /* InvalidCombination */,
9667
- /* eslint-disable @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
9668
- first: tokens.item(a),
9669
- second: tokens.item(b)
9670
- /* eslint-enable @typescript-eslint/no-non-null-assertion */
9671
- };
9672
- this.report({
9673
- node,
9674
- message: getTerminalMessage(context),
9675
- location: tokens.location(b),
9676
- context
9677
- });
9678
- }
9585
+ }
9586
+
9587
+ class H32 extends Rule {
9588
+ documentation() {
9589
+ return {
9590
+ description: "WCAG 2.1 requires each `<form>` element to have at least one submit button.",
9591
+ url: "https://html-validate.org/rules/wcag/h32.html"
9592
+ };
9593
+ }
9594
+ setup() {
9595
+ const formTags = this.getTagsWithProperty("form");
9596
+ const formSelector = formTags.join(",");
9597
+ this.on("dom:ready", (event) => {
9598
+ const { document } = event;
9599
+ const forms = document.querySelectorAll(formSelector);
9600
+ for (const form of forms) {
9601
+ if (hasNestedSubmit(form)) {
9602
+ continue;
9603
+ }
9604
+ if (hasAssociatedSubmit(document, form)) {
9605
+ continue;
9606
+ }
9607
+ this.report(form, `<${form.tagName}> element must have a submit button`);
9608
+ }
9609
+ });
9679
9610
  }
9680
- /**
9681
- * Ensure contact token is only used with field names from the second list.
9682
- */
9683
- validateContact(node, tokens, order) {
9684
- if (order.includes("contact") && order.includes("field1")) {
9685
- const a = order.indexOf("field1");
9686
- const b = order.indexOf("contact");
9687
- const context = {
9688
- msg: 4 /* InvalidCombination */,
9689
- /* eslint-disable @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
9690
- first: tokens.item(a),
9691
- second: tokens.item(b)
9692
- /* eslint-enable @typescript-eslint/no-non-null-assertion */
9693
- };
9694
- this.report({
9695
- node,
9696
- message: getTerminalMessage(context),
9697
- location: tokens.location(b),
9698
- context
9699
- });
9700
- }
9611
+ }
9612
+ function isSubmit(node) {
9613
+ const type = node.getAttribute("type");
9614
+ return Boolean(!type || type.valueMatches(/submit|image/));
9615
+ }
9616
+ function isAssociated(id, node) {
9617
+ const form = node.getAttribute("form");
9618
+ return Boolean(form == null ? void 0 : form.valueMatches(id, true));
9619
+ }
9620
+ function hasNestedSubmit(form) {
9621
+ const matches = form.querySelectorAll("button,input").filter(isSubmit).filter((node) => !node.hasAttribute("form"));
9622
+ return matches.length > 0;
9623
+ }
9624
+ function hasAssociatedSubmit(document, form) {
9625
+ const { id } = form;
9626
+ if (!id) {
9627
+ return false;
9701
9628
  }
9702
- validateOrder(node, tokens, order) {
9703
- const indicies = order.map((it) => expectedOrder.indexOf(it));
9704
- for (let i = 0; i < indicies.length - 1; i++) {
9705
- if (indicies[0] > indicies[i + 1]) {
9706
- const context = {
9707
- msg: 2 /* InvalidOrder */,
9708
- /* eslint-disable @typescript-eslint/no-non-null-assertion -- it must be present of it wouldn't be found */
9709
- first: tokens.item(i),
9710
- second: tokens.item(i + 1)
9711
- /* eslint-enable @typescript-eslint/no-non-null-assertion */
9712
- };
9629
+ const matches = document.querySelectorAll("button[form],input[form]").filter(isSubmit).filter((node) => isAssociated(id, node));
9630
+ return matches.length > 0;
9631
+ }
9632
+
9633
+ class H36 extends Rule {
9634
+ documentation() {
9635
+ return {
9636
+ description: [
9637
+ "WCAG 2.1 requires all images used as submit buttons to have a non-empty textual description using the `alt` attribute.",
9638
+ 'The alt text cannot be empty (`alt=""`).'
9639
+ ].join("\n"),
9640
+ url: "https://html-validate.org/rules/wcag/h36.html"
9641
+ };
9642
+ }
9643
+ setup() {
9644
+ this.on("tag:end", (event) => {
9645
+ const node = event.previous;
9646
+ if (node.tagName !== "input")
9647
+ return;
9648
+ if (node.getAttributeValue("type") !== "image") {
9649
+ return;
9650
+ }
9651
+ if (!inAccessibilityTree(node)) {
9652
+ return;
9653
+ }
9654
+ if (!hasAltText(node)) {
9655
+ const message = "image used as submit button must have non-empty alt text";
9656
+ const alt = node.getAttribute("alt");
9713
9657
  this.report({
9714
9658
  node,
9715
- message: getTerminalMessage(context),
9716
- location: tokens.location(i + 1),
9717
- context
9659
+ message,
9660
+ location: alt ? alt.keyLocation : node.location
9718
9661
  });
9719
9662
  }
9720
- }
9721
- }
9722
- validateControlGroup(node, tokens, fieldTokens) {
9723
- const numFields = fieldTokens.filter(Boolean).length;
9724
- if (numFields === 0) {
9725
- return;
9726
- }
9727
- if (!node.is("input")) {
9728
- return;
9729
- }
9730
- const attr = node.getAttribute("type");
9731
- const type = (attr == null ? void 0 : attr.value) ?? "text";
9732
- if (type instanceof DynamicValue) {
9733
- return;
9734
- }
9735
- const controlGroups = getControlGroups(type);
9736
- const fieldIndex = fieldTokens.indexOf(true);
9737
- const fieldToken = tokens.item(fieldIndex);
9738
- const fieldGroup = fieldNameGroup[fieldToken];
9739
- if (!controlGroups.includes(fieldGroup)) {
9740
- const context = {
9741
- msg: 1 /* InvalidValue */,
9742
- type,
9743
- value: fieldToken,
9744
- what: `<input type="${type}">`
9745
- };
9746
- this.report({
9747
- node,
9748
- message: getTerminalMessage(context),
9749
- location: tokens.location(fieldIndex),
9750
- context
9751
- });
9752
- }
9663
+ });
9753
9664
  }
9754
9665
  }
9755
9666
 
9756
- const defaults$3 = {
9757
- relaxed: false
9667
+ const defaults$1 = {
9668
+ allowEmpty: true,
9669
+ alias: []
9758
9670
  };
9759
- class ValidID extends Rule {
9671
+ class H37 extends Rule {
9760
9672
  constructor(options) {
9761
- super({ ...defaults$3, ...options });
9673
+ super({ ...defaults$1, ...options });
9674
+ if (!Array.isArray(this.options.alias)) {
9675
+ this.options.alias = [this.options.alias];
9676
+ }
9762
9677
  }
9763
9678
  static schema() {
9764
9679
  return {
9765
- relaxed: {
9680
+ alias: {
9681
+ anyOf: [
9682
+ {
9683
+ items: {
9684
+ type: "string"
9685
+ },
9686
+ type: "array"
9687
+ },
9688
+ {
9689
+ type: "string"
9690
+ }
9691
+ ]
9692
+ },
9693
+ allowEmpty: {
9766
9694
  type: "boolean"
9767
9695
  }
9768
9696
  };
9769
9697
  }
9770
- documentation(context) {
9771
- const { relaxed } = this.options;
9772
- const message = this.messages[context].replace("id", "ID").replace(/^(.)/, (m) => m.toUpperCase());
9773
- const relaxedDescription = relaxed ? [] : [
9774
- " - ID must begin with a letter",
9775
- " - ID must only contain letters, digits, `-` and `_`"
9776
- ];
9698
+ documentation() {
9777
9699
  return {
9778
- description: [
9779
- `${message}.`,
9780
- "",
9781
- "Under the current configuration the following rules are applied:",
9782
- "",
9783
- " - ID must not be empty",
9784
- " - ID must not contain any whitespace characters",
9785
- ...relaxedDescription
9786
- ].join("\n"),
9787
- url: "https://html-validate.org/rules/valid-id.html"
9700
+ description: "Both HTML5 and WCAG 2.0 requires images to have a alternative text for each image.",
9701
+ url: "https://html-validate.org/rules/wcag/h37.html"
9788
9702
  };
9789
9703
  }
9790
9704
  setup() {
9791
- this.on("attr", this.isRelevant, (event) => {
9792
- const { value } = event;
9793
- if (value === null || value instanceof DynamicValue) {
9794
- return;
9705
+ this.on("dom:ready", (event) => {
9706
+ const { document } = event;
9707
+ const nodes = document.querySelectorAll("img");
9708
+ for (const node of nodes) {
9709
+ this.validateNode(node);
9795
9710
  }
9796
- if (value === "") {
9797
- const context = 1 /* EMPTY */;
9798
- this.report(event.target, this.messages[context], event.location, context);
9711
+ });
9712
+ }
9713
+ validateNode(node) {
9714
+ if (!inAccessibilityTree(node)) {
9715
+ return;
9716
+ }
9717
+ if (Boolean(node.getAttributeValue("alt")) || Boolean(node.hasAttribute("alt") && this.options.allowEmpty)) {
9718
+ return;
9719
+ }
9720
+ for (const attr of this.options.alias) {
9721
+ if (node.getAttribute(attr)) {
9799
9722
  return;
9800
9723
  }
9801
- if (value.match(/\s/)) {
9802
- const context = 2 /* WHITESPACE */;
9803
- this.report(event.target, this.messages[context], event.valueLocation, context);
9724
+ }
9725
+ const tag = node.annotatedName;
9726
+ if (node.hasAttribute("alt")) {
9727
+ const attr = node.getAttribute("alt");
9728
+ this.report(node, `${tag} cannot have empty "alt" attribute`, attr.keyLocation);
9729
+ } else {
9730
+ this.report(node, `${tag} is missing required "alt" attribute`, node.location);
9731
+ }
9732
+ }
9733
+ }
9734
+
9735
+ var _a;
9736
+ const { enum: validScopes } = (_a = html5.th.attributes) == null ? void 0 : _a.scope;
9737
+ const joinedScopes = naturalJoin(validScopes);
9738
+ class H63 extends Rule {
9739
+ documentation() {
9740
+ return {
9741
+ description: "H63: Using the scope attribute to associate header cells and data cells in data tables",
9742
+ url: "https://html-validate.org/rules/wcag/h63.html"
9743
+ };
9744
+ }
9745
+ setup() {
9746
+ this.on("tag:ready", (event) => {
9747
+ const node = event.target;
9748
+ if (node.tagName !== "th") {
9804
9749
  return;
9805
9750
  }
9806
- const { relaxed } = this.options;
9807
- if (relaxed) {
9751
+ const scope = node.getAttribute("scope");
9752
+ const value = scope == null ? void 0 : scope.value;
9753
+ if (value instanceof DynamicValue) {
9808
9754
  return;
9809
9755
  }
9810
- if (value.match(/^[^\p{L}]/u)) {
9811
- const context = 3 /* LEADING_CHARACTER */;
9812
- this.report(event.target, this.messages[context], event.valueLocation, context);
9756
+ if (value && validScopes.includes(value)) {
9813
9757
  return;
9814
9758
  }
9815
- if (value.match(/[^\p{L}\p{N}_-]/u)) {
9816
- const context = 4 /* DISALLOWED_CHARACTER */;
9817
- this.report(event.target, this.messages[context], event.valueLocation, context);
9818
- }
9759
+ const message = `<th> element must have a valid scope attribute: ${joinedScopes}`;
9760
+ const location = (scope == null ? void 0 : scope.valueLocation) ?? (scope == null ? void 0 : scope.keyLocation) ?? node.location;
9761
+ this.report(node, message, location);
9819
9762
  });
9820
9763
  }
9821
- get messages() {
9822
- return {
9823
- [1 /* EMPTY */]: "element id must not be empty",
9824
- [2 /* WHITESPACE */]: "element id must not contain whitespace",
9825
- [3 /* LEADING_CHARACTER */]: "element id must begin with a letter",
9826
- [4 /* DISALLOWED_CHARACTER */]: "element id must only contain letters, digits, dash and underscore characters"
9827
- };
9828
- }
9829
- isRelevant(event) {
9830
- return event.key === "id";
9831
- }
9832
- }
9833
-
9834
- class VoidContent extends Rule {
9835
- documentation(tagName) {
9836
- const doc = {
9837
- description: "HTML void elements cannot have any content and must not have content or end tag.",
9838
- url: "https://html-validate.org/rules/void-content.html"
9764
+ }
9765
+
9766
+ class H67 extends Rule {
9767
+ documentation() {
9768
+ return {
9769
+ description: "A decorative image cannot have a title attribute. Either remove `title` or add a descriptive `alt` text.",
9770
+ url: "https://html-validate.org/rules/wcag/h67.html"
9839
9771
  };
9840
- if (tagName) {
9841
- doc.description = `<${tagName}> is a void element and must not have content or end tag.`;
9842
- }
9843
- return doc;
9844
9772
  }
9845
9773
  setup() {
9846
9774
  this.on("tag:end", (event) => {
9847
9775
  const node = event.target;
9848
- if (!node) {
9776
+ if (!node || node.tagName !== "img") {
9849
9777
  return;
9850
9778
  }
9851
- if (!node.voidElement) {
9779
+ const title = node.getAttribute("title");
9780
+ if (!title || title.value === "") {
9852
9781
  return;
9853
9782
  }
9854
- if (node.closed === NodeClosed.EndTag) {
9855
- this.report(
9856
- null,
9857
- `End tag for <${node.tagName}> must be omitted`,
9858
- node.location,
9859
- node.tagName
9860
- );
9783
+ const alt = node.getAttributeValue("alt");
9784
+ if (alt && alt !== "") {
9785
+ return;
9861
9786
  }
9787
+ this.report(node, "<img> with empty alt text cannot have title attribute", title.keyLocation);
9862
9788
  });
9863
9789
  }
9864
9790
  }
9865
9791
 
9866
- const defaults$2 = {
9867
- style: "omit"
9868
- };
9869
- class VoidStyle extends Rule {
9870
- constructor(options) {
9871
- super({ ...defaults$2, ...options });
9872
- this.style = parseStyle(this.options.style);
9873
- }
9874
- static schema() {
9875
- return {
9876
- style: {
9877
- enum: ["omit", "selfclose", "selfclosing"],
9878
- type: "string"
9879
- }
9880
- };
9881
- }
9882
- documentation(context) {
9883
- const [desc, end] = styleDescription(context.style);
9792
+ class H71 extends Rule {
9793
+ documentation() {
9884
9794
  return {
9885
- description: `The current configuration requires void elements to ${desc}, use <${context.tagName}${end}> instead.`,
9886
- url: "https://html-validate.org/rules/void-style.html"
9795
+ description: "H71: Providing a description for groups of form controls using fieldset and legend elements",
9796
+ url: "https://html-validate.org/rules/wcag/h71.html"
9887
9797
  };
9888
9798
  }
9889
9799
  setup() {
9890
- this.on("tag:end", (event) => {
9891
- const active = event.previous;
9892
- if (active.meta) {
9893
- this.validateActive(active);
9800
+ this.on("dom:ready", (event) => {
9801
+ const { document } = event;
9802
+ const fieldsets = document.querySelectorAll(this.selector);
9803
+ for (const fieldset of fieldsets) {
9804
+ this.validate(fieldset);
9894
9805
  }
9895
9806
  });
9896
9807
  }
9897
- validateActive(node) {
9898
- if (!node.voidElement) {
9899
- return;
9900
- }
9901
- if (this.shouldBeOmitted(node)) {
9902
- this.reportError(
9903
- node,
9904
- `Expected omitted end tag <${node.tagName}> instead of self-closing element <${node.tagName}/>`
9905
- );
9906
- }
9907
- if (this.shouldBeSelfClosed(node)) {
9908
- this.reportError(
9909
- node,
9910
- `Expected self-closing element <${node.tagName}/> instead of omitted end-tag <${node.tagName}>`
9911
- );
9808
+ validate(fieldset) {
9809
+ const legend = fieldset.querySelectorAll("> legend");
9810
+ if (legend.length === 0) {
9811
+ this.reportNode(fieldset);
9912
9812
  }
9913
9813
  }
9914
- reportError(node, message) {
9915
- const context = {
9916
- style: this.style,
9917
- tagName: node.tagName
9918
- };
9919
- super.report(node, message, null, context);
9920
- }
9921
- shouldBeOmitted(node) {
9922
- return this.style === 1 /* AlwaysOmit */ && node.closed === NodeClosed.VoidSelfClosed;
9814
+ reportNode(node) {
9815
+ super.report(node, `${node.annotatedName} must have a <legend> as the first child`);
9923
9816
  }
9924
- shouldBeSelfClosed(node) {
9925
- return this.style === 2 /* AlwaysSelfclose */ && node.closed === NodeClosed.VoidOmitted;
9817
+ get selector() {
9818
+ return this.getTagsDerivedFrom("fieldset").join(",");
9926
9819
  }
9927
9820
  }
9928
- function parseStyle(name) {
9929
- switch (name) {
9930
- case "omit":
9931
- return 1 /* AlwaysOmit */;
9932
- case "selfclose":
9933
- case "selfclosing":
9934
- return 2 /* AlwaysSelfclose */;
9935
- default:
9936
- throw new Error(`Invalid style "${name}" for "void-style" rule`);
9937
- }
9821
+
9822
+ const bundledRules$1 = {
9823
+ "wcag/h30": H30,
9824
+ "wcag/h32": H32,
9825
+ "wcag/h36": H36,
9826
+ "wcag/h37": H37,
9827
+ "wcag/h63": H63,
9828
+ "wcag/h67": H67,
9829
+ "wcag/h71": H71
9830
+ };
9831
+
9832
+ const bundledRules = {
9833
+ "allowed-links": AllowedLinks,
9834
+ "area-alt": AreaAlt,
9835
+ "aria-hidden-body": AriaHiddenBody,
9836
+ "aria-label-misuse": AriaLabelMisuse,
9837
+ "attr-case": AttrCase,
9838
+ "attr-delimiter": AttrDelimiter,
9839
+ "attr-pattern": AttrPattern,
9840
+ "attr-quotes": AttrQuotes,
9841
+ "attr-spacing": AttrSpacing,
9842
+ "attribute-allowed-values": AttributeAllowedValues,
9843
+ "attribute-boolean-style": AttributeBooleanStyle,
9844
+ "attribute-empty-style": AttributeEmptyStyle,
9845
+ "attribute-misuse": AttributeMisuse,
9846
+ "class-pattern": ClassPattern,
9847
+ "close-attr": CloseAttr,
9848
+ "close-order": CloseOrder,
9849
+ deprecated: Deprecated,
9850
+ "deprecated-rule": DeprecatedRule,
9851
+ "doctype-html": NoStyleTag$1,
9852
+ "doctype-style": DoctypeStyle,
9853
+ "element-case": ElementCase,
9854
+ "element-name": ElementName,
9855
+ "element-permitted-content": ElementPermittedContent,
9856
+ "element-permitted-occurrences": ElementPermittedOccurrences,
9857
+ "element-permitted-order": ElementPermittedOrder,
9858
+ "element-permitted-parent": ElementPermittedParent,
9859
+ "element-required-ancestor": ElementRequiredAncestor,
9860
+ "element-required-attributes": ElementRequiredAttributes,
9861
+ "element-required-content": ElementRequiredContent,
9862
+ "empty-heading": EmptyHeading,
9863
+ "empty-title": EmptyTitle,
9864
+ "form-dup-name": FormDupName,
9865
+ "heading-level": HeadingLevel,
9866
+ "hidden-focusable": HiddenFocusable,
9867
+ "id-pattern": IdPattern,
9868
+ "input-attributes": InputAttributes,
9869
+ "input-missing-label": InputMissingLabel,
9870
+ "long-title": LongTitle,
9871
+ "map-dup-name": MapDupName,
9872
+ "map-id-name": MapIdName,
9873
+ "meta-refresh": MetaRefresh,
9874
+ "missing-doctype": MissingDoctype,
9875
+ "multiple-labeled-controls": MultipleLabeledControls,
9876
+ "name-pattern": NamePattern,
9877
+ "no-abstract-role": NoAbstractRole,
9878
+ "no-autoplay": NoAutoplay,
9879
+ "no-conditional-comment": NoConditionalComment,
9880
+ "no-deprecated-attr": NoDeprecatedAttr,
9881
+ "no-dup-attr": NoDupAttr,
9882
+ "no-dup-class": NoDupClass,
9883
+ "no-dup-id": NoDupID,
9884
+ "no-implicit-button-type": NoImplicitButtonType,
9885
+ "no-implicit-input-type": NoImplicitInputType,
9886
+ "no-implicit-close": NoImplicitClose,
9887
+ "no-inline-style": NoInlineStyle,
9888
+ "no-missing-references": NoMissingReferences,
9889
+ "no-multiple-main": NoMultipleMain,
9890
+ "no-raw-characters": NoRawCharacters,
9891
+ "no-redundant-aria-label": NoRedundantAriaLabel,
9892
+ "no-redundant-for": NoRedundantFor,
9893
+ "no-redundant-role": NoRedundantRole,
9894
+ "no-self-closing": NoSelfClosing,
9895
+ "no-style-tag": NoStyleTag,
9896
+ "no-trailing-whitespace": NoTrailingWhitespace,
9897
+ "no-unknown-elements": NoUnknownElements,
9898
+ "no-unused-disable": NoUnusedDisable,
9899
+ "no-utf8-bom": NoUtf8Bom,
9900
+ "prefer-button": PreferButton,
9901
+ "prefer-native-element": PreferNativeElement,
9902
+ "prefer-tbody": PreferTbody,
9903
+ "require-csp-nonce": RequireCSPNonce,
9904
+ "require-sri": RequireSri,
9905
+ "script-element": ScriptElement,
9906
+ "script-type": ScriptType,
9907
+ "svg-focusable": SvgFocusable,
9908
+ "tel-non-breaking": TelNonBreaking,
9909
+ "text-content": TextContent,
9910
+ "unique-landmark": UniqueLandmark,
9911
+ "unrecognized-char-ref": UnknownCharReference,
9912
+ "valid-autocomplete": ValidAutocomplete,
9913
+ "valid-id": ValidID,
9914
+ "void-content": VoidContent,
9915
+ "void-style": VoidStyle,
9916
+ ...bundledRules$1
9917
+ };
9918
+
9919
+ const ruleIds = new Set(Object.keys(bundledRules));
9920
+ function ruleExists(ruleId) {
9921
+ return ruleIds.has(ruleId);
9938
9922
  }
9939
- function styleDescription(style) {
9940
- switch (style) {
9941
- case 1 /* AlwaysOmit */:
9942
- return ["omit end tag", ""];
9943
- case 2 /* AlwaysSelfclose */:
9944
- return ["be self-closed", "/"];
9945
- default:
9946
- throw new Error(`Unknown style`);
9923
+
9924
+ function depthFirst(root, callback) {
9925
+ if (root instanceof DOMTree) {
9926
+ if (root.readyState !== "complete") {
9927
+ throw new Error(`Cannot call walk.depthFirst(..) before document is ready`);
9928
+ }
9929
+ root = root.root;
9930
+ }
9931
+ function visit(node) {
9932
+ node.childElements.forEach(visit);
9933
+ if (!node.isRootElement()) {
9934
+ callback(node);
9935
+ }
9947
9936
  }
9937
+ visit(root);
9948
9938
  }
9939
+ const walk = {
9940
+ depthFirst
9941
+ };
9949
9942
 
9950
- class H30 extends Rule {
9951
- documentation() {
9952
- return {
9953
- 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.",
9954
- url: "https://html-validate.org/rules/wcag/h30.html"
9955
- };
9943
+ class DOMTree {
9944
+ /**
9945
+ * @internal
9946
+ */
9947
+ constructor(location) {
9948
+ this.root = HtmlElement.rootNode(location);
9949
+ this.active = this.root;
9950
+ this.doctype = null;
9951
+ this._readyState = "loading";
9952
+ }
9953
+ /**
9954
+ * @internal
9955
+ */
9956
+ pushActive(node) {
9957
+ this.active = node;
9958
+ }
9959
+ /**
9960
+ * @internal
9961
+ */
9962
+ popActive() {
9963
+ if (this.active.isRootElement()) {
9964
+ return;
9965
+ }
9966
+ this.active = this.active.parent ?? this.root;
9967
+ }
9968
+ /**
9969
+ * @internal
9970
+ */
9971
+ getActive() {
9972
+ return this.active;
9973
+ }
9974
+ /**
9975
+ * Describes the loading state of the document.
9976
+ *
9977
+ * When `"loading"` it is still not safe to use functions such as
9978
+ * `querySelector` or presence of attributes, child nodes, etc.
9979
+ */
9980
+ get readyState() {
9981
+ return this._readyState;
9956
9982
  }
9957
- setup() {
9958
- this.on("dom:ready", (event) => {
9959
- const links = event.document.getElementsByTagName("a");
9960
- for (const link of links) {
9961
- if (!link.hasAttribute("href")) {
9962
- continue;
9963
- }
9964
- if (!inAccessibilityTree(link)) {
9965
- continue;
9966
- }
9967
- const textClassification = classifyNodeText(link, { ignoreHiddenRoot: true });
9968
- if (textClassification !== TextClassification.EMPTY_TEXT) {
9969
- continue;
9970
- }
9971
- const images = link.querySelectorAll("img");
9972
- if (images.some((image) => hasAltText(image))) {
9973
- continue;
9974
- }
9975
- const labels = link.querySelectorAll("[aria-label]");
9976
- if (hasAriaLabel(link) || labels.some((cur) => hasAriaLabel(cur))) {
9977
- continue;
9978
- }
9979
- this.report(link, "Anchor link must have a text describing its purpose");
9980
- }
9983
+ /**
9984
+ * Resolve dynamic meta expressions.
9985
+ *
9986
+ * @internal
9987
+ */
9988
+ resolveMeta(table) {
9989
+ this._readyState = "complete";
9990
+ walk.depthFirst(this, (node) => {
9991
+ table.resolve(node);
9981
9992
  });
9982
9993
  }
9983
- }
9984
-
9985
- class H32 extends Rule {
9986
- documentation() {
9987
- return {
9988
- description: "WCAG 2.1 requires each `<form>` element to have at least one submit button.",
9989
- url: "https://html-validate.org/rules/wcag/h32.html"
9990
- };
9994
+ getElementsByTagName(tagName) {
9995
+ return this.root.getElementsByTagName(tagName);
9991
9996
  }
9992
- setup() {
9993
- const formTags = this.getTagsWithProperty("form");
9994
- const formSelector = formTags.join(",");
9995
- this.on("dom:ready", (event) => {
9996
- const { document } = event;
9997
- const forms = document.querySelectorAll(formSelector);
9998
- for (const form of forms) {
9999
- if (hasNestedSubmit(form)) {
10000
- continue;
10001
- }
10002
- if (hasAssociatedSubmit(document, form)) {
10003
- continue;
10004
- }
10005
- this.report(form, `<${form.tagName}> element must have a submit button`);
10006
- }
10007
- });
9997
+ /**
9998
+ * @deprecated use utility function `walk.depthFirst(..)` instead (since 8.21.0).
9999
+ */
10000
+ visitDepthFirst(callback) {
10001
+ walk.depthFirst(this, callback);
10008
10002
  }
10009
- }
10010
- function isSubmit(node) {
10011
- const type = node.getAttribute("type");
10012
- return Boolean(!type || type.valueMatches(/submit|image/));
10013
- }
10014
- function isAssociated(id, node) {
10015
- const form = node.getAttribute("form");
10016
- return Boolean(form == null ? void 0 : form.valueMatches(id, true));
10017
- }
10018
- function hasNestedSubmit(form) {
10019
- const matches = form.querySelectorAll("button,input").filter(isSubmit).filter((node) => !node.hasAttribute("form"));
10020
- return matches.length > 0;
10021
- }
10022
- function hasAssociatedSubmit(document, form) {
10023
- const { id } = form;
10024
- if (!id) {
10025
- return false;
10003
+ /**
10004
+ * @deprecated use `querySelector(..)` instead (since 8.21.0)
10005
+ */
10006
+ find(callback) {
10007
+ return this.root.find(callback);
10026
10008
  }
10027
- const matches = document.querySelectorAll("button[form],input[form]").filter(isSubmit).filter((node) => isAssociated(id, node));
10028
- return matches.length > 0;
10029
- }
10030
-
10031
- class H36 extends Rule {
10032
- documentation() {
10033
- return {
10034
- description: [
10035
- "WCAG 2.1 requires all images used as submit buttons to have a non-empty textual description using the `alt` attribute.",
10036
- 'The alt text cannot be empty (`alt=""`).'
10037
- ].join("\n"),
10038
- url: "https://html-validate.org/rules/wcag/h36.html"
10039
- };
10009
+ querySelector(selector) {
10010
+ return this.root.querySelector(selector);
10040
10011
  }
10041
- setup() {
10042
- this.on("tag:end", (event) => {
10043
- const node = event.previous;
10044
- if (node.tagName !== "input")
10045
- return;
10046
- if (node.getAttributeValue("type") !== "image") {
10047
- return;
10048
- }
10049
- if (!inAccessibilityTree(node)) {
10050
- return;
10051
- }
10052
- if (!hasAltText(node)) {
10053
- const message = "image used as submit button must have non-empty alt text";
10054
- const alt = node.getAttribute("alt");
10055
- this.report({
10056
- node,
10057
- message,
10058
- location: alt ? alt.keyLocation : node.location
10059
- });
10060
- }
10061
- });
10012
+ querySelectorAll(selector) {
10013
+ return this.root.querySelectorAll(selector);
10062
10014
  }
10063
10015
  }
10064
10016
 
10065
- const defaults$1 = {
10066
- allowEmpty: true,
10067
- alias: []
10068
- };
10069
- class H37 extends Rule {
10070
- constructor(options) {
10071
- super({ ...defaults$1, ...options });
10072
- if (!Array.isArray(this.options.alias)) {
10073
- this.options.alias = [this.options.alias];
10017
+ const allowedKeys = ["exclude"];
10018
+ class Validator {
10019
+ /**
10020
+ * Test if element is used in a proper context.
10021
+ *
10022
+ * @param node - Element to test.
10023
+ * @param rules - List of rules.
10024
+ * @returns `true` if element passes all tests.
10025
+ */
10026
+ static validatePermitted(node, rules) {
10027
+ if (!rules) {
10028
+ return true;
10074
10029
  }
10030
+ return rules.some((rule) => {
10031
+ return Validator.validatePermittedRule(node, rule);
10032
+ });
10075
10033
  }
10076
- static schema() {
10077
- return {
10078
- alias: {
10079
- anyOf: [
10080
- {
10081
- items: {
10082
- type: "string"
10083
- },
10084
- type: "array"
10085
- },
10086
- {
10087
- type: "string"
10034
+ /**
10035
+ * Test if an element is used the correct amount of times.
10036
+ *
10037
+ * For instance, a `<table>` element can only contain a single `<tbody>`
10038
+ * child. If multiple `<tbody>` exists this test will fail both nodes.
10039
+ * Note that this is called on the parent but will fail the children violating
10040
+ * the rule.
10041
+ *
10042
+ * @param children - Array of children to validate.
10043
+ * @param rules - List of rules of the parent element.
10044
+ * @returns `true` if the parent element of the children passes the test.
10045
+ */
10046
+ static validateOccurrences(children, rules, cb) {
10047
+ if (!rules) {
10048
+ return true;
10049
+ }
10050
+ let valid = true;
10051
+ for (const rule of rules) {
10052
+ if (typeof rule !== "string") {
10053
+ return false;
10054
+ }
10055
+ const [, category, quantifier] = rule.match(/^(@?.*?)([?*]?)$/);
10056
+ const limit = category && quantifier && parseQuantifier(quantifier);
10057
+ if (limit) {
10058
+ const siblings = children.filter(
10059
+ (cur) => Validator.validatePermittedCategory(cur, rule, true)
10060
+ );
10061
+ if (siblings.length > limit) {
10062
+ for (const child of siblings.slice(limit)) {
10063
+ cb(child, category);
10088
10064
  }
10089
- ]
10090
- },
10091
- allowEmpty: {
10092
- type: "boolean"
10065
+ valid = false;
10066
+ }
10093
10067
  }
10094
- };
10095
- }
10096
- documentation() {
10097
- return {
10098
- description: "Both HTML5 and WCAG 2.0 requires images to have a alternative text for each image.",
10099
- url: "https://html-validate.org/rules/wcag/h37.html"
10100
- };
10068
+ }
10069
+ return valid;
10101
10070
  }
10102
- setup() {
10103
- this.on("dom:ready", (event) => {
10104
- const { document } = event;
10105
- const nodes = document.querySelectorAll("img");
10106
- for (const node of nodes) {
10107
- this.validateNode(node);
10071
+ /**
10072
+ * Validate elements order.
10073
+ *
10074
+ * Given a parent element with children and metadata containing permitted
10075
+ * order it will validate each children and ensure each one exists in the
10076
+ * specified order.
10077
+ *
10078
+ * For instance, for a `<table>` element the `<caption>` element must come
10079
+ * before a `<thead>` which must come before `<tbody>`.
10080
+ *
10081
+ * @param children - Array of children to validate.
10082
+ */
10083
+ static validateOrder(children, rules, cb) {
10084
+ if (!rules) {
10085
+ return true;
10086
+ }
10087
+ let i = 0;
10088
+ let prev = null;
10089
+ for (const node of children) {
10090
+ const old = i;
10091
+ while (rules[i] && !Validator.validatePermittedCategory(node, rules[i], true)) {
10092
+ i++;
10093
+ }
10094
+ if (i >= rules.length) {
10095
+ const orderSpecified = rules.find(
10096
+ (cur) => Validator.validatePermittedCategory(node, cur, true)
10097
+ );
10098
+ if (orderSpecified) {
10099
+ cb(node, prev);
10100
+ return false;
10101
+ }
10102
+ i = old;
10108
10103
  }
10104
+ prev = node;
10105
+ }
10106
+ return true;
10107
+ }
10108
+ /**
10109
+ * Validate element ancestors.
10110
+ *
10111
+ * Check if an element has the required set of elements. At least one of the
10112
+ * selectors must match.
10113
+ */
10114
+ static validateAncestors(node, rules) {
10115
+ if (!rules || rules.length === 0) {
10116
+ return true;
10117
+ }
10118
+ return rules.some((rule) => node.closest(rule));
10119
+ }
10120
+ /**
10121
+ * Validate element required content.
10122
+ *
10123
+ * Check if an element has the required set of elements. At least one of the
10124
+ * selectors must match.
10125
+ *
10126
+ * Returns `[]` when valid or a list of required but missing tagnames or
10127
+ * categories.
10128
+ */
10129
+ static validateRequiredContent(node, rules) {
10130
+ if (!rules || rules.length === 0) {
10131
+ return [];
10132
+ }
10133
+ return rules.filter((tagName) => {
10134
+ const haveMatchingChild = node.childElements.some(
10135
+ (child) => Validator.validatePermittedCategory(child, tagName, false)
10136
+ );
10137
+ return !haveMatchingChild;
10109
10138
  });
10110
10139
  }
10111
- validateNode(node) {
10112
- if (!inAccessibilityTree(node)) {
10113
- return;
10140
+ /**
10141
+ * Test if an attribute has an allowed value and/or format.
10142
+ *
10143
+ * @param attr - Attribute to test.
10144
+ * @param rules - Element attribute metadta.
10145
+ * @returns `true` if attribute passes all tests.
10146
+ */
10147
+ static validateAttribute(attr, rules) {
10148
+ const rule = rules[attr.key];
10149
+ if (!rule) {
10150
+ return true;
10114
10151
  }
10115
- if (Boolean(node.getAttributeValue("alt")) || Boolean(node.hasAttribute("alt") && this.options.allowEmpty)) {
10116
- return;
10152
+ const value = attr.value;
10153
+ if (value instanceof DynamicValue) {
10154
+ return true;
10117
10155
  }
10118
- for (const attr of this.options.alias) {
10119
- if (node.getAttribute(attr)) {
10120
- return;
10121
- }
10156
+ const empty = value === null || value === "";
10157
+ if (rule.boolean) {
10158
+ return empty || value === attr.key;
10122
10159
  }
10123
- const tag = node.annotatedName;
10124
- if (node.hasAttribute("alt")) {
10125
- const attr = node.getAttribute("alt");
10126
- this.report(node, `${tag} cannot have empty "alt" attribute`, attr.keyLocation);
10127
- } else {
10128
- this.report(node, `${tag} is missing required "alt" attribute`, node.location);
10160
+ if (rule.omit && empty) {
10161
+ return true;
10129
10162
  }
10163
+ if (rule.list) {
10164
+ const tokens = new DOMTokenList(value, attr.valueLocation);
10165
+ return tokens.every((token) => {
10166
+ return this.validateAttributeValue(token, rule);
10167
+ });
10168
+ }
10169
+ return this.validateAttributeValue(value, rule);
10130
10170
  }
10131
- }
10132
-
10133
- var _a;
10134
- const { enum: validScopes } = (_a = html5.th.attributes) == null ? void 0 : _a.scope;
10135
- const joinedScopes = naturalJoin(validScopes);
10136
- class H63 extends Rule {
10137
- documentation() {
10138
- return {
10139
- description: "H63: Using the scope attribute to associate header cells and data cells in data tables",
10140
- url: "https://html-validate.org/rules/wcag/h63.html"
10141
- };
10142
- }
10143
- setup() {
10144
- this.on("tag:ready", (event) => {
10145
- const node = event.target;
10146
- if (node.tagName !== "th") {
10147
- return;
10148
- }
10149
- const scope = node.getAttribute("scope");
10150
- const value = scope == null ? void 0 : scope.value;
10151
- if (value instanceof DynamicValue) {
10152
- return;
10153
- }
10154
- if (value && validScopes.includes(value)) {
10155
- return;
10156
- }
10157
- const message = `<th> element must have a valid scope attribute: ${joinedScopes}`;
10158
- const location = (scope == null ? void 0 : scope.valueLocation) ?? (scope == null ? void 0 : scope.keyLocation) ?? node.location;
10159
- this.report(node, message, location);
10160
- });
10161
- }
10162
- }
10163
-
10164
- class H67 extends Rule {
10165
- documentation() {
10166
- return {
10167
- description: "A decorative image cannot have a title attribute. Either remove `title` or add a descriptive `alt` text.",
10168
- url: "https://html-validate.org/rules/wcag/h67.html"
10169
- };
10170
- }
10171
- setup() {
10172
- this.on("tag:end", (event) => {
10173
- const node = event.target;
10174
- if (!node || node.tagName !== "img") {
10175
- return;
10176
- }
10177
- const title = node.getAttribute("title");
10178
- if (!title || title.value === "") {
10179
- return;
10180
- }
10181
- const alt = node.getAttributeValue("alt");
10182
- if (alt && alt !== "") {
10183
- return;
10171
+ static validateAttributeValue(value, rule) {
10172
+ if (!rule.enum) {
10173
+ return true;
10174
+ }
10175
+ if (value === null) {
10176
+ return false;
10177
+ }
10178
+ const caseInsensitiveValue = value.toLowerCase();
10179
+ return rule.enum.some((entry) => {
10180
+ if (entry instanceof RegExp) {
10181
+ return !!value.match(entry);
10182
+ } else {
10183
+ return caseInsensitiveValue === entry;
10184
10184
  }
10185
- this.report(node, "<img> with empty alt text cannot have title attribute", title.keyLocation);
10186
10185
  });
10187
10186
  }
10188
- }
10189
-
10190
- class H71 extends Rule {
10191
- documentation() {
10192
- return {
10193
- description: "H71: Providing a description for groups of form controls using fieldset and legend elements",
10194
- url: "https://html-validate.org/rules/wcag/h71.html"
10195
- };
10196
- }
10197
- setup() {
10198
- this.on("dom:ready", (event) => {
10199
- const { document } = event;
10200
- const fieldsets = document.querySelectorAll(this.selector);
10201
- for (const fieldset of fieldsets) {
10202
- this.validate(fieldset);
10187
+ static validatePermittedRule(node, rule, isExclude = false) {
10188
+ if (typeof rule === "string") {
10189
+ return Validator.validatePermittedCategory(node, rule, !isExclude);
10190
+ } else if (Array.isArray(rule)) {
10191
+ return rule.every((inner) => {
10192
+ return Validator.validatePermittedRule(node, inner, isExclude);
10193
+ });
10194
+ } else {
10195
+ validateKeys(rule);
10196
+ if (rule.exclude) {
10197
+ if (Array.isArray(rule.exclude)) {
10198
+ return !rule.exclude.some((inner) => {
10199
+ return Validator.validatePermittedRule(node, inner, true);
10200
+ });
10201
+ } else {
10202
+ return !Validator.validatePermittedRule(node, rule.exclude, true);
10203
+ }
10204
+ } else {
10205
+ return true;
10203
10206
  }
10204
- });
10207
+ }
10205
10208
  }
10206
- validate(fieldset) {
10207
- const legend = fieldset.querySelectorAll("> legend");
10208
- if (legend.length === 0) {
10209
- this.reportNode(fieldset);
10209
+ /**
10210
+ * Validate node against a content category.
10211
+ *
10212
+ * When matching parent nodes against permitted parents use the superset
10213
+ * parameter to also match for `@flow`. E.g. if a node expects a `@phrasing`
10214
+ * parent it should also allow `@flow` parent since `@phrasing` is a subset of
10215
+ * `@flow`.
10216
+ *
10217
+ * @param node - The node to test against
10218
+ * @param category - Name of category with `@` prefix or tag name.
10219
+ * @param defaultMatch - The default return value when node categories is not known.
10220
+ */
10221
+ /* eslint-disable-next-line complexity -- rule does not like switch */
10222
+ static validatePermittedCategory(node, category, defaultMatch) {
10223
+ const [, rawCategory] = category.match(/^(@?.*?)([?*]?)$/);
10224
+ if (!rawCategory.startsWith("@")) {
10225
+ return node.tagName === rawCategory;
10226
+ }
10227
+ if (!node.meta) {
10228
+ return defaultMatch;
10229
+ }
10230
+ switch (rawCategory) {
10231
+ case "@meta":
10232
+ return node.meta.metadata;
10233
+ case "@flow":
10234
+ return node.meta.flow;
10235
+ case "@sectioning":
10236
+ return node.meta.sectioning;
10237
+ case "@heading":
10238
+ return node.meta.heading;
10239
+ case "@phrasing":
10240
+ return node.meta.phrasing;
10241
+ case "@embedded":
10242
+ return node.meta.embedded;
10243
+ case "@interactive":
10244
+ return node.meta.interactive;
10245
+ case "@script":
10246
+ return Boolean(node.meta.scriptSupporting);
10247
+ case "@form":
10248
+ return Boolean(node.meta.form);
10249
+ default:
10250
+ throw new Error(`Invalid content category "${category}"`);
10210
10251
  }
10211
10252
  }
10212
- reportNode(node) {
10213
- super.report(node, `${node.annotatedName} must have a <legend> as the first child`);
10253
+ }
10254
+ function validateKeys(rule) {
10255
+ for (const key of Object.keys(rule)) {
10256
+ if (!allowedKeys.includes(key)) {
10257
+ const str = JSON.stringify(rule);
10258
+ throw new Error(`Permitted rule "${str}" contains unknown property "${key}"`);
10259
+ }
10214
10260
  }
10215
- get selector() {
10216
- return this.getTagsDerivedFrom("fieldset").join(",");
10261
+ }
10262
+ function parseQuantifier(quantifier) {
10263
+ switch (quantifier) {
10264
+ case "?":
10265
+ return 1;
10266
+ case "*":
10267
+ return null;
10268
+ default:
10269
+ throw new Error(`Invalid quantifier "${quantifier}" used`);
10217
10270
  }
10218
10271
  }
10219
10272
 
10220
- const bundledRules$1 = {
10221
- "wcag/h30": H30,
10222
- "wcag/h32": H32,
10223
- "wcag/h36": H36,
10224
- "wcag/h37": H37,
10225
- "wcag/h63": H63,
10226
- "wcag/h67": H67,
10227
- "wcag/h71": H71
10273
+ const $schema = "http://json-schema.org/draft-06/schema#";
10274
+ const $id = "https://html-validate.org/schemas/config.json";
10275
+ const type = "object";
10276
+ const additionalProperties = false;
10277
+ const properties = {
10278
+ $schema: {
10279
+ type: "string"
10280
+ },
10281
+ root: {
10282
+ type: "boolean",
10283
+ title: "Mark as root configuration",
10284
+ description: "If this is set to true no further configurations will be searched.",
10285
+ "default": false
10286
+ },
10287
+ "extends": {
10288
+ type: "array",
10289
+ items: {
10290
+ type: "string"
10291
+ },
10292
+ title: "Configurations to extend",
10293
+ description: "Array of shareable or builtin configurations to extend."
10294
+ },
10295
+ elements: {
10296
+ type: "array",
10297
+ items: {
10298
+ anyOf: [
10299
+ {
10300
+ type: "string"
10301
+ },
10302
+ {
10303
+ type: "object"
10304
+ }
10305
+ ]
10306
+ },
10307
+ title: "Element metadata to load",
10308
+ description: "Array of modules, plugins or files to load element metadata from. Use <rootDir> to refer to the folder with the package.json file.",
10309
+ examples: [
10310
+ [
10311
+ "html-validate:recommended",
10312
+ "plugin:recommended",
10313
+ "module",
10314
+ "./local-file.json"
10315
+ ]
10316
+ ]
10317
+ },
10318
+ plugins: {
10319
+ type: "array",
10320
+ items: {
10321
+ anyOf: [
10322
+ {
10323
+ type: "string"
10324
+ },
10325
+ {
10326
+ type: "object"
10327
+ }
10328
+ ]
10329
+ },
10330
+ title: "Plugins to load",
10331
+ description: "Array of plugins load. Use <rootDir> to refer to the folder with the package.json file.",
10332
+ examples: [
10333
+ [
10334
+ "my-plugin",
10335
+ "./local-plugin"
10336
+ ]
10337
+ ]
10338
+ },
10339
+ transform: {
10340
+ type: "object",
10341
+ additionalProperties: {
10342
+ type: "string"
10343
+ },
10344
+ title: "File transformations to use.",
10345
+ description: "Object where key is regular expression to match filename and value is name of transformer.",
10346
+ examples: [
10347
+ {
10348
+ "^.*\\.foo$": "my-transformer",
10349
+ "^.*\\.bar$": "my-plugin",
10350
+ "^.*\\.baz$": "my-plugin:named"
10351
+ }
10352
+ ]
10353
+ },
10354
+ rules: {
10355
+ type: "object",
10356
+ patternProperties: {
10357
+ ".*": {
10358
+ anyOf: [
10359
+ {
10360
+ "enum": [
10361
+ 0,
10362
+ 1,
10363
+ 2,
10364
+ "off",
10365
+ "warn",
10366
+ "error"
10367
+ ]
10368
+ },
10369
+ {
10370
+ type: "array",
10371
+ minItems: 1,
10372
+ maxItems: 1,
10373
+ items: [
10374
+ {
10375
+ "enum": [
10376
+ 0,
10377
+ 1,
10378
+ 2,
10379
+ "off",
10380
+ "warn",
10381
+ "error"
10382
+ ]
10383
+ }
10384
+ ]
10385
+ },
10386
+ {
10387
+ type: "array",
10388
+ minItems: 2,
10389
+ maxItems: 2,
10390
+ items: [
10391
+ {
10392
+ "enum": [
10393
+ 0,
10394
+ 1,
10395
+ 2,
10396
+ "off",
10397
+ "warn",
10398
+ "error"
10399
+ ]
10400
+ },
10401
+ {
10402
+ }
10403
+ ]
10404
+ }
10405
+ ]
10406
+ }
10407
+ },
10408
+ title: "Rule configuration.",
10409
+ description: "Enable/disable rules, set severity. Some rules have additional configuration like style or patterns to use.",
10410
+ examples: [
10411
+ {
10412
+ foo: "error",
10413
+ bar: "off",
10414
+ baz: [
10415
+ "error",
10416
+ {
10417
+ style: "camelcase"
10418
+ }
10419
+ ]
10420
+ }
10421
+ ]
10422
+ }
10423
+ };
10424
+ var configurationSchema = {
10425
+ $schema: $schema,
10426
+ $id: $id,
10427
+ type: type,
10428
+ additionalProperties: additionalProperties,
10429
+ properties: properties
10228
10430
  };
10229
10431
 
10230
- const bundledRules = {
10231
- "allowed-links": AllowedLinks,
10232
- "area-alt": AreaAlt,
10233
- "aria-hidden-body": AriaHiddenBody,
10234
- "aria-label-misuse": AriaLabelMisuse,
10235
- "attr-case": AttrCase,
10236
- "attr-delimiter": AttrDelimiter,
10237
- "attr-pattern": AttrPattern,
10238
- "attr-quotes": AttrQuotes,
10239
- "attr-spacing": AttrSpacing,
10240
- "attribute-allowed-values": AttributeAllowedValues,
10241
- "attribute-boolean-style": AttributeBooleanStyle,
10242
- "attribute-empty-style": AttributeEmptyStyle,
10243
- "attribute-misuse": AttributeMisuse,
10244
- "class-pattern": ClassPattern,
10245
- "close-attr": CloseAttr,
10246
- "close-order": CloseOrder,
10247
- deprecated: Deprecated,
10248
- "deprecated-rule": DeprecatedRule,
10249
- "doctype-html": NoStyleTag$1,
10250
- "doctype-style": DoctypeStyle,
10251
- "element-case": ElementCase,
10252
- "element-name": ElementName,
10253
- "element-permitted-content": ElementPermittedContent,
10254
- "element-permitted-occurrences": ElementPermittedOccurrences,
10255
- "element-permitted-order": ElementPermittedOrder,
10256
- "element-permitted-parent": ElementPermittedParent,
10257
- "element-required-ancestor": ElementRequiredAncestor,
10258
- "element-required-attributes": ElementRequiredAttributes,
10259
- "element-required-content": ElementRequiredContent,
10260
- "empty-heading": EmptyHeading,
10261
- "empty-title": EmptyTitle,
10262
- "form-dup-name": FormDupName,
10263
- "heading-level": HeadingLevel,
10264
- "hidden-focusable": HiddenFocusable,
10265
- "id-pattern": IdPattern,
10266
- "input-attributes": InputAttributes,
10267
- "input-missing-label": InputMissingLabel,
10268
- "long-title": LongTitle,
10269
- "map-dup-name": MapDupName,
10270
- "map-id-name": MapIdName,
10271
- "meta-refresh": MetaRefresh,
10272
- "missing-doctype": MissingDoctype,
10273
- "multiple-labeled-controls": MultipleLabeledControls,
10274
- "name-pattern": NamePattern,
10275
- "no-abstract-role": NoAbstractRole,
10276
- "no-autoplay": NoAutoplay,
10277
- "no-conditional-comment": NoConditionalComment,
10278
- "no-deprecated-attr": NoDeprecatedAttr,
10279
- "no-dup-attr": NoDupAttr,
10280
- "no-dup-class": NoDupClass,
10281
- "no-dup-id": NoDupID,
10282
- "no-implicit-button-type": NoImplicitButtonType,
10283
- "no-implicit-input-type": NoImplicitInputType,
10284
- "no-implicit-close": NoImplicitClose,
10285
- "no-inline-style": NoInlineStyle,
10286
- "no-missing-references": NoMissingReferences,
10287
- "no-multiple-main": NoMultipleMain,
10288
- "no-raw-characters": NoRawCharacters,
10289
- "no-redundant-aria-label": NoRedundantAriaLabel,
10290
- "no-redundant-for": NoRedundantFor,
10291
- "no-redundant-role": NoRedundantRole,
10292
- "no-self-closing": NoSelfClosing,
10293
- "no-style-tag": NoStyleTag,
10294
- "no-trailing-whitespace": NoTrailingWhitespace,
10295
- "no-unknown-elements": NoUnknownElements,
10296
- "no-unused-disable": NoUnusedDisable,
10297
- "no-utf8-bom": NoUtf8Bom,
10298
- "prefer-button": PreferButton,
10299
- "prefer-native-element": PreferNativeElement,
10300
- "prefer-tbody": PreferTbody,
10301
- "require-csp-nonce": RequireCSPNonce,
10302
- "require-sri": RequireSri,
10303
- "script-element": ScriptElement,
10304
- "script-type": ScriptType,
10305
- "svg-focusable": SvgFocusable,
10306
- "tel-non-breaking": TelNonBreaking,
10307
- "text-content": TextContent,
10308
- "unique-landmark": UniqueLandmark,
10309
- "unrecognized-char-ref": UnknownCharReference,
10310
- "valid-autocomplete": ValidAutocomplete,
10311
- "valid-id": ValidID,
10312
- "void-content": VoidContent,
10313
- "void-style": VoidStyle,
10314
- ...bundledRules$1
10432
+ const TRANSFORMER_API = {
10433
+ VERSION: 1
10315
10434
  };
10316
10435
 
10317
10436
  var defaultConfig = {};
@@ -11439,7 +11558,7 @@ class Parser {
11439
11558
  location
11440
11559
  };
11441
11560
  this.trigger("tag:end", event);
11442
- if (active && !active.isRootElement()) {
11561
+ if (!active.isRootElement()) {
11443
11562
  this.trigger("element:ready", {
11444
11563
  target: active,
11445
11564
  location: active.location
@@ -11791,15 +11910,6 @@ class Parser {
11791
11910
  }
11792
11911
  }
11793
11912
 
11794
- function isThenable(value) {
11795
- return value && typeof value === "object" && "then" in value && typeof value.then === "function";
11796
- }
11797
-
11798
- const ruleIds = new Set(Object.keys(bundledRules));
11799
- function ruleExists(ruleId) {
11800
- return ruleIds.has(ruleId);
11801
- }
11802
-
11803
11913
  function freeze(src) {
11804
11914
  return {
11805
11915
  ...src,
@@ -11970,7 +12080,7 @@ class Engine {
11970
12080
  const directiveContext = {
11971
12081
  rules,
11972
12082
  reportUnused(rules2, unused, options, location2) {
11973
- if (noUnusedDisable && !rules2.has(noUnusedDisable.name)) {
12083
+ if (!rules2.has(noUnusedDisable.name)) {
11974
12084
  noUnusedDisable.reportUnused(unused, options, location2);
11975
12085
  }
11976
12086
  }
@@ -12047,33 +12157,8 @@ class Engine {
12047
12157
  }
12048
12158
  dumpTree(source) {
12049
12159
  const parser = this.instantiateParser();
12050
- const document = parser.parseHtml(source[0]);
12051
- const lines = [];
12052
- function decoration(node) {
12053
- let output = "";
12054
- if (node.id) {
12055
- output += `#${node.id}`;
12056
- }
12057
- if (node.hasAttribute("class")) {
12058
- output += `.${node.classList.join(".")}`;
12059
- }
12060
- return output;
12061
- }
12062
- function writeNode(node, level, sibling) {
12063
- if (node.parent) {
12064
- const indent = " ".repeat(level - 1);
12065
- const l = node.childElements.length > 0 ? "\u252C" : "\u2500";
12066
- const b = sibling < node.parent.childElements.length - 1 ? "\u251C" : "\u2514";
12067
- lines.push(`${indent}${b}\u2500${l} ${node.tagName}${decoration(node)}`);
12068
- } else {
12069
- lines.push("(root)");
12070
- }
12071
- node.childElements.forEach((child, index) => {
12072
- writeNode(child, level + 1, index);
12073
- });
12074
- }
12075
- writeNode(document, 0, 0);
12076
- return lines;
12160
+ const root = parser.parseHtml(source[0]);
12161
+ return dumpTree(root);
12077
12162
  }
12078
12163
  /**
12079
12164
  * Get rule documentation.
@@ -12101,7 +12186,9 @@ class Engine {
12101
12186
  return new this.ParserClass(this.config);
12102
12187
  }
12103
12188
  processDirective(event, parser, context) {
12104
- const rules = event.data.split(",").map((name) => name.trim()).map((name) => context.rules[name]).filter((rule) => rule);
12189
+ const rules = event.data.split(",").map((name) => name.trim()).map((name) => context.rules[name]).filter((rule) => {
12190
+ return Boolean(rule);
12191
+ });
12105
12192
  const location = event.optionsLocation ?? event.location;
12106
12193
  switch (event.action) {
12107
12194
  case "enable":
@@ -12125,7 +12212,7 @@ class Engine {
12125
12212
  rule.setServerity(Severity.ERROR);
12126
12213
  }
12127
12214
  }
12128
- parser.on("tag:start", (event, data) => {
12215
+ parser.on("tag:start", (_event, data) => {
12129
12216
  data.target.enableRules(rules.map((rule) => rule.name));
12130
12217
  });
12131
12218
  }
@@ -12133,7 +12220,7 @@ class Engine {
12133
12220
  for (const rule of rules) {
12134
12221
  rule.setEnabled(false);
12135
12222
  }
12136
- parser.on("tag:start", (event, data) => {
12223
+ parser.on("tag:start", (_event, data) => {
12137
12224
  data.target.disableRules(rules.map((rule) => rule.name));
12138
12225
  });
12139
12226
  }
@@ -12145,14 +12232,14 @@ class Engine {
12145
12232
  for (const rule of rules) {
12146
12233
  rule.block(blocker);
12147
12234
  }
12148
- const unregisterOpen = parser.on("tag:start", (event, data) => {
12235
+ const unregisterOpen = parser.on("tag:start", (_event, data) => {
12149
12236
  var _a;
12150
12237
  if (directiveBlock === null) {
12151
12238
  directiveBlock = ((_a = data.target.parent) == null ? void 0 : _a.unique) ?? null;
12152
12239
  }
12153
12240
  data.target.blockRules(ruleIds, blocker);
12154
12241
  });
12155
- const unregisterClose = parser.on("tag:end", (event, data) => {
12242
+ const unregisterClose = parser.on("tag:end", (_event, data) => {
12156
12243
  const lastNode = directiveBlock === null;
12157
12244
  const parentClosed = directiveBlock === data.previous.unique;
12158
12245
  if (lastNode || parentClosed) {
@@ -12163,7 +12250,7 @@ class Engine {
12163
12250
  }
12164
12251
  }
12165
12252
  });
12166
- parser.on("rule:error", (event, data) => {
12253
+ parser.on("rule:error", (_event, data) => {
12167
12254
  if (data.blockers.includes(blocker)) {
12168
12255
  unused.delete(data.ruleId);
12169
12256
  }
@@ -12179,10 +12266,10 @@ class Engine {
12179
12266
  for (const rule of rules) {
12180
12267
  rule.block(blocker);
12181
12268
  }
12182
- const unregister = parser.on("tag:start", (event, data) => {
12269
+ const unregister = parser.on("tag:start", (_event, data) => {
12183
12270
  data.target.blockRules(ruleIds, blocker);
12184
12271
  });
12185
- parser.on("rule:error", (event, data) => {
12272
+ parser.on("rule:error", (_event, data) => {
12186
12273
  if (data.blockers.includes(blocker)) {
12187
12274
  unused.delete(data.ruleId);
12188
12275
  }
@@ -12746,7 +12833,7 @@ class HtmlValidate {
12746
12833
  }
12747
12834
 
12748
12835
  const name = "html-validate";
12749
- const version = "8.20.1";
12836
+ const version = "8.22.0";
12750
12837
  const bugs = "https://gitlab.com/html-validate/html-validate/issues/new";
12751
12838
 
12752
12839
  function definePlugin(plugin) {
@@ -12834,17 +12921,26 @@ const REPLACERS = [
12834
12921
  [
12835
12922
  // (a\ ) -> (a )
12836
12923
  // (a ) -> (a)
12924
+ // (a ) -> (a)
12837
12925
  // (a \ ) -> (a )
12838
- /\\?\s+$/,
12839
- match => match.indexOf('\\') === 0
12840
- ? SPACE
12841
- : EMPTY
12926
+ /((?:\\\\)*?)(\\?\s+)$/,
12927
+ (_, m1, m2) => m1 + (
12928
+ m2.indexOf('\\') === 0
12929
+ ? SPACE
12930
+ : EMPTY
12931
+ )
12842
12932
  ],
12843
12933
 
12844
12934
  // replace (\ ) with ' '
12935
+ // (\ ) -> ' '
12936
+ // (\\ ) -> '\\ '
12937
+ // (\\\ ) -> '\\ '
12845
12938
  [
12846
- /\\\s/g,
12847
- () => SPACE
12939
+ /(\\+?)\s/g,
12940
+ (_, m1) => {
12941
+ const {length} = m1;
12942
+ return m1.slice(0, length - length % 2) + SPACE
12943
+ }
12848
12944
  ],
12849
12945
 
12850
12946
  // Escape metacharacters
@@ -13072,7 +13168,8 @@ const makeRegex = (pattern, ignoreCase) => {
13072
13168
 
13073
13169
  if (!source) {
13074
13170
  source = REPLACERS.reduce(
13075
- (prev, current) => prev.replace(current[0], current[1].bind(pattern)),
13171
+ (prev, [matcher, replacer]) =>
13172
+ prev.replace(matcher, replacer.bind(pattern)),
13076
13173
  pattern
13077
13174
  );
13078
13175
  regexCache[pattern] = source;
@@ -13438,6 +13535,81 @@ const defaults = {
13438
13535
  showSummary: true,
13439
13536
  showSelector: false
13440
13537
  };
13538
+ const NEWLINE = /\r\n|[\n\r\u2028\u2029]/;
13539
+ function getMarkerLines(loc, source) {
13540
+ const startLoc = {
13541
+ ...loc.start
13542
+ };
13543
+ const endLoc = {
13544
+ ...startLoc,
13545
+ ...loc.end
13546
+ };
13547
+ const linesAbove = 2;
13548
+ const linesBelow = 3;
13549
+ const startLine = startLoc.line;
13550
+ const startColumn = startLoc.column;
13551
+ const endLine = endLoc.line;
13552
+ const endColumn = endLoc.column;
13553
+ const start = Math.max(startLine - (linesAbove + 1), 0);
13554
+ const end = Math.min(source.length, endLine + linesBelow);
13555
+ const lineDiff = endLine - startLine;
13556
+ const markerLines = {};
13557
+ if (lineDiff) {
13558
+ for (let i = 0; i <= lineDiff; i++) {
13559
+ const lineNumber = i + startLine;
13560
+ if (!startColumn) {
13561
+ markerLines[lineNumber] = true;
13562
+ } else if (i === 0) {
13563
+ const sourceLength = source[lineNumber - 1].length;
13564
+ markerLines[lineNumber] = [startColumn, sourceLength - startColumn + 1];
13565
+ } else if (i === lineDiff) {
13566
+ markerLines[lineNumber] = [0, endColumn];
13567
+ } else {
13568
+ const sourceLength = source[lineNumber - i].length;
13569
+ markerLines[lineNumber] = [0, sourceLength];
13570
+ }
13571
+ }
13572
+ } else {
13573
+ if (startColumn === endColumn) {
13574
+ if (startColumn) {
13575
+ markerLines[startLine] = [startColumn, 0];
13576
+ } else {
13577
+ markerLines[startLine] = true;
13578
+ }
13579
+ } else {
13580
+ markerLines[startLine] = [startColumn, endColumn - startColumn];
13581
+ }
13582
+ }
13583
+ return { start, end, markerLines };
13584
+ }
13585
+ function codeFrameColumns(rawLines, loc) {
13586
+ const lines = rawLines.split(NEWLINE);
13587
+ const { start, end, markerLines } = getMarkerLines(loc, lines);
13588
+ const numberMaxWidth = String(end).length;
13589
+ return rawLines.split(NEWLINE, end).slice(start, end).map((line, index) => {
13590
+ const number = start + 1 + index;
13591
+ const paddedNumber = ` ${String(number)}`.slice(-numberMaxWidth);
13592
+ const gutter = ` ${paddedNumber} |`;
13593
+ const hasMarker = markerLines[number];
13594
+ if (hasMarker) {
13595
+ let markerLine = "";
13596
+ if (Array.isArray(hasMarker)) {
13597
+ const markerSpacing = line.slice(0, Math.max(hasMarker[0] - 1, 0)).replace(/[^\t]/g, " ");
13598
+ const numberOfMarkers = hasMarker[1] || 1;
13599
+ markerLine = [
13600
+ "\n ",
13601
+ gutter.replace(/\d/g, " "),
13602
+ " ",
13603
+ markerSpacing,
13604
+ "^".repeat(numberOfMarkers)
13605
+ ].join("");
13606
+ }
13607
+ return [">", gutter, line.length > 0 ? ` ${line}` : "", markerLine].join("");
13608
+ } else {
13609
+ return [" ", gutter, line.length > 0 ? ` ${line}` : ""].join("");
13610
+ }
13611
+ }).join("\n");
13612
+ }
13441
13613
  function pluralize(word, count) {
13442
13614
  return count === 1 ? word : `${word}s`;
13443
13615
  }
@@ -13480,16 +13652,11 @@ function formatMessage(message, parentResult, options) {
13480
13652
  ].filter(String).join(" ");
13481
13653
  const result = [firstLine];
13482
13654
  if (sourceCode) {
13483
- result.push(
13484
- codeFrameColumns(
13485
- sourceCode,
13486
- {
13487
- start: getStartLocation(message),
13488
- end: getEndLocation(message, sourceCode)
13489
- },
13490
- { highlightCode: false }
13491
- )
13492
- );
13655
+ const output = codeFrameColumns(sourceCode, {
13656
+ start: getStartLocation(message),
13657
+ end: getEndLocation(message, sourceCode)
13658
+ });
13659
+ result.push(output);
13493
13660
  }
13494
13661
  if (options.showSelector) {
13495
13662
  result.push(`${kleur.bold("Selector:")} ${message.selector ?? "-"}`);
@@ -13621,5 +13788,5 @@ function compatibilityCheckImpl(name, declared, options) {
13621
13788
  return false;
13622
13789
  }
13623
13790
 
13624
- export { Attribute as A, definePlugin as B, Config as C, DOMNode as D, Parser as E, ruleExists as F, EventHandler as G, HtmlValidate as H, compatibilityCheckImpl as I, codeframe as J, name as K, bugs as L, MetaCopyableProperty as M, NodeClosed as N, Presets as P, ResolvedConfig as R, Severity as S, TextNode as T, UserError as U, Validator as V, WrappedError as W, ConfigError as a, ConfigLoader as b, defineConfig as c, deepmerge$1 as d, ensureError as e, StaticConfigLoader as f, getFormatter as g, DOMTokenList as h, ignore$1 as i, DOMTree as j, DynamicValue as k, HtmlElement as l, NodeType as m, NestedError as n, SchemaValidationError as o, MetaTable as p, TextContent$1 as q, Rule as r, staticResolver as s, ariaNaming as t, TextClassification as u, version as v, classifyNodeText as w, keywordPatternMatcher as x, sliceLocation as y, Reporter as z };
13791
+ export { Attribute as A, definePlugin as B, Config as C, DOMNode as D, Parser as E, ruleExists as F, walk as G, HtmlValidate as H, EventHandler as I, compatibilityCheckImpl as J, codeframe as K, name as L, MetaCopyableProperty as M, NodeClosed as N, bugs as O, Presets as P, ResolvedConfig as R, Severity as S, TextNode as T, UserError as U, Validator as V, WrappedError as W, ConfigError as a, ConfigLoader as b, defineConfig as c, deepmerge$1 as d, ensureError as e, StaticConfigLoader as f, getFormatter as g, DOMTokenList as h, ignore$1 as i, DOMTree as j, DynamicValue as k, HtmlElement as l, NodeType as m, NestedError as n, SchemaValidationError as o, MetaTable as p, TextContent$1 as q, Rule as r, staticResolver as s, ariaNaming as t, TextClassification as u, version as v, classifyNodeText as w, keywordPatternMatcher as x, sliceLocation as y, Reporter as z };
13625
13792
  //# sourceMappingURL=core.js.map