html-validate 6.5.0 → 6.7.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 (43) hide show
  1. package/dist/cjs/browser.d.ts +1 -1
  2. package/dist/cjs/browser.js +2 -0
  3. package/dist/cjs/browser.js.map +1 -1
  4. package/dist/cjs/cli.js +9 -5
  5. package/dist/cjs/cli.js.map +1 -1
  6. package/dist/cjs/core.d.ts +14 -5
  7. package/dist/cjs/core.js +362 -123
  8. package/dist/cjs/core.js.map +1 -1
  9. package/dist/cjs/html-validate.js +57 -5
  10. package/dist/cjs/html-validate.js.map +1 -1
  11. package/dist/cjs/index.d.ts +2 -2
  12. package/dist/cjs/index.js +2 -0
  13. package/dist/cjs/index.js.map +1 -1
  14. package/dist/cjs/jest-lib.d.ts +2 -1
  15. package/dist/cjs/jest-lib.js +52 -27
  16. package/dist/cjs/jest-lib.js.map +1 -1
  17. package/dist/cjs/jest.d.ts +2 -2
  18. package/dist/cjs/jest.js +2 -0
  19. package/dist/cjs/jest.js.map +1 -1
  20. package/dist/cjs/test-utils.d.ts +1 -1
  21. package/dist/cjs/test-utils.js.map +1 -1
  22. package/dist/es/browser.d.ts +1 -1
  23. package/dist/es/browser.js +2 -0
  24. package/dist/es/browser.js.map +1 -1
  25. package/dist/es/cli.js +9 -5
  26. package/dist/es/cli.js.map +1 -1
  27. package/dist/es/core.d.ts +14 -5
  28. package/dist/es/core.js +340 -121
  29. package/dist/es/core.js.map +1 -1
  30. package/dist/es/html-validate.js +57 -5
  31. package/dist/es/html-validate.js.map +1 -1
  32. package/dist/es/index.d.ts +2 -2
  33. package/dist/es/index.js +2 -0
  34. package/dist/es/index.js.map +1 -1
  35. package/dist/es/jest-lib.d.ts +2 -1
  36. package/dist/es/jest-lib.js +52 -27
  37. package/dist/es/jest-lib.js.map +1 -1
  38. package/dist/es/jest.d.ts +2 -2
  39. package/dist/es/jest.js +2 -0
  40. package/dist/es/jest.js.map +1 -1
  41. package/dist/es/test-utils.d.ts +1 -1
  42. package/dist/es/test-utils.js.map +1 -1
  43. package/package.json +1 -152
package/dist/es/core.js CHANGED
@@ -2,6 +2,8 @@ import fs from 'fs';
2
2
  import betterAjvErrors from '@sidvind/better-ajv-errors';
3
3
  import Ajv from 'ajv';
4
4
  import deepmerge from 'deepmerge';
5
+ import * as espree from 'espree';
6
+ import * as walk from 'acorn-walk';
5
7
  import path from 'path';
6
8
  import semver from 'semver';
7
9
  import kleur from 'kleur';
@@ -245,7 +247,7 @@ class NestedError extends Error {
245
247
  constructor(message, nested) {
246
248
  super(message);
247
249
  Error.captureStackTrace(this, NestedError);
248
- if (nested) {
250
+ if (nested && nested.stack) {
249
251
  this.stack += `\nCaused by: ${nested.stack}`;
250
252
  }
251
253
  }
@@ -1838,8 +1840,13 @@ var NodeClosed;
1838
1840
  NodeClosed[NodeClosed["VoidSelfClosed"] = 3] = "VoidSelfClosed";
1839
1841
  NodeClosed[NodeClosed["ImplicitClosed"] = 4] = "ImplicitClosed";
1840
1842
  })(NodeClosed || (NodeClosed = {}));
