html-validate 7.8.0 → 7.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cjs/core.js CHANGED
@@ -7,10 +7,10 @@ var Ajv = require('ajv');
7
7
  var deepmerge = require('deepmerge');
8
8
  var espree = require('espree');
9
9
  var walk = require('acorn-walk');
10
+ var elements = require('./elements.js');
10
11
  var path = require('path');
11
12
  var semver = require('semver');
12
13
  var kleur = require('kleur');
13
- var elements = require('./elements.js');
14
14
  var codeFrame = require('@babel/code-frame');
15
15
  var stylishImpl = require('@html-validate/stylish');
16
16
 
@@ -3565,7 +3565,7 @@ class Rule {
3565
3565
  }
3566
3566
  }
3567
3567
 
3568
- const defaults$t = {
3568
+ const defaults$u = {
3569
3569
  allowExternal: true,
3570
3570
  allowRelative: true,
3571
3571
  allowAbsolute: true,
@@ -3609,7 +3609,7 @@ function matchList(value, list) {
3609
3609
  }
3610
3610
  class AllowedLinks extends Rule {
3611
3611
  constructor(options) {
3612
- super({ ...defaults$t, ...options });
3612
+ super({ ...defaults$u, ...options });
3613
3613
  this.allowExternal = parseAllow(this.options.allowExternal);
3614
3614
  this.allowRelative = parseAllow(this.options.allowRelative);
3615
3615
  this.allowAbsolute = parseAllow(this.options.allowAbsolute);
@@ -3757,7 +3757,7 @@ var RuleContext$1;
3757
3757
  RuleContext["MISSING_ALT"] = "missing-alt";
3758
3758
  RuleContext["MISSING_HREF"] = "missing-href";
3759
3759
  })(RuleContext$1 || (RuleContext$1 = {}));
3760
- const defaults$s = {
3760
+ const defaults$t = {
3761
3761
  accessible: true,
3762
3762
  };
3763
3763
  function findByTarget(target, siblings) {
@@ -3766,7 +3766,7 @@ function findByTarget(target, siblings) {
3766
3766
  function getAltText(node) {
3767
3767
  return node.getAttributeValue("alt");
3768
3768
  }
3769
- function getDescription(context) {
3769
+ function getDescription$1(context) {
3770
3770
  switch (context) {
3771
3771
  case RuleContext$1.MISSING_ALT:
3772
3772
  return [
@@ -3795,7 +3795,7 @@ function getDescription(context) {
3795
3795
  }
3796
3796
  class AreaAlt extends Rule {
3797
3797
  constructor(options) {
3798
- super({ ...defaults$s, ...options });
3798
+ super({ ...defaults$t, ...options });
3799
3799
  }
3800
3800
  static schema() {
3801
3801
  return {
@@ -3806,7 +3806,7 @@ class AreaAlt extends Rule {
3806
3806
  }
3807
3807
  documentation(context) {
3808
3808
  return {
3809
- description: getDescription(context).join("\n"),
3809
+ description: getDescription$1(context).join("\n"),
3810
3810
  url: "https://html-validate.org/rules/area-alt.html",
3811
3811
  };
3812
3812
  }
@@ -3959,13 +3959,13 @@ class AriaLabelMisuse extends Rule {
3959
3959
  class ConfigError extends UserError {
3960
3960
  }
3961
3961
 
3962
- const defaults$r = {
3962
+ const defaults$s = {
3963
3963
  style: "lowercase",
3964
3964
  ignoreForeign: true,
3965
3965
  };
3966
3966
  class AttrCase extends Rule {
3967
3967
  constructor(options) {
3968
- super({ ...defaults$r, ...options });
3968
+ super({ ...defaults$s, ...options });
3969
3969
  this.style = new rulesHelper.CaseStyle(this.options.style, "attr-case");
3970
3970
  }
3971
3971
  static schema() {
@@ -4310,7 +4310,7 @@ class AttrDelimiter extends Rule {
4310
4310
  }
4311
4311
 
4312
4312
  const DEFAULT_PATTERN = "[a-z0-9-:]+";
4313
- const defaults$q = {
4313
+ const defaults$r = {
4314
4314
  pattern: DEFAULT_PATTERN,
4315
4315
  ignoreForeign: true,
4316
4316
  };
@@ -4347,7 +4347,7 @@ function generateDescription(name, pattern) {
4347
4347
  }
4348
4348
  class AttrPattern extends Rule {
4349
4349
  constructor(options) {
4350
- super({ ...defaults$q, ...options });
4350
+ super({ ...defaults$r, ...options });
4351
4351
  this.pattern = generateRegexp(this.options.pattern);
4352
4352
  }
4353
4353
  static schema() {
@@ -4408,7 +4408,7 @@ var QuoteStyle;
4408
4408
  QuoteStyle["AUTO_QUOTE"] = "auto";
4409
4409
  QuoteStyle["ANY_QUOTE"] = "any";
4410
4410
  })(QuoteStyle || (QuoteStyle = {}));
4411
- const defaults$p = {
4411
+ const defaults$q = {
4412
4412
  style: "auto",
4413
4413
  unquoted: false,
4414
4414
  };
@@ -4448,10 +4448,6 @@ function describeStyle(style, unquoted) {
4448
4448
  return `${description.join(" or\n")}\n`;
4449
4449
  }
4450
4450
  class AttrQuotes extends Rule {
4451
- constructor(options) {
4452
- super({ ...defaults$p, ...options });
4453
- this.style = parseStyle$4(this.options.style);
4454
- }
4455
4451
  static schema() {
4456
4452
  return {
4457
4453
  style: {
@@ -4478,6 +4474,10 @@ class AttrQuotes extends Rule {
4478
4474
  url: "https://html-validate.org/rules/attr-quotes.html",
4479
4475
  };
4480
4476
  }
4477
+ constructor(options) {
4478
+ super({ ...defaults$q, ...options });
4479
+ this.style = parseStyle$4(this.options.style);
4480
+ }
4481
4481
  setup() {
4482
4482
  this.on("attr", (event) => {
4483
4483
  /* ignore attributes with no value */
@@ -4645,12 +4645,12 @@ class AttributeAllowedValues extends Rule {
4645
4645
  }
4646
4646
  }
4647
4647
 
4648
- const defaults$o = {
4648
+ const defaults$p = {
4649
4649
  style: "omit",
4650
4650
  };
4651
4651
  class AttributeBooleanStyle extends Rule {
4652
4652
  constructor(options) {
4653
- super({ ...defaults$o, ...options });
4653
+ super({ ...defaults$p, ...options });
4654
4654
  this.hasInvalidStyle = parseStyle$3(this.options.style);
4655
4655
  }
4656
4656
  static schema() {
@@ -4726,12 +4726,12 @@ function reportMessage$1(attr, style) {
4726
4726
  return "";
4727
4727
  }
4728
4728
 
4729
- const defaults$n = {
4729
+ const defaults$o = {
4730
4730
  style: "omit",
4731
4731
  };
4732
4732
  class AttributeEmptyStyle extends Rule {
4733
4733
  constructor(options) {
4734
- super({ ...defaults$n, ...options });
4734
+ super({ ...defaults$o, ...options });
4735
4735
  this.hasInvalidStyle = parseStyle$2(this.options.style);
4736
4736
  }
4737
4737
  static schema() {
@@ -4887,12 +4887,12 @@ function describePattern(pattern) {
4887
4887
  }
4888
4888
  }
4889
4889
 
4890
- const defaults$m = {
4890
+ const defaults$n = {
4891
4891
  pattern: "kebabcase",
4892
4892
  };
4893
4893
  class ClassPattern extends Rule {
4894
4894
  constructor(options) {
4895
- super({ ...defaults$m, ...options });
4895
+ super({ ...defaults$n, ...options });
4896
4896
  this.pattern = parsePattern(this.options.pattern);
4897
4897
  }
4898
4898
  static schema() {
@@ -5001,13 +5001,13 @@ class CloseOrder extends Rule {
5001
5001
  }
5002
5002
  }
5003
5003
 
5004
- const defaults$l = {
5004
+ const defaults$m = {
5005
5005
  include: null,
5006
5006
  exclude: null,
5007
5007
  };
5008
5008
  class Deprecated extends Rule {
5009
5009
  constructor(options) {
5010
- super({ ...defaults$l, ...options });
5010
+ super({ ...defaults$m, ...options });
5011
5011
  }
5012
5012
  static schema() {
5013
5013
  return {
@@ -5170,12 +5170,12 @@ let NoStyleTag$1 = class NoStyleTag extends Rule {
5170
5170
  }
5171
5171
  };
5172
5172
 
5173
- const defaults$k = {
5173
+ const defaults$l = {
5174
5174
  style: "uppercase",
5175
5175
  };
5176
5176
  class DoctypeStyle extends Rule {
5177
5177
  constructor(options) {
5178
- super({ ...defaults$k, ...options });
5178
+ super({ ...defaults$l, ...options });
5179
5179
  }
5180
5180
  static schema() {
5181
5181
  return {
@@ -5207,12 +5207,12 @@ class DoctypeStyle extends Rule {
5207
5207
  }
5208
5208
  }
5209
5209
 
5210
- const defaults$j = {
5210
+ const defaults$k = {
5211
5211
  style: "lowercase",
5212
5212
  };
5213
5213
  class ElementCase extends Rule {
5214
5214
  constructor(options) {
5215
- super({ ...defaults$j, ...options });
5215
+ super({ ...defaults$k, ...options });
5216
5216
  this.style = new rulesHelper.CaseStyle(this.options.style, "element-case");
5217
5217
  }
5218
5218
  static schema() {
@@ -5278,14 +5278,14 @@ class ElementCase extends Rule {
5278
5278
  }
5279
5279
  }
5280
5280
 
5281
- const defaults$i = {
5281
+ const defaults$j = {
5282
5282
  pattern: "^[a-z][a-z0-9\\-._]*-[a-z0-9\\-._]*$",
5283
5283
  whitelist: [],
5284
5284
  blacklist: [],
5285
5285
  };
5286
5286
  class ElementName extends Rule {
5287
5287
  constructor(options) {
5288
- super({ ...defaults$i, ...options });
5288
+ super({ ...defaults$j, ...options });
5289
5289
  // eslint-disable-next-line security/detect-non-literal-regexp
5290
5290
  this.pattern = new RegExp(this.options.pattern);
5291
5291
  }
@@ -5326,7 +5326,7 @@ class ElementName extends Rule {
5326
5326
  ...context.blacklist.map((cur) => `- ${cur}`),
5327
5327
  ];
5328
5328
  }
5329
- if (context.pattern !== defaults$i.pattern) {
5329
+ if (context.pattern !== defaults$j.pattern) {
5330
5330
  return [
5331
5331
  `<${context.tagName}> is not a valid element name. This project is configured to only allow names matching the following regular expression:`,
5332
5332
  "",
@@ -5799,9 +5799,9 @@ function hasImgAltText(node) {
5799
5799
  else if (node.is("svg")) {
5800
5800
  return node.textContent.trim() !== "";
5801
5801
  }
5802
- else {
5803
- return false;
5804
- }
5802
+ /* istanbul ignore next -- querySelector(..) is only going to return the two
5803
+ * above tags but this serves as a sane default if above assumption changes */
5804
+ return false;
5805
5805
  }
5806
5806
  class EmptyHeading extends Rule {
5807
5807
  documentation() {
@@ -5825,7 +5825,7 @@ class EmptyHeading extends Rule {
5825
5825
  return;
5826
5826
  }
5827
5827
  }
5828
- switch (rulesHelper.classifyNodeText(heading)) {
5828
+ switch (rulesHelper.classifyNodeText(heading, { ignoreHiddenRoot: true })) {
5829
5829
  case rulesHelper.TextClassification.DYNAMIC_TEXT:
5830
5830
  case rulesHelper.TextClassification.STATIC_TEXT:
5831
5831
  /* have some text content, consider ok */
@@ -5874,7 +5874,7 @@ class EmptyTitle extends Rule {
5874
5874
  }
5875
5875
  }
5876
5876
 
5877
- const defaults$h = {
5877
+ const defaults$i = {
5878
5878
  allowMultipleH1: false,
5879
5879
  minInitialRank: "h1",
5880
5880
  sectioningRoots: ["dialog", '[role="dialog"]'],
@@ -5905,7 +5905,7 @@ function parseMaxInitial(value) {
5905
5905
  }
5906
5906
  class HeadingLevel extends Rule {
5907
5907
  constructor(options) {
5908
- super({ ...defaults$h, ...options });
5908
+ super({ ...defaults$i, ...options });
5909
5909
  this.stack = [];
5910
5910
  this.minInitialRank = parseMaxInitial(this.options.minInitialRank);
5911
5911
  this.sectionRoots = this.options.sectioningRoots.map((it) => new Pattern(it));
@@ -6063,12 +6063,12 @@ class HeadingLevel extends Rule {
6063
6063
  }
6064
6064
  }
6065
6065
 
6066
- const defaults$g = {
6066
+ const defaults$h = {
6067
6067
  pattern: "kebabcase",
6068
6068
  };
6069
6069
  class IdPattern extends Rule {
6070
6070
  constructor(options) {
6071
- super({ ...defaults$g, ...options });
6071
+ super({ ...defaults$h, ...options });
6072
6072
  this.pattern = parsePattern(this.options.pattern);
6073
6073
  }
6074
6074
  static schema() {
@@ -6350,12 +6350,12 @@ function findLabelByParent(el) {
6350
6350
  return [];
6351
6351
  }
6352
6352
 
6353
- const defaults$f = {
6353
+ const defaults$g = {
6354
6354
  maxlength: 70,
6355
6355
  };
6356
6356
  class LongTitle extends Rule {
6357
6357
  constructor(options) {
6358
- super({ ...defaults$f, ...options });
6358
+ super({ ...defaults$g, ...options });
6359
6359
  this.maxlength = this.options.maxlength;
6360
6360
  }
6361
6361
  static schema() {
@@ -6438,6 +6438,49 @@ function parseContent(text) {
6438
6438
  }
6439
6439
  }
6440
6440
 
6441
+ function getName(attr) {
6442
+ const name = attr.value;
6443
+ if (!name || name instanceof DynamicValue) {
6444
+ return null;
6445
+ }
6446
+ return name;
6447
+ }
6448
+ class MapDupName extends Rule {
6449
+ documentation() {
6450
+ return {
6451
+ description: "`<map>` must have a unique name, it cannot be the same name as another `<map>` element",
6452
+ url: "https://html-validate.org/rules/map-dup-name.html",
6453
+ };
6454
+ }
6455
+ setup() {
6456
+ this.on("dom:ready", (event) => {
6457
+ const { document } = event;
6458
+ const maps = document.querySelectorAll("map[name]");
6459
+ const names = new Set();
6460
+ for (const map of maps) {
6461
+ const attr = map.getAttribute("name");
6462
+ /* istanbul ignore next -- should not happen as querySelector matches
6463
+ * only the elements with the name attribute */
6464
+ if (!attr) {
6465
+ continue;
6466
+ }
6467
+ const name = getName(attr);
6468
+ if (!name) {
6469
+ continue;
6470
+ }
6471
+ if (names.has(name)) {
6472
+ this.report({
6473
+ node: map,
6474
+ message: `<map> name must be unique`,
6475
+ location: attr.keyLocation,
6476
+ });
6477
+ }
6478
+ names.add(name);
6479
+ }
6480
+ });
6481
+ }
6482
+ }
6483
+
6441
6484
  class MissingDoctype extends Rule {
6442
6485
  documentation() {
6443
6486
  return {
@@ -6502,13 +6545,13 @@ class MultipleLabeledControls extends Rule {
6502
6545
  }
6503
6546
  }
6504
6547
 
6505
- const defaults$e = {
6548
+ const defaults$f = {
6506
6549
  include: null,
6507
6550
  exclude: null,
6508
6551
  };
6509
6552
  class NoAutoplay extends Rule {
6510
6553
  constructor(options) {
6511
- super({ ...defaults$e, ...options });
6554
+ super({ ...defaults$f, ...options });
6512
6555
  }
6513
6556
  documentation(context) {
6514
6557
  const tagName = context ? ` on <${context.tagName}>` : "";
@@ -6749,14 +6792,14 @@ Omitted end tags can be ambigious for humans to read and many editors have troub
6749
6792
  }
6750
6793
  }
6751
6794
 
6752
- const defaults$d = {
6795
+ const defaults$e = {
6753
6796
  include: null,
6754
6797
  exclude: null,
6755
6798
  allowedProperties: ["display"],
6756
6799
  };
6757
6800
  class NoInlineStyle extends Rule {
6758
6801
  constructor(options) {
6759
- super({ ...defaults$d, ...options });
6802
+ super({ ...defaults$e, ...options });
6760
6803
  }
6761
6804
  static schema() {
6762
6805
  return {
@@ -6958,7 +7001,7 @@ class NoMultipleMain extends Rule {
6958
7001
  }
6959
7002
  }
6960
7003
 
6961
- const defaults$c = {
7004
+ const defaults$d = {
6962
7005
  relaxed: false,
6963
7006
  };
6964
7007
  const textRegexp = /([<>]|&(?![a-zA-Z0-9#]+;))/g;
@@ -6975,7 +7018,7 @@ const replacementTable = {
6975
7018
  };
6976
7019
  class NoRawCharacters extends Rule {
6977
7020
  constructor(options) {
6978
- super({ ...defaults$c, ...options });
7021
+ super({ ...defaults$d, ...options });
6979
7022
  this.relaxed = this.options.relaxed;
6980
7023
  }
6981
7024
  static schema() {
@@ -7153,13 +7196,13 @@ class NoRedundantRole extends Rule {
7153
7196
  }
7154
7197
 
7155
7198
  const xmlns = /^(.+):.+$/;
7156
- const defaults$b = {
7199
+ const defaults$c = {
7157
7200
  ignoreForeign: true,
7158
7201
  ignoreXML: true,
7159
7202
  };
7160
7203
  class NoSelfClosing extends Rule {
7161
7204
  constructor(options) {
7162
- super({ ...defaults$b, ...options });
7205
+ super({ ...defaults$c, ...options });
7163
7206
  }
7164
7207
  static schema() {
7165
7208
  return {
@@ -7292,13 +7335,13 @@ const replacement = {
7292
7335
  reset: '<button type="reset">',
7293
7336
  image: '<button type="button">',
7294
7337
  };
7295
- const defaults$a = {
7338
+ const defaults$b = {
7296
7339
  include: null,
7297
7340
  exclude: null,
7298
7341
  };
7299
7342
  class PreferButton extends Rule {
7300
7343
  constructor(options) {
7301
- super({ ...defaults$a, ...options });
7344
+ super({ ...defaults$b, ...options });
7302
7345
  }
7303
7346
  static schema() {
7304
7347
  return {
@@ -7373,7 +7416,7 @@ class PreferButton extends Rule {
7373
7416
  }
7374
7417
  }
7375
7418
 
7376
- const defaults$9 = {
7419
+ const defaults$a = {
7377
7420
  mapping: {
7378
7421
  article: "article",
7379
7422
  banner: "header",
@@ -7403,7 +7446,7 @@ const defaults$9 = {
7403
7446
  };
7404
7447
  class PreferNativeElement extends Rule {
7405
7448
  constructor(options) {
7406
- super({ ...defaults$9, ...options });
7449
+ super({ ...defaults$a, ...options });
7407
7450
  }
7408
7451
  static schema() {
7409
7452
  return {
@@ -7523,12 +7566,12 @@ class PreferTbody extends Rule {
7523
7566
  }
7524
7567
  }
7525
7568
 
7526
- const defaults$8 = {
7569
+ const defaults$9 = {
7527
7570
  tags: ["script", "style"],
7528
7571
  };
7529
7572
  class RequireCSPNonce extends Rule {
7530
7573
  constructor(options) {
7531
- super({ ...defaults$8, ...options });
7574
+ super({ ...defaults$9, ...options });
7532
7575
  }
7533
7576
  static schema() {
7534
7577
  return {
@@ -7579,7 +7622,7 @@ class RequireCSPNonce extends Rule {
7579
7622
  }
7580
7623
  }
7581
7624
 
7582
- const defaults$7 = {
7625
+ const defaults$8 = {
7583
7626
  target: "all",
7584
7627
  include: null,
7585
7628
  exclude: null,
@@ -7591,7 +7634,7 @@ const supportSri = {
7591
7634
  };
7592
7635
  class RequireSri extends Rule {
7593
7636
  constructor(options) {
7594
- super({ ...defaults$7, ...options });
7637
+ super({ ...defaults$8, ...options });
7595
7638
  this.target = this.options.target;
7596
7639
  }
7597
7640
  static schema() {
@@ -7753,7 +7796,7 @@ class SvgFocusable extends Rule {
7753
7796
  }
7754
7797
  }
7755
7798
 
7756
- const defaults$6 = {
7799
+ const defaults$7 = {
7757
7800
  characters: [
7758
7801
  { pattern: " ", replacement: "&nbsp;", description: "non-breaking space" },
7759
7802
  { pattern: "-", replacement: "&#8209;", description: "non-breaking hyphen" },
@@ -7796,7 +7839,7 @@ function matchAll(text, regexp) {
7796
7839
  }
7797
7840
  class TelNonBreaking extends Rule {
7798
7841
  constructor(options) {
7799
- super({ ...defaults$6, ...options });
7842
+ super({ ...defaults$7, ...options });
7800
7843
  this.regex = constructRegex(this.options.characters);
7801
7844
  }
7802
7845
  static schema() {
@@ -8084,11 +8127,63 @@ class TextContent extends Rule {
8084
8127
  }
8085
8128
  }
8086
8129
 
8087
- const regexp$1 = /&([a-z0-9]+|#x?[0-9a-f]+);/gi;
8130
+ const defaults$6 = {
8131
+ ignoreCase: false,
8132
+ requireSemicolon: true,
8133
+ };
8134
+ const regexp$1 = /&(?:[a-z0-9]+|#x?[0-9a-f]+)(;|[^a-z0-9]|$)/gi;
8135
+ const lowercaseEntities = elements.entities.map((it) => it.toLowerCase());
8136
+ function isNumerical(entity) {
8137
+ return entity.startsWith("&#");
8138
+ }
8139
+ function getLocation(location, entity, match) {
8140
+ var _a;
8141
+ /* istanbul ignore next: never happens in practive */
8142
+ const index = (_a = match.index) !== null && _a !== void 0 ? _a : 0;
8143
+ return sliceLocation(location, index, index + entity.length);
8144
+ }
8145
+ function getDescription(context, options) {
8146
+ const url = "https://html.spec.whatwg.org/multipage/named-characters.html";
8147
+ let message;
8148
+ if (context) {
8149
+ if (context.terminated) {
8150
+ message = `Unrecognized character reference \`${context.entity}\`.`;
8151
+ }
8152
+ else {
8153
+ message = `Character reference \`${context.entity}\` must be terminated by a semicolon.`;
8154
+ }
8155
+ }
8156
+ else {
8157
+ message = `Unrecognized character reference.`;
8158
+ }
8159
+ return [
8160
+ message,
8161
+ `HTML5 defines a set of [valid character references](${url}) but this is not a valid one.`,
8162
+ "",
8163
+ "Ensure that:",
8164
+ "",
8165
+ "1. The character is one of the listed names.",
8166
+ ...(options.ignoreCase ? [] : ["1. The case is correct (names are case sensitive)."]),
8167
+ ...(options.requireSemicolon ? ["1. The name is terminated with a `;`."] : []),
8168
+ ].join("\n");
8169
+ }
8088
8170
  class UnknownCharReference extends Rule {
8171
+ constructor(options) {
8172
+ super({ ...defaults$6, ...options });
8173
+ }
8174
+ static schema() {
8175
+ return {
8176
+ ignoreCase: {
8177
+ type: "boolean",
8178
+ },
8179
+ requireSemicolon: {
8180
+ type: "boolean",
8181
+ },
8182
+ };
8183
+ }
8089
8184
  documentation(context) {
8090
8185
  return {
8091
- description: `HTML defines a set of valid character references but ${context || "this"} is not a valid one.`,
8186
+ description: getDescription(context, this.options),
8092
8187
  url: "https://html-validate.org/rules/unrecognized-char-ref.html",
8093
8188
  };
8094
8189
  }
@@ -8100,7 +8195,9 @@ class UnknownCharReference extends Rule {
8100
8195
  if (child.nodeType !== NodeType.TEXT_NODE) {
8101
8196
  continue;
8102
8197
  }
8103
- this.findCharacterReferences(child.textContent, child.location);
8198
+ this.findCharacterReferences(node, child.textContent, child.location, {
8199
+ isAttribute: false,
8200
+ });
8104
8201
  }
8105
8202
  });
8106
8203
  this.on("attr", (event) => {
@@ -8108,25 +8205,72 @@ class UnknownCharReference extends Rule {
8108
8205
  if (!event.value) {
8109
8206
  return;
8110
8207
  }
8111
- this.findCharacterReferences(event.value.toString(), event.valueLocation);
8208
+ this.findCharacterReferences(event.target, event.value.toString(), event.valueLocation, {
8209
+ isAttribute: true,
8210
+ });
8112
8211
  });
8113
8212
  }
8114
- findCharacterReferences(text, location) {
8213
+ get entities() {
8214
+ if (this.options.ignoreCase) {
8215
+ return lowercaseEntities;
8216
+ }
8217
+ else {
8218
+ return elements.entities;
8219
+ }
8220
+ }
8221
+ /* eslint-disable-next-line complexity */
8222
+ findCharacterReferences(node, text, location, { isAttribute }) {
8223
+ const { requireSemicolon } = this.options;
8224
+ const isQuerystring = isAttribute && text.includes("?");
8225
+ for (const { match, entity, raw, terminated } of this.getMatches(text)) {
8226
+ /* assume numeric entities are valid for now */
8227
+ if (isNumerical(entity)) {
8228
+ continue;
8229
+ }
8230
+ /* special case: when attributes use query parameters we skip checking
8231
+ * unterminated attributes */
8232
+ if (isQuerystring && !terminated) {
8233
+ continue;
8234
+ }
8235
+ const found = this.entities.includes(entity);
8236
+ /* ignore if this is a known character reference name */
8237
+ if (found && (terminated || !requireSemicolon)) {
8238
+ continue;
8239
+ }
8240
+ if (found && !terminated) {
8241
+ const entityLocation = getLocation(location, entity, match);
8242
+ const message = `Character reference "{{ entity }}" must be terminated by a semicolon`;
8243
+ const context = {
8244
+ entity: raw,
8245
+ terminated: false,
8246
+ };
8247
+ this.report(node, message, entityLocation, context);
8248
+ continue;
8249
+ }
8250
+ const entityLocation = getLocation(location, entity, match);
8251
+ const message = `Unrecognized character reference "{{ entity }}"`;
8252
+ const context = {
8253
+ entity: raw,
8254
+ terminated: true,
8255
+ };
8256
+ this.report(node, message, entityLocation, context);
8257
+ }
8258
+ }
8259
+ *getMatches(text) {
8115
8260
  let match;
8116
8261
  do {
8117
8262
  match = regexp$1.exec(text);
8118
8263
  if (match) {
8119
- const entity = match[0];
8120
- /* assume numeric entities are valid for now */
8121
- if (entity.startsWith("&#")) {
8122
- continue;
8264
+ const terminator = match[1]; // === ";" ? match[1] : "";
8265
+ const terminated = terminator === ";";
8266
+ const needSlice = terminator !== ";" && terminator.length > 0;
8267
+ const entity = needSlice ? match[0].slice(0, -1) : match[0];
8268
+ if (this.options.ignoreCase) {
8269
+ yield { match, entity: entity.toLowerCase(), raw: entity, terminated };
8123
8270
  }
8124
- /* ignore if this is a known character reference name */
8125
- if (elements.entities.includes(entity)) {
8126
- continue;
8271
+ else {
8272
+ yield { match, entity, raw: entity, terminated };
8127
8273
  }
8128
- const entityLocation = sliceLocation(location, match.index, match.index + entity.length);
8129
- this.report(null, `Unrecognized character reference "${entity}"`, entityLocation, entity);
8130
8274
  }
8131
8275
  } while (match);
8132
8276
  }
@@ -8231,10 +8375,6 @@ const defaults$4 = {
8231
8375
  style: "omit",
8232
8376
  };
8233
8377
  class Void extends Rule {
8234
- constructor(options) {
8235
- super({ ...defaults$4, ...options });
8236
- this.style = parseStyle$1(this.options.style);
8237
- }
8238
8378
  get deprecated() {
8239
8379
  return true;
8240
8380
  }
@@ -8252,6 +8392,10 @@ class Void extends Rule {
8252
8392
  url: "https://html-validate.org/rules/void.html",
8253
8393
  };
8254
8394
  }
8395
+ constructor(options) {
8396
+ super({ ...defaults$4, ...options });
8397
+ this.style = parseStyle$1(this.options.style);
8398
+ }
8255
8399
  setup() {
8256
8400
  this.on("tag:end", (event) => {
8257
8401
  const current = event.target; // The current element being closed
@@ -8434,7 +8578,7 @@ class H30 extends Rule {
8434
8578
  continue;
8435
8579
  }
8436
8580
  /* check if text content is present (or dynamic) */
8437
- const textClassification = rulesHelper.classifyNodeText(link);
8581
+ const textClassification = rulesHelper.classifyNodeText(link, { ignoreHiddenRoot: true });
8438
8582
  if (textClassification !== rulesHelper.TextClassification.EMPTY_TEXT) {
8439
8583
  continue;
8440
8584
  }
@@ -8622,6 +8766,32 @@ class H37 extends Rule {
8622
8766
  }
8623
8767
  }
8624
8768
 
8769
+ class H63 extends Rule {
8770
+ documentation() {
8771
+ return {
8772
+ description: "H63: Using the scope attribute to associate header cells and data cells in data tables",
8773
+ url: "https://html-validate.org/rules/wcag/h63.html",
8774
+ };
8775
+ }
8776
+ setup() {
8777
+ this.on("tag:ready", (event) => {
8778
+ var _a, _b, _c, _d;
8779
+ const node = event.target;
8780
+ /* only validate th */
8781
+ if (!node || node.tagName !== "th") {
8782
+ return;
8783
+ }
8784
+ /* ignore elements with valid scope values */
8785
+ const scope = node.getAttributeValue("scope");
8786
+ const scopeMeta = (_b = (_a = elements.html5 === null || elements.html5 === void 0 ? void 0 : elements.html5.th) === null || _a === void 0 ? void 0 : _a.attributes) === null || _b === void 0 ? void 0 : _b.scope;
8787
+ if (scope && ((_c = scopeMeta.enum) === null || _c === void 0 ? void 0 : _c.includes(scope))) {
8788
+ return;
8789
+ }
8790
+ this.report(node, `<th> element must have a valid scope attribute: ${((_d = scopeMeta.enum) !== null && _d !== void 0 ? _d : []).join(", ")}`, node.location);
8791
+ });
8792
+ }
8793
+ }
8794
+
8625
8795
  class H67 extends Rule {
8626
8796
  documentation() {
8627
8797
  return {
@@ -8686,6 +8856,7 @@ const bundledRules$1 = {
8686
8856
  "wcag/h32": H32,
8687
8857
  "wcag/h36": H36,
8688
8858
  "wcag/h37": H37,
8859
+ "wcag/h63": H63,
8689
8860
  "wcag/h67": H67,
8690
8861
  "wcag/h71": H71,
8691
8862
  };
@@ -8727,6 +8898,7 @@ const bundledRules = {
8727
8898
  "input-attributes": InputAttributes,
8728
8899
  "input-missing-label": InputMissingLabel,
8729
8900
  "long-title": LongTitle,
8901
+ "map-dup-name": MapDupName,
8730
8902
  "meta-refresh": MetaRefresh,
8731
8903
  "missing-doctype": MissingDoctype,
8732
8904
  "multiple-labeled-controls": MultipleLabeledControls,
@@ -8789,6 +8961,7 @@ const config$3 = {
8789
8961
  "wcag/h32": "error",
8790
8962
  "wcag/h36": "error",
8791
8963
  "wcag/h37": "error",
8964
+ "wcag/h63": "error",
8792
8965
  "wcag/h67": "error",
8793
8966
  "wcag/h71": "error",
8794
8967
  },
@@ -8836,6 +9009,7 @@ const config$1 = {
8836
9009
  "empty-title": "error",
8837
9010
  "input-attributes": "error",
8838
9011
  "long-title": "error",
9012
+ "map-dup-name": "error",
8839
9013
  "meta-refresh": "error",
8840
9014
  "multiple-labeled-controls": "error",
8841
9015
  "no-autoplay": ["error", { include: ["audio", "video"] }],
@@ -8870,6 +9044,7 @@ const config$1 = {
8870
9044
  "wcag/h32": "error",
8871
9045
  "wcag/h36": "error",
8872
9046
  "wcag/h37": "error",
9047
+ "wcag/h63": "error",
8873
9048
  "wcag/h67": "error",
8874
9049
  "wcag/h71": "error",
8875
9050
  },
@@ -8894,6 +9069,7 @@ const config = {
8894
9069
  "element-required-ancestor": "error",
8895
9070
  "element-required-attributes": "error",
8896
9071
  "element-required-content": "error",
9072
+ "map-dup-name": "error",
8897
9073
  "multiple-labeled-controls": "error",
8898
9074
  "no-deprecated-attr": "error",
8899
9075
  "no-dup-attr": "error",
@@ -9070,38 +9246,6 @@ function configDataFromFile(filename) {
9070
9246
  * @public
9071
9247
  */
9072
9248
  class Config {
9073
- /**
9074
- * @internal
9075
- */
9076
- constructor(options) {
9077
- var _a;
9078
- this.transformers = [];
9079
- const initial = {
9080
- extends: [],
9081
- plugins: [],
9082
- rules: {},
9083
- transform: {},
9084
- };
9085
- this.config = mergeInternal(initial, options || {});
9086
- this.metaTable = null;
9087
- this.rootDir = this.findRootDir();
9088
- this.initialized = false;
9089
- /* load plugins */
9090
- this.plugins = this.loadPlugins(this.config.plugins || []);
9091
- this.configurations = this.loadConfigurations(this.plugins);
9092
- this.extendMeta(this.plugins);
9093
- /* process extended configs */
9094
- this.config = this.extendConfig((_a = this.config.extends) !== null && _a !== void 0 ? _a : []);
9095
- /* reset extends as we already processed them, this prevents the next config
9096
- * from reapplying config from extended config as well as duplicate entries
9097
- * when merging arrays */
9098
- this.config.extends = [];
9099
- /* rules explicitly set by passed options should have precedence over any
9100
- * extended rules, not the other way around. */
9101
- if (options && options.rules) {
9102
- this.config = mergeInternal(this.config, { rules: options.rules });
9103
- }
9104
- }
9105
9249
  /**
9106
9250
  * Create a new blank configuration. See also `Config.defaultConfig()`.
9107
9251
  */
@@ -9163,6 +9307,38 @@ class Config {
9163
9307
  static defaultConfig() {
9164
9308
  return new Config(defaultConfig);
9165
9309
  }
9310
+ /**
9311
+ * @internal
9312
+ */
9313
+ constructor(options) {
9314
+ var _a;
9315
+ this.transformers = [];
9316
+ const initial = {
9317
+ extends: [],
9318
+ plugins: [],
9319
+ rules: {},
9320
+ transform: {},
9321
+ };
9322
+ this.config = mergeInternal(initial, options || {});
9323
+ this.metaTable = null;
9324
+ this.rootDir = this.findRootDir();
9325
+ this.initialized = false;
9326
+ /* load plugins */
9327
+ this.plugins = this.loadPlugins(this.config.plugins || []);
9328
+ this.configurations = this.loadConfigurations(this.plugins);
9329
+ this.extendMeta(this.plugins);
9330
+ /* process extended configs */
9331
+ this.config = this.extendConfig((_a = this.config.extends) !== null && _a !== void 0 ? _a : []);
9332
+ /* reset extends as we already processed them, this prevents the next config
9333
+ * from reapplying config from extended config as well as duplicate entries
9334
+ * when merging arrays */
9335
+ this.config.extends = [];
9336
+ /* rules explicitly set by passed options should have precedence over any
9337
+ * extended rules, not the other way around. */
9338
+ if (options && options.rules) {
9339
+ this.config = mergeInternal(this.config, { rules: options.rules });
9340
+ }
9341
+ }
9166
9342
  /**
9167
9343
  * Initialize plugins, transforms etc.
9168
9344
  *
@@ -10982,10 +11158,19 @@ class HtmlValidate {
10982
11158
  /** @public */
10983
11159
  const name = "html-validate";
10984
11160
  /** @public */
10985
- const version = "7.8.0";
11161
+ const version = "7.10.0";
10986
11162
  /** @public */
10987
11163
  const bugs = "https://gitlab.com/html-validate/html-validate/issues/new";
10988
11164
 
11165
+ /**
11166
+ * Helper function to assist IDE with completion and type-checking.
11167
+ *
11168
+ * @public
11169
+ */
11170
+ function definePlugin(plugin) {
11171
+ return plugin;
11172
+ }
11173
+
10989
11174
  const defaults$1 = {
10990
11175
  silent: false,
10991
11176
  version,
@@ -11440,6 +11625,7 @@ exports.bugs = bugs;
11440
11625
  exports.codeframe = codeframe;
11441
11626
  exports.compatibilityCheck = compatibilityCheck;
11442
11627
  exports.configDataFromFile = configDataFromFile;
11628
+ exports.definePlugin = definePlugin;
11443
11629
  exports.ensureError = ensureError;
11444
11630
  exports.generateIdSelector = generateIdSelector;
11445
11631
  exports.getFormatter = getFormatter;