1841
- function isElement(node) {
1842
- return node.nodeType === NodeType.ELEMENT_NODE;
1843
+ /**
1844
+ * Returns true if the node is an element node.
1845
+ *
1846
+ * @public
1847
+ */
1848
+ function isElementNode(node) {
1849
+ return Boolean(node && node.nodeType === NodeType.ELEMENT_NODE);
1843
1850
  }
1844
1851
  function isValidTagName(tagName) {
1845
1852
  return Boolean(tagName !== "" && tagName !== "*");
@@ -1880,10 +1887,13 @@ class HtmlElement extends DOMNode {
1880
1887
  }
1881
1888
  /**
1882
1889
  * @internal
1890
+ *
1891
+ * @param namespace - If given it is appended to the tagName.
1883
1892
  */
1884
- static fromTokens(startToken, endToken, parent, metaTable) {
1885
- const tagName = startToken.data[2];
1886
- if (!tagName) {
1893
+ static fromTokens(startToken, endToken, parent, metaTable, namespace = "") {
1894
+ const name = startToken.data[2];
1895
+ const tagName = namespace ? `${namespace}:${name}` : name;
1896
+ if (!name) {
1887
1897
  throw new Error("tagName cannot be empty");
1888
1898
  }
1889
1899
  const meta = metaTable ? metaTable.getMetaFor(tagName) : null;
@@ -1910,7 +1920,7 @@ class HtmlElement extends DOMNode {
1910
1920
  * Similar to childNodes but only elements.
1911
1921
  */
1912
1922
  get childElements() {
1913
- return this.childNodes.filter(isElement);
1923
+ return this.childNodes.filter(isElementNode);
1914
1924
  }
1915
1925
  /**
1916
1926
  * Find the first ancestor matching a selector.
@@ -2171,8 +2181,9 @@ class HtmlElement extends DOMNode {
2171
2181
  }, []);
2172
2182
  }
2173
2183
  querySelector(selector) {
2184
+ var _a;
2174
2185
  const it = this.querySelectorImpl(selector);
2175
- return it.next().value || null;
2186
+ return (_a = it.next().value) !== null && _a !== void 0 ? _a : null; // eslint-disable-line @typescript-eslint/no-unsafe-return
2176
2187
  }
2177
2188
  querySelectorAll(selector) {
2178
2189
  const it = this.querySelectorImpl(selector);
@@ -2739,8 +2750,6 @@ var configurationSchema = {
2739
2750
  };
2740
2751
 
2741
2752
  /* eslint-disable @typescript-eslint/no-non-null-assertion */
2742
- const espree = legacyRequire("espree");
2743
- const walk = legacyRequire("acorn-walk");
2744
2753
  function joinTemplateLiteral(nodes) {
2745
2754
  let offset = nodes[0].start + 1;
2746
2755
  let output = "";
@@ -2921,7 +2930,8 @@ class TemplateExtractor {
2921
2930
  extractObjectProperty(key) {
2922
2931
  const result = [];
2923
2932
  const { filename, data } = this;
2924
- walk.simple(this.ast, {
2933
+ const node = this.ast;
2934
+ walk.simple(node, {
2925
2935
  Property(node) {
2926
2936
  if (compareKey(node.key, key, filename)) {
2927
2937
  const source = extractLiteral(node.value, filename, data);
@@ -2945,7 +2955,7 @@ var TRANSFORMER_API;
2945
2955
  /** @public */
2946
2956
  const name = "html-validate";
2947
2957
  /** @public */
2948
- const version = "6.5.0";
2958
+ const version = "6.7.0";
2949
2959
  /** @public */
2950
2960
  const homepage = "https://html-validate.org";
2951
2961
  /** @public */
@@ -3266,7 +3276,7 @@ function ruleDocumentationUrl(filename) {
3266
3276
  return `${homepage}/rules/${normalized}.html`;
3267
3277
  }
3268
3278
 
3269
- const defaults$p = {
3279
+ const defaults$q = {
3270
3280
  allowExternal: true,
3271
3281
  allowRelative: true,
3272
3282
  allowAbsolute: true,
@@ -3310,7 +3320,7 @@ function matchList(value, list) {
3310
3320
  }
3311
3321
  class AllowedLinks extends Rule {
3312
3322
  constructor(options) {
3313
- super({ ...defaults$p, ...options });
3323
+ super({ ...defaults$q, ...options });
3314
3324
  this.allowExternal = parseAllow(this.options.allowExternal);
3315
3325
  this.allowRelative = parseAllow(this.options.allowRelative);
3316
3326
  this.allowAbsolute = parseAllow(this.options.allowAbsolute);
@@ -3589,7 +3599,7 @@ class CaseStyle {
3589
3599
  default: {
3590
3600
  const last = names.slice(-1);
3591
3601
  const rest = names.slice(0, -1);
3592
- return `${rest.join(", ")} or ${last}`;
3602
+ return `${rest.join(", ")} or ${last[0]}`;
3593
3603
  }
3594
3604
  }
3595
3605
  }
@@ -3605,19 +3615,19 @@ class CaseStyle {
3605
3615
  case "camelcase":
3606
3616
  return { pattern: /^[a-z][A-Za-z]*$/, name: "camelCase" };
3607
3617
  default:
3608
- throw new ConfigError(`Invalid style "${style}" for ${ruleId} rule`);
3618
+ throw new ConfigError(`Invalid style "${cur}" for ${ruleId} rule`);
3609
3619
  }
3610
3620
  });
3611
3621
  }
3612
3622
  }
3613
3623
 
3614
- const defaults$o = {
3624
+ const defaults$p = {
3615
3625
  style: "lowercase",
3616
3626
  ignoreForeign: true,
3617
3627
  };
3618
3628
  class AttrCase extends Rule {
3619
3629
  constructor(options) {
3620
- super({ ...defaults$o, ...options });
3630
+ super({ ...defaults$p, ...options });
3621
3631
  this.style = new CaseStyle(this.options.style, "attr-case");
3622
3632
  }
3623
3633
  static schema() {
@@ -3644,8 +3654,11 @@ class AttrCase extends Rule {
3644
3654
  };
3645
3655
  }
3646
3656
  documentation() {
3657
+ const { style } = this.options;
3647
3658
  return {
3648
- description: `Attribute name must be ${this.options.style}.`,
3659
+ description: Array.isArray(style)
3660
+ ? [`Attribute name must be in one of:`, "", ...style.map((it) => `- ${it}`)].join("\n")
3661
+ : `Attribute name must be in ${style}.`,
3649
3662
  url: ruleDocumentationUrl("@/rules/attr-case.ts"),
3650
3663
  };
3651
3664
  }
@@ -3936,7 +3949,7 @@ function isRelevant$3(event) {
3936
3949
  class AttrDelimiter extends Rule {
3937
3950
  documentation() {
3938
3951
  return {
3939
- description: `Attribute value should be separated by `,
3952
+ description: `Attribute value must not be separated by whitespace.`,
3940
3953
  url: ruleDocumentationUrl("@/rules/attr-delimiter.ts"),
3941
3954
  };
3942
3955
  }
@@ -3953,7 +3966,7 @@ class AttrDelimiter extends Rule {
3953
3966
  }
3954
3967
 
3955
3968
  const DEFAULT_PATTERN = "[a-z0-9-:]+";
3956
- const defaults$n = {
3969
+ const defaults$o = {
3957
3970
  pattern: DEFAULT_PATTERN,
3958
3971
  ignoreForeign: true,
3959
3972
  };
@@ -3990,7 +4003,7 @@ function generateDescription(name, pattern) {
3990
4003
  }
3991
4004
  class AttrPattern extends Rule {
3992
4005
  constructor(options) {
3993
- super({ ...defaults$n, ...options });
4006
+ super({ ...defaults$o, ...options });
3994
4007
  this.pattern = generateRegexp(this.options.pattern);
3995
4008
  }
3996
4009
  static schema() {
@@ -4050,13 +4063,13 @@ var QuoteStyle;
4050
4063
  QuoteStyle["DOUBLE_QUOTE"] = "\"";
4051
4064
  QuoteStyle["AUTO_QUOTE"] = "auto";
4052
4065
  })(QuoteStyle || (QuoteStyle = {}));
4053
- const defaults$m = {
4066
+ const defaults$n = {
4054
4067
  style: "auto",
4055
4068
  unquoted: false,
4056
4069
  };
4057
4070
  class AttrQuotes extends Rule {
4058
4071
  constructor(options) {
4059
- super({ ...defaults$m, ...options });
4072
+ super({ ...defaults$n, ...options });
4060
4073
  this.style = parseStyle$4(this.options.style);
4061
4074
  }
4062
4075
  static schema() {
@@ -4221,12 +4234,12 @@ class AttributeAllowedValues extends Rule {
4221
4234
  }
4222
4235
  }
4223
4236
 
4224
- const defaults$l = {
4237
+ const defaults$m = {
4225
4238
  style: "omit",
4226
4239
  };
4227
4240
  class AttributeBooleanStyle extends Rule {
4228
4241
  constructor(options) {
4229
- super({ ...defaults$l, ...options });
4242
+ super({ ...defaults$m, ...options });
4230
4243
  this.hasInvalidStyle = parseStyle$3(this.options.style);
4231
4244
  }
4232
4245
  static schema() {
@@ -4302,12 +4315,12 @@ function reportMessage$1(attr, style) {
4302
4315
  return "";
4303
4316
  }
4304
4317
 
4305
- const defaults$k = {
4318
+ const defaults$l = {
4306
4319
  style: "omit",
4307
4320
  };
4308
4321
  class AttributeEmptyStyle extends Rule {
4309
4322
  constructor(options) {
4310
- super({ ...defaults$k, ...options });
4323
+ super({ ...defaults$l, ...options });
4311
4324
  this.hasInvalidStyle = parseStyle$2(this.options.style);
4312
4325
  }
4313
4326
  static schema() {
@@ -4415,12 +4428,12 @@ function describePattern(pattern) {
4415
4428
  }
4416
4429
  }
4417
4430
 
4418
- const defaults$j = {
4431
+ const defaults$k = {
4419
4432
  pattern: "kebabcase",
4420
4433
  };
4421
4434
  class ClassPattern extends Rule {
4422
4435
  constructor(options) {
4423
- super({ ...defaults$j, ...options });
4436
+ super({ ...defaults$k, ...options });
4424
4437
  this.pattern = parsePattern(this.options.pattern);
4425
4438
  }
4426
4439
  static schema() {
@@ -4446,7 +4459,9 @@ class ClassPattern extends Rule {
4446
4459
  classes.forEach((cur, index) => {
4447
4460
  if (!cur.match(this.pattern)) {
4448
4461
  const location = classes.location(index);
4449
- this.report(event.target, `Class "${cur}" does not match required pattern "${this.pattern}"`, location);
4462
+ const pattern = this.pattern.toString();
4463
+ const message = `Class "${cur}" does not match required pattern "${pattern}"`;
4464
+ this.report(event.target, message, location);
4450
4465
  }
4451
4466
  });
4452
4467
  });
@@ -4527,13 +4542,13 @@ class CloseOrder extends Rule {
4527
4542
  }
4528
4543
  }
4529
4544
 
4530
- const defaults$i = {
4545
+ const defaults$j = {
4531
4546
  include: null,
4532
4547
  exclude: null,
4533
4548
  };
4534
4549
  class Deprecated extends Rule {
4535
4550
  constructor(options) {
4536
- super({ ...defaults$i, ...options });
4551
+ super({ ...defaults$j, ...options });
4537
4552
  }
4538
4553
  static schema() {
4539
4554
  return {
@@ -4696,12 +4711,12 @@ class NoStyleTag$1 extends Rule {
4696
4711
  }
4697
4712
  }
4698
4713
 
4699
- const defaults$h = {
4714
+ const defaults$i = {
4700
4715
  style: "uppercase",
4701
4716
  };
4702
4717
  class DoctypeStyle extends Rule {
4703
4718
  constructor(options) {
4704
- super({ ...defaults$h, ...options });
4719
+ super({ ...defaults$i, ...options });
4705
4720
  }
4706
4721
  static schema() {
4707
4722
  return {
@@ -4733,12 +4748,12 @@ class DoctypeStyle extends Rule {
4733
4748
  }
4734
4749
  }
4735
4750
 
4736
- const defaults$g = {
4751
+ const defaults$h = {
4737
4752
  style: "lowercase",
4738
4753
  };
4739
4754
  class ElementCase extends Rule {
4740
4755
  constructor(options) {
4741
- super({ ...defaults$g, ...options });
4756
+ super({ ...defaults$h, ...options });
4742
4757
  this.style = new CaseStyle(this.options.style, "element-case");
4743
4758
  }
4744
4759
  static schema() {
@@ -4762,8 +4777,11 @@ class ElementCase extends Rule {
4762
4777
  };
4763
4778
  }
4764
4779
  documentation() {
4780
+ const { style } = this.options;
4765
4781
  return {
4766
- description: `Element tagname must be ${this.options.style}.`,
4782
+ description: Array.isArray(style)
4783
+ ? [`Element tagname must be in one of:`, "", ...style.map((it) => `- ${it}`)].join("\n")
4784
+ : `Element tagname must be in ${style}.`,
4767
4785
  url: ruleDocumentationUrl("@/rules/element-case.ts"),
4768
4786
  };
4769
4787
  }
@@ -4801,14 +4819,14 @@ class ElementCase extends Rule {
4801
4819
  }
4802
4820
  }
4803
4821
 
4804
- const defaults$f = {
4822
+ const defaults$g = {
4805
4823
  pattern: "^[a-z][a-z0-9\\-._]*-[a-z0-9\\-._]*$",
4806
4824
  whitelist: [],
4807
4825
  blacklist: [],
4808
4826
  };
4809
4827
  class ElementName extends Rule {
4810
4828
  constructor(options) {
4811
- super({ ...defaults$f, ...options });
4829
+ super({ ...defaults$g, ...options });
4812
4830
  // eslint-disable-next-line security/detect-non-literal-regexp
4813
4831
  this.pattern = new RegExp(this.options.pattern);
4814
4832
  }
@@ -4849,7 +4867,7 @@ class ElementName extends Rule {
4849
4867
  ...context.blacklist.map((cur) => `- ${cur}`),
4850
4868
  ];
4851
4869
  }
4852
- if (context.pattern !== defaults$f.pattern) {
4870
+ if (context.pattern !== defaults$g.pattern) {
4853
4871
  return [
4854
4872
  `<${context.tagName}> is not a valid element name. This project is configured to only allow names matching the following regular expression:`,
4855
4873
  "",
@@ -5251,7 +5269,7 @@ class EmptyTitle extends Rule {
5251
5269
  }
5252
5270
  }
5253
5271
 
5254
- const defaults$e = {
5272
+ const defaults$f = {
5255
5273
  allowMultipleH1: false,
5256
5274
  minInitialRank: "h1",
5257
5275
  sectioningRoots: ["dialog", '[role="dialog"]'],
@@ -5282,7 +5300,7 @@ function parseMaxInitial(value) {
5282
5300
  }
5283
5301
  class HeadingLevel extends Rule {
5284
5302
  constructor(options) {
5285
- super({ ...defaults$e, ...options });
5303
+ super({ ...defaults$f, ...options });
5286
5304
  this.stack = [];
5287
5305
  this.minInitialRank = parseMaxInitial(this.options.minInitialRank);
5288
5306
  this.sectionRoots = this.options.sectioningRoots.map((it) => new Pattern(it));
@@ -5440,12 +5458,12 @@ class HeadingLevel extends Rule {
5440
5458
  }
5441
5459
  }
5442
5460
 
5443
- const defaults$d = {
5461
+ const defaults$e = {
5444
5462
  pattern: "kebabcase",
5445
5463
  };
5446
5464
  class IdPattern extends Rule {
5447
5465
  constructor(options) {
5448
- super({ ...defaults$d, ...options });
5466
+ super({ ...defaults$e, ...options });
5449
5467
  this.pattern = parsePattern(this.options.pattern);
5450
5468
  }
5451
5469
  static schema() {
@@ -5464,6 +5482,7 @@ class IdPattern extends Rule {
5464
5482
  }
5465
5483
  setup() {
5466
5484
  this.on("attr", (event) => {
5485
+ var _a;
5467
5486
  if (event.key.toLowerCase() !== "id") {
5468
5487
  return;
5469
5488
  }
@@ -5472,7 +5491,10 @@ class IdPattern extends Rule {
5472
5491
  return;
5473
5492
  }
5474
5493
  if (!event.value || !event.value.match(this.pattern)) {
5475
- this.report(event.target, `ID "${event.value}" does not match required pattern "${this.pattern}"`, event.valueLocation);
5494
+ const value = (_a = event.value) !== null && _a !== void 0 ? _a : "";
5495
+ const pattern = this.pattern.toString();
5496
+ const message = `ID "${value}" does not match required pattern "${pattern}"`;
5497
+ this.report(event.target, message, event.valueLocation);
5476
5498
  }
5477
5499
  });
5478
5500
  }
@@ -5792,12 +5814,12 @@ function findLabelByParent(el) {
5792
5814
  return [];
5793
5815
  }
5794
5816
 
5795
- const defaults$c = {
5817
+ const defaults$d = {
5796
5818
  maxlength: 70,
5797
5819
  };
5798
5820
  class LongTitle extends Rule {
5799
5821
  constructor(options) {
5800
- super({ ...defaults$c, ...options });
5822
+ super({ ...defaults$d, ...options });
5801
5823
  this.maxlength = this.options.maxlength;
5802
5824
  }
5803
5825
  static schema() {
@@ -5944,13 +5966,13 @@ class MultipleLabeledControls extends Rule {
5944
5966
  }
5945
5967
  }
5946
5968
 
5947
- const defaults$b = {
5969
+ const defaults$c = {
5948
5970
  include: null,
5949
5971
  exclude: null,
5950
5972
  };
5951
5973
  class NoAutoplay extends Rule {
5952
5974
  constructor(options) {
5953
- super({ ...defaults$b, ...options });
5975
+ super({ ...defaults$c, ...options });
5954
5976
  }
5955
5977
  documentation(context) {
5956
5978
  const tagName = context ? ` on <${context.tagName}>` : "";
@@ -6191,7 +6213,7 @@ Omitted end tags can be ambigious for humans to read and many editors have troub
6191
6213
  }
6192
6214
  }
6193
6215
 
6194
- const defaults$a = {
6216
+ const defaults$b = {
6195
6217
  include: null,
6196
6218
  exclude: null,
6197
6219
  allowedProperties: ["display"],
@@ -6208,7 +6230,7 @@ function getCSSDeclarations(value) {
6208
6230
  }
6209
6231
  class NoInlineStyle extends Rule {
6210
6232
  constructor(options) {
6211
- super({ ...defaults$a, ...options });
6233
+ super({ ...defaults$b, ...options });
6212
6234
  }
6213
6235
  static schema() {
6214
6236
  return {
@@ -6413,24 +6435,24 @@ class NoMultipleMain extends Rule {
6413
6435
  }
6414
6436
  }
6415
6437
 
6416
- const defaults$9 = {
6438
+ const defaults$a = {
6417
6439
  relaxed: false,
6418
6440
  };
6419
6441
  const textRegexp = /([<>]|&(?![a-zA-Z0-9#]+;))/g;
6420
6442
  const unquotedAttrRegexp = /([<>"'=`]|&(?![a-zA-Z0-9#]+;))/g;
6421
6443
  const matchTemplate = /^(<%.*?%>|<\?.*?\?>|<\$.*?\$>)$/s;
6422
- const replacementTable = new Map([
6423
- ['"', "&quot;"],
6424
- ["&", "&amp;"],
6425
- ["'", "&apos;"],
6426
- ["<", "&lt;"],
6427
- ["=", "&equals;"],
6428
- [">", "&gt;"],
6429
- ["`", "&grave;"],
6430
- ]);
6444
+ const replacementTable = {
6445
+ '"': "&quot;",
6446
+ "&": "&amp;",
6447
+ "'": "&apos;",
6448
+ "<": "&lt;",
6449
+ "=": "&equals;",
6450
+ ">": "&gt;",
6451
+ "`": "&grave;",
6452
+ };
6431
6453
  class NoRawCharacters extends Rule {
6432
6454
  constructor(options) {
6433
- super({ ...defaults$9, ...options });
6455
+ super({ ...defaults$a, ...options });
6434
6456
  this.relaxed = this.options.relaxed;
6435
6457
  }
6436
6458
  static schema() {
@@ -6480,7 +6502,6 @@ class NoRawCharacters extends Rule {
6480
6502
  * @param text - The full text to find unescaped raw characters in.
6481
6503
  * @param location - Location of text.
6482
6504
  * @param regexp - Regexp pattern to match using.
6483
- * @param ignore - List of characters to ignore for this text.
6484
6505
  */
6485
6506
  findRawChars(node, text, location, regexp) {
6486
6507
  let match;
@@ -6496,7 +6517,7 @@ class NoRawCharacters extends Rule {
6496
6517
  continue;
6497
6518
  }
6498
6519
  /* determine replacement character and location */
6499
- const replacement = replacementTable.get(char);
6520
+ const replacement = replacementTable[char];
6500
6521
  const charLocation = sliceLocation(location, match.index, match.index + 1);
6501
6522
  /* report as error */
6502
6523
  this.report(node, `Raw "${char}" must be encoded as "${replacement}"`, charLocation);
@@ -6609,13 +6630,13 @@ class NoRedundantRole extends Rule {
6609
6630
  }
6610
6631
 
6611
6632
  const xmlns = /^(.+):.+$/;
6612
- const defaults$8 = {
6633
+ const defaults$9 = {
6613
6634
  ignoreForeign: true,
6614
6635
  ignoreXML: true,
6615
6636
  };
6616
6637
  class NoSelfClosing extends Rule {
6617
6638
  constructor(options) {
6618
- super({ ...defaults$8, ...options });
6639
+ super({ ...defaults$9, ...options });
6619
6640
  }
6620
6641
  static schema() {
6621
6642
  return {
@@ -6748,13 +6769,13 @@ const replacement = {
6748
6769
  reset: '<button type="reset">',
6749
6770
  image: '<button type="button">',
6750
6771
  };
6751
- const defaults$7 = {
6772
+ const defaults$8 = {
6752
6773
  include: null,
6753
6774
  exclude: null,
6754
6775
  };
6755
6776
  class PreferButton extends Rule {
6756
6777
  constructor(options) {
6757
- super({ ...defaults$7, ...options });
6778
+ super({ ...defaults$8, ...options });
6758
6779
  }
6759
6780
  static schema() {
6760
6781
  return {
@@ -6829,7 +6850,7 @@ class PreferButton extends Rule {
6829
6850
  }
6830
6851
  }
6831
6852
 
6832
- const defaults$6 = {
6853
+ const defaults$7 = {
6833
6854
  mapping: {
6834
6855
  article: "article",
6835
6856
  banner: "header",
@@ -6859,7 +6880,7 @@ const defaults$6 = {
6859
6880
  };
6860
6881
  class PreferNativeElement extends Rule {
6861
6882
  constructor(options) {
6862
- super({ ...defaults$6, ...options });
6883
+ super({ ...defaults$7, ...options });
6863
6884
  }
6864
6885
  static schema() {
6865
6886
  return {
@@ -6979,7 +7000,7 @@ class PreferTbody extends Rule {
6979
7000
  }
6980
7001
  }
6981
7002
 
6982
- const defaults$5 = {
7003
+ const defaults$6 = {
6983
7004
  target: "all",
6984
7005
  };
6985
7006
  const crossorigin = new RegExp("^(\\w+://|//)"); /* e.g. https:// or // */
@@ -6989,7 +7010,7 @@ const supportSri = {
6989
7010
  };
6990
7011
  class RequireSri extends Rule {
6991
7012
  constructor(options) {
6992
- super({ ...defaults$5, ...options });
7013
+ super({ ...defaults$6, ...options });
6993
7014
  this.target = this.options.target;
6994
7015
  }
6995
7016
  static schema() {
@@ -7117,6 +7138,155 @@ class SvgFocusable extends Rule {
7117
7138
  }
7118
7139
  }
7119
7140
 
7141
+ const defaults$5 = {
7142
+ characters: [
7143
+ { pattern: " ", replacement: "&nbsp;", description: "non-breaking space" },
7144
+ { pattern: "-", replacement: "&#8209;", description: "non-breaking hyphen" },
7145
+ ],
7146
+ ignoreClasses: [],
7147
+ };
7148
+ function constructRegex(characters) {
7149
+ const disallowed = characters
7150
+ .map((it) => {
7151
+ return it.pattern;
7152
+ })
7153
+ .join("|");
7154
+ const pattern = `(${disallowed})`;
7155
+ /* eslint-disable-next-line security/detect-non-literal-regexp */
7156
+ return new RegExp(pattern, "g");
7157
+ }
7158
+ function getText(node) {
7159
+ /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */
7160
+ const match = node.textContent.match(/^(\s*)(.*)$/);
7161
+ const [, leading, text] = match;
7162
+ return [leading.length, text.trimEnd()];
7163
+ }
7164
+ /**
7165
+ * Node 12 does not support String.matchAll, this simulates it's behavior.
7166
+ */
7167
+ function matchAll(text, regexp) {
7168
+ /* eslint-disable-next-line security/detect-non-literal-regexp */
7169
+ const copy = new RegExp(regexp);
7170
+ const matches = [];
7171
+ /* eslint-disable-next-line no-constant-condition */
7172
+ while (true) {
7173
+ const match = copy.exec(text);
7174
+ if (match === null) {
7175
+ break;
7176
+ }
7177
+ matches.push(match);
7178
+ }
7179
+ return matches;
7180
+ }
7181
+ class TelNonBreaking extends Rule {
7182
+ constructor(options) {
7183
+ super({ ...defaults$5, ...options });
7184
+ this.regex = constructRegex(this.options.characters);
7185
+ }
7186
+ static schema() {
7187
+ return {
7188
+ characters: {
7189
+ type: "array",
7190
+ items: {
7191
+ type: "object",
7192
+ additionalProperties: false,
7193
+ properties: {
7194
+ pattern: {
7195
+ type: "string",
7196
+ },
7197
+ replacement: {
7198
+ type: "string",
7199
+ },
7200
+ description: {
7201
+ type: "string",
7202
+ },
7203
+ },
7204
+ },
7205
+ },
7206
+ ignoreClasses: {
7207
+ type: "array",
7208
+ items: {
7209
+ type: "string",
7210
+ },
7211
+ },
7212
+ };
7213
+ }
7214
+ documentation(context) {
7215
+ const { characters } = this.options;
7216
+ const replacements = characters.map((it) => {
7217
+ return ` - \`${it.pattern}\` - replace with \`${it.replacement}\` (${it.description}).`;
7218
+ });
7219
+ return {
7220
+ description: [
7221
+ context
7222
+ ? `The \`${context.pattern}\` character should be replaced with \`${context.replacement}\` character (${context.description}) when used in a telephone number.`
7223
+ : `Replace this character with a non-breaking version.`,
7224
+ "",
7225
+ "Unless non-breaking characters is used there could be a line break inserted at that character.",
7226
+ "Line breaks make is harder to read and understand the telephone number.",
7227
+ "",
7228
+ "The following characters should be avoided:",
7229
+ "",
7230
+ ...replacements,
7231
+ ].join("\n"),
7232
+ url: ruleDocumentationUrl("@/rules/tel-non-breaking.ts"),
7233
+ };
7234
+ }
7235
+ setup() {
7236
+ this.on("element:ready", this.isRelevant, (event) => {
7237
+ const { target } = event;
7238
+ const { ignoreClasses } = this.options;
7239
+ /* skip if element has a class in the ignore list */
7240
+ const isIgnored = ignoreClasses.some((it) => target.classList.contains(it));
7241
+ if (isIgnored) {
7242
+ return;
7243
+ }
7244
+ this.walk(target);
7245
+ });
7246
+ }
7247
+ isRelevant(event) {
7248
+ const { target } = event;
7249
+ /* should only deal with anchors */
7250
+ if (!target.is("a")) {
7251
+ return false;
7252
+ }
7253
+ /* ignore if anchor does not have tel href */
7254
+ const attr = target.getAttribute("href");
7255
+ if (!attr || !attr.valueMatches(/^tel:/, false)) {
7256
+ return false;
7257
+ }
7258
+ return true;
7259
+ }
7260
+ walk(node) {
7261
+ for (const child of node.childNodes) {
7262
+ if (isTextNode(child)) {
7263
+ this.detectDisallowed(child);
7264
+ }
7265
+ else if (isElementNode(child)) {
7266
+ this.walk(child);
7267
+ }
7268
+ }
7269
+ }
7270
+ detectDisallowed(node) {
7271
+ const [offset, text] = getText(node);
7272
+ const matches = matchAll(text, this.regex);
7273
+ for (const match of matches) {
7274
+ const detected = match[0];
7275
+ const entry = this.options.characters.find((it) => it.pattern === detected);
7276
+ /* istanbul ignore next: should never happen and cannot be tested, just a sanity check */
7277
+ if (!entry) {
7278
+ throw new Error(`Failed to find entry for "${detected}" when searching text "${text}"`);
7279
+ }
7280
+ const message = `"${detected}" should be replaced with "${entry.replacement}" in telephone number`;
7281
+ const begin = offset + match.index;
7282
+ const end = begin + detected.length;
7283
+ const location = sliceLocation(node.location, begin, end);
7284
+ const context = entry;
7285
+ this.report(node, message, location, context);
7286
+ }
7287
+ }
7288
+ }
7289
+
7120
7290
  function hasAltText(image) {
7121
7291
  const alt = image.getAttribute("alt");
7122
7292
  /* missing or boolean */
@@ -9147,9 +9317,6 @@ function parseStyle$1(name) {
9147
9317
  case "selfclose":
9148
9318
  case "selfclosing":
9149
9319
  return Style$1.AlwaysSelfclose;
9150
- /* istanbul ignore next: covered by schema validation */
9151
- default:
9152
- throw new Error(`Invalid style "${name}" for "void" rule`);
9153
9320
  }
9154
9321
  }
9155
9322
 
@@ -9581,6 +9748,7 @@ const bundledRules = {
9581
9748
  "script-element": ScriptElement,
9582
9749
  "script-type": ScriptType,
9583
9750
  "svg-focusable": SvgFocusable,
9751
+ "tel-non-breaking": TelNonBreaking,
9584
9752
  "text-content": TextContent,
9585
9753
  "unrecognized-char-ref": UnknownCharReference,
9586
9754
  void: Void,
@@ -9605,7 +9773,7 @@ const config$3 = {
9605
9773
  "no-redundant-for": "error",
9606
9774
  "no-redundant-role": "error",
9607
9775
  "prefer-native-element": "error",
9608
- "svg-focusable": "error",
9776
+ "svg-focusable": "off",
9609
9777
  "text-content": "error",
9610
9778
  "wcag/h30": "error",
9611
9779
  "wcag/h32": "error",
@@ -9676,7 +9844,8 @@ const config$1 = {
9676
9844
  "prefer-tbody": "error",
9677
9845
  "script-element": "error",
9678
9846
  "script-type": "error",
9679
- "svg-focusable": "error",
9847
+ "svg-focusable": "off",
9848
+ "tel-non-breaking": "error",
9680
9849
  "text-content": "error",
9681
9850
  "unrecognized-char-ref": "error",
9682
9851
  void: "off",
@@ -9782,7 +9951,8 @@ class ResolvedConfig {
9782
9951
  });
9783
9952
  }
9784
9953
  catch (err) {
9785
- throw new NestedError(`When transforming "${source.filename}": ${err.message}`, err);
9954
+ const message = err instanceof Error ? err.message : String(err);
9955
+ throw new NestedError(`When transforming "${source.filename}": ${message}`, err);
9786
9956
  }
9787
9957
  }
9788
9958
  else {
@@ -9858,9 +10028,10 @@ function loadFromFile(filename) {
9858
10028
  }
9859
10029
  /* expand any relative paths */
9860
10030
  for (const key of ["extends", "elements", "plugins"]) {
9861
- if (!json[key])
10031
+ const value = json[key];
10032
+ if (!value)
9862
10033
  continue;
9863
- json[key] = json[key].map((ref) => {
10034
+ json[key] = value.map((ref) => {
9864
10035
  return Config.expandRelative(ref, path.dirname(filename));
9865
10036
  });
9866
10037
  }
@@ -10018,6 +10189,7 @@ class Config {
10018
10189
  /**
10019
10190
  * Get element metadata.
10020
10191
  */
10192
+ /* eslint-disable-next-line complexity, sonarjs/cognitive-complexity */
10021
10193
  getMetaTable() {
10022
10194
  /* use cached table if it exists */
10023
10195
  if (this.metaTable) {
@@ -10056,7 +10228,8 @@ class Config {
10056
10228
  metaTable.loadFromObject(legacyRequire(entry));
10057
10229
  }
10058
10230
  catch (err) {
10059
- throw new ConfigError(`Failed to load elements from "${entry}": ${err.message}`, err);
10231
+ const message = err instanceof Error ? err.message : String(err);
10232
+ throw new ConfigError(`Failed to load elements from "${entry}": ${message}`, err);
10060
10233
  }
10061
10234
  }
10062
10235
  metaTable.init();
@@ -10132,7 +10305,8 @@ class Config {
10132
10305
  return plugin;
10133
10306
  }
10134
10307
  catch (err) {
10135
- throw new ConfigError(`Failed to load plugin "${moduleName}": ${err}`, err);
10308
+ const message = err instanceof Error ? err.message : String(err);
10309
+ throw new ConfigError(`Failed to load plugin "${moduleName}": ${message}`, err);
10136
10310
  }
10137
10311
  });
10138
10312
  }
@@ -10454,6 +10628,9 @@ class ParserError extends Error {
10454
10628
  function isAttrValueToken(token) {
10455
10629
  return Boolean(token && token.type === TokenType.ATTR_VALUE);
10456
10630
  }
10631
+ function svgShouldRetainTag(foreignTagName, tagName) {
10632
+ return foreignTagName === "svg" && ["title", "desc"].includes(tagName);
10633
+ }
10457
10634
  /**
10458
10635
  * Parse HTML document into a DOM tree.
10459
10636
  *
@@ -10466,6 +10643,7 @@ class Parser {
10466
10643
  * @param config - Configuration
10467
10644
  */
10468
10645
  constructor(config) {
10646
+ this.currentNamespace = "";
10469
10647
  this.event = new EventHandler();
10470
10648
  this.dom = null;
10471
10649
  this.metaTable = config.getMetaTable();
@@ -10476,7 +10654,6 @@ class Parser {
10476
10654
  * @param source - HTML markup.
10477
10655
  * @returns DOM tree representing the HTML markup.
10478
10656
  */
10479
- // eslint-disable-next-line complexity
10480
10657
  parseHtml(source) {
10481
10658
  var _a, _b, _c, _d;
10482
10659
  if (typeof source === "string") {
@@ -10507,40 +10684,7 @@ class Parser {
10507
10684
  let it = this.next(tokenStream);
10508
10685
  while (!it.done) {
10509
10686
  const token = it.value;
10510
- switch (token.type) {
10511
- case TokenType.UNICODE_BOM:
10512
- /* ignore */
10513
- break;
10514
- case TokenType.TAG_OPEN:
10515
- this.consumeTag(source, token, tokenStream);
10516
- break;
10517
- case TokenType.WHITESPACE:
10518
- this.trigger("whitespace", {
10519
- text: token.data[0],
10520
- location: token.location,
10521
- });
10522
- this.appendText(token.data[0], token.location);
10523
- break;
10524
- case TokenType.DIRECTIVE:
10525
- this.consumeDirective(token);
10526
- break;
10527
- case TokenType.CONDITIONAL:
10528
- this.consumeConditional(token);
10529
- break;
10530
- case TokenType.COMMENT:
10531
- this.consumeComment(token);
10532
- break;
10533
- case TokenType.DOCTYPE_OPEN:
10534
- this.consumeDoctype(token, tokenStream);
10535
- break;
10536
- case TokenType.TEXT:
10537
- case TokenType.TEMPLATING:
10538
- this.appendText(token.data[0], token.location);
10539
- break;
10540
- case TokenType.EOF:
10541
- this.closeTree(source, token.location);
10542
- break;
10543
- }
10687
+ this.consume(source, token, tokenStream);
10544
10688
  it = this.next(tokenStream);
10545
10689
  }
10546
10690
  /* resolve any dynamic meta element properties */
@@ -10587,13 +10731,50 @@ class Parser {
10587
10731
  return Boolean(active.parent && active.parent.is(tagName) && meta.includes(active.tagName));
10588
10732
  }
10589
10733
  }
10734
+ /* eslint-disable-next-line complexity */
10735
+ consume(source, token, tokenStream) {
10736
+ switch (token.type) {
10737
+ case TokenType.UNICODE_BOM:
10738
+ /* ignore */
10739
+ break;
10740
+ case TokenType.TAG_OPEN:
10741
+ this.consumeTag(source, token, tokenStream);
10742
+ break;
10743
+ case TokenType.WHITESPACE:
10744
+ this.trigger("whitespace", {
10745
+ text: token.data[0],
10746
+ location: token.location,
10747
+ });
10748
+ this.appendText(token.data[0], token.location);
10749
+ break;
10750
+ case TokenType.DIRECTIVE:
10751
+ this.consumeDirective(token);
10752
+ break;
10753
+ case TokenType.CONDITIONAL:
10754
+ this.consumeConditional(token);
10755
+ break;
10756
+ case TokenType.COMMENT:
10757
+ this.consumeComment(token);
10758
+ break;
10759
+ case TokenType.DOCTYPE_OPEN:
10760
+ this.consumeDoctype(token, tokenStream);
10761
+ break;
10762
+ case TokenType.TEXT:
10763
+ case TokenType.TEMPLATING:
10764
+ this.appendText(token.data[0], token.location);
10765
+ break;
10766
+ case TokenType.EOF:
10767
+ this.closeTree(source, token.location);
10768
+ break;
10769
+ }
10770
+ }
10590
10771
  /* eslint-disable-next-line complexity, sonarjs/cognitive-complexity */
10591
10772
  consumeTag(source, startToken, tokenStream) {
10592
10773
  const tokens = Array.from(this.consumeUntil(tokenStream, TokenType.TAG_CLOSE, startToken.location));
10593
10774
  const endToken = tokens.slice(-1)[0];
10594
10775
  const closeOptional = this.closeOptional(startToken);
10595
10776
  const parent = closeOptional ? this.dom.getActive().parent : this.dom.getActive();
10596
- const node = HtmlElement.fromTokens(startToken, endToken, parent, this.metaTable);
10777
+ const node = HtmlElement.fromTokens(startToken, endToken, parent, this.metaTable, this.currentNamespace);
10597
10778
  const isStartTag = !startToken.data[1];
10598
10779
  const isClosing = !isStartTag || node.closed !== NodeClosed.Open;
10599
10780
  const isForeign = node.meta && node.meta.foreign;
@@ -10698,6 +10879,15 @@ class Parser {
10698
10879
  const tokens = Array.from(this.consumeUntil(tokenStream, TokenType.TAG_OPEN, errorLocation));
10699
10880
  const [last] = tokens.slice(-1);
10700
10881
  const [, tagClosed, tagName] = last.data;
10882
+ /* special case: svg <title> and <desc> should be intact as it affects accessibility */
10883
+ if (!tagClosed && svgShouldRetainTag(foreignTagName, tagName)) {
10884
+ const oldNamespace = this.currentNamespace;
10885
+ this.currentNamespace = "svg";
10886
+ this.consumeTag(source, last, tokenStream);
10887
+ this.consumeUntilMatchingTag(source, tokenStream, tagName);
10888
+ this.currentNamespace = oldNamespace;
10889
+ continue;
10890
+ }
10701
10891
  /* keep going unless the new tag matches the foreign root element */
10702
10892
  if (tagName !== foreignTagName) {
10703
10893
  continue;
@@ -10896,6 +11086,35 @@ class Parser {
10896
11086
  }
10897
11087
  throw new ParserError(errorLocation, `stream ended before ${TokenType[search]} token was found`);
10898
11088
  }
11089
+ /**
11090
+ * Consumes tokens until a matching close-tag is found. Tags are appended to
11091
+ * the document.
11092
+ *
11093
+ * @internal
11094
+ */
11095
+ consumeUntilMatchingTag(source, tokenStream, searchTag) {
11096
+ let numOpen = 1;
11097
+ let it = this.next(tokenStream);
11098
+ while (!it.done) {
11099
+ const token = it.value;
11100
+ this.consume(source, token, tokenStream);
11101
+ if (token.type === TokenType.TAG_OPEN) {
11102
+ const [, close, tagName] = token.data;
11103
+ if (tagName === searchTag) {
11104
+ if (close) {
11105
+ numOpen--;
11106
+ }
11107
+ else {
11108
+ numOpen++;
11109
+ }
11110
+ if (numOpen === 0) {
11111
+ return;
11112
+ }
11113
+ }
11114
+ }
11115
+ it = this.next(tokenStream);
11116
+ }
11117
+ }
10899
11118
  next(tokenStream) {
10900
11119
  const it = tokenStream.next();
10901
11120
  if (!it.done) {
@@ -11091,7 +11310,7 @@ class Engine {
11091
11310
  /**
11092
11311
  * Lint sources and return report
11093
11312
  *
11094
- * @param src - Parsed source.
11313
+ * @param sources - Sources to lint.
11095
11314
  * @returns Report output.
11096
11315
  */
11097
11316
  lint(sources) {
@@ -11185,7 +11404,7 @@ class Engine {
11185
11404
  const lines = [];
11186
11405
  function decoration(node) {
11187
11406
  let output = "";
11188
- if (node.hasAttribute("id")) {
11407
+ if (node.id) {
11189
11408
  output += `#${node.id}`;
11190
11409
  }
11191
11410
  if (node.hasAttribute("class")) {