html-validate 7.12.1 → 7.13.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
@@ -543,6 +543,7 @@ class DOMNode {
543
543
  this.nodeName = nodeName !== null && nodeName !== void 0 ? nodeName : DOCUMENT_NODE_NAME;
544
544
  this.location = location;
545
545
  this.disabledRules = new Set();
546
+ this.blockedRules = new Map();
546
547
  this.childNodes = [];
547
548
  this.unique = counter++;
548
549
  this.cache = null;
@@ -618,14 +619,42 @@ class DOMNode {
618
619
  get lastChild() {
619
620
  return this.childNodes[this.childNodes.length - 1] || null;
620
621
  }
622
+ /**
623
+ * Block a rule for this node.
624
+ *
625
+ * @internal
626
+ */
627
+ blockRule(ruleId, blocker) {
628
+ const current = this.blockedRules.get(ruleId);
629
+ if (current) {
630
+ current.push(blocker);
631
+ }
632
+ else {
633
+ this.blockedRules.set(ruleId, [blocker]);
634
+ }
635
+ }
636
+ /**
637
+ * Blocks multiple rules.
638
+ *
639
+ * @internal
640
+ */
641
+ blockRules(rules, blocker) {
642
+ for (const rule of rules) {
643
+ this.blockRule(rule, blocker);
644
+ }
645
+ }
621
646
  /**
622
647
  * Disable a rule for this node.
648
+ *
649
+ * @internal
623
650
  */
624
651
  disableRule(ruleId) {
625
652
  this.disabledRules.add(ruleId);
626
653
  }
627
654
  /**
628
655
  * Disables multiple rules.
656
+ *
657
+ * @internal
629
658
  */
630
659
  disableRules(rules) {
631
660
  for (const rule of rules) {
@@ -648,10 +677,21 @@ class DOMNode {
648
677
  }
649
678
  /**
650
679
  * Test if a rule is enabled for this node.
680
+ *
681
+ * @internal
651
682
  */
652
683
  ruleEnabled(ruleId) {
653
684
  return !this.disabledRules.has(ruleId);
654
685
  }
686
+ /**
687
+ * Test if a rule is blocked for this node.
688
+ *
689
+ * @internal
690
+ */
691
+ ruleBlockers(ruleId) {
692
+ var _a;
693
+ return (_a = this.blockedRules.get(ruleId)) !== null && _a !== void 0 ? _a : [];
694
+ }
655
695
  generateSelector() {
656
696
  return null;
657
697
  }
@@ -2707,6 +2747,8 @@ function matchAttributeFacade(node, match) {
2707
2747
  const allowedKeys = ["exclude"];
2708
2748
  /**
2709
2749
  * Helper class to validate elements against metadata rules.
2750
+ *
2751
+ * @public
2710
2752
  */
2711
2753
  class Validator {
2712
2754
  /**
@@ -3462,6 +3504,7 @@ class Rule {
3462
3504
  this.event = null;
3463
3505
  this.options = options;
3464
3506
  this.enabled = true;
3507
+ this.blockers = [];
3465
3508
  this.severity = 0;
3466
3509
  this.name = "";
3467
3510
  }
@@ -3471,6 +3514,26 @@ class Rule {
3471
3514
  setServerity(severity) {
3472
3515
  this.severity = severity;
3473
3516
  }
3517
+ /**
3518
+ * Block this rule from generating errors. Pass in an id generated by {@link
3519
+ * createBlocker}. Can be unblocked by {@link unblock}.
3520
+ *
3521
+ * A blocked rule is similar to disabling it but it will still receive parser
3522
+ * events. A list of all blockers is passed to the `rule:error` event.
3523
+ *
3524
+ * @internal
3525
+ */
3526
+ block(id) {
3527
+ this.blockers.push(id);
3528
+ }
3529
+ /**
3530
+ * Unblock a rule previously blocked by {@link block}.
3531
+ *
3532
+ * @internal
3533
+ */
3534
+ unblock(id) {
3535
+ this.blockers = this.blockers.filter((it) => it !== id);
3536
+ }
3474
3537
  setEnabled(enabled) {
3475
3538
  this.enabled = enabled;
3476
3539
  }
@@ -3487,9 +3550,36 @@ class Rule {
3487
3550
  *
3488
3551
  * To be considered enabled the enabled flag must be true and the severity at
3489
3552
  * least warning.
3553
+ *
3554
+ * @internal
3555
+ */
3556
+ isEnabled(node) {
3557
+ return this.enabled && this.severity >= exports.Severity.WARN && (!node || node.ruleEnabled(this.name));
3558
+ }
3559
+ /**
3560
+ * Test if rule is enabled.
3561
+ *
3562
+ * To be considered enabled the enabled flag must be true and the severity at
3563
+ * least warning.
3564
+ *
3565
+ * @internal
3490
3566
  */
3491
- isEnabled() {
3492
- return this.enabled && this.severity >= exports.Severity.WARN;
3567
+ isBlocked(node) {
3568
+ if (this.blockers.length > 0) {
3569
+ return true;
3570
+ }
3571
+ if (node && node.ruleBlockers(this.name).length > 0) {
3572
+ return true;
3573
+ }
3574
+ return false;
3575
+ }
3576
+ /**
3577
+ * Get a list of all blockers currently active this rule.
3578
+ *
3579
+ * @internal
3580
+ */
3581
+ getBlockers(node) {
3582
+ return [...this.blockers, ...(node ? node.ruleBlockers(this.name) : [])];
3493
3583
  }
3494
3584
  /**
3495
3585
  * Check if keyword is being ignored by the current rule configuration.
@@ -3553,8 +3643,16 @@ class Rule {
3553
3643
  }
3554
3644
  report(...args) {
3555
3645
  const { node, message, location, context } = unpackErrorDescriptor(args);
3556
- if (this.isEnabled() && (!node || node.ruleEnabled(this.name))) {
3557
- const where = this.findLocation({ node, location, event: this.event });
3646
+ const enabled = this.isEnabled(node);
3647
+ const blocked = this.isBlocked(node);
3648
+ const where = this.findLocation({ node, location, event: this.event });
3649
+ this.parser.trigger("rule:error", {
3650
+ location: where,
3651
+ ruleId: this.name,
3652
+ enabled,
3653
+ blockers: this.getBlockers(node),
3654
+ });
3655
+ if (enabled && !blocked) {
3558
3656
  const interpolated = interpolate(message, context !== null && context !== void 0 ? context : {});
3559
3657
  this.reporter.add(this, interpolated, this.severity, node, where, context);
3560
3658
  }
@@ -3653,7 +3751,7 @@ class Rule {
3653
3751
  }
3654
3752
  }
3655
3753
 
3656
- const defaults$v = {
3754
+ const defaults$w = {
3657
3755
  allowExternal: true,
3658
3756
  allowRelative: true,
3659
3757
  allowAbsolute: true,
@@ -3697,7 +3795,7 @@ function matchList(value, list) {
3697
3795
  }
3698
3796
  class AllowedLinks extends Rule {
3699
3797
  constructor(options) {
3700
- super({ ...defaults$v, ...options });
3798
+ super({ ...defaults$w, ...options });
3701
3799
  this.allowExternal = parseAllow(this.options.allowExternal);
3702
3800
  this.allowRelative = parseAllow(this.options.allowRelative);
3703
3801
  this.allowAbsolute = parseAllow(this.options.allowAbsolute);
@@ -3845,7 +3943,7 @@ var RuleContext$1;
3845
3943
  RuleContext["MISSING_ALT"] = "missing-alt";
3846
3944
  RuleContext["MISSING_HREF"] = "missing-href";
3847
3945
  })(RuleContext$1 || (RuleContext$1 = {}));
3848
- const defaults$u = {
3946
+ const defaults$v = {
3849
3947
  accessible: true,
3850
3948
  };
3851
3949
  function findByTarget(target, siblings) {
@@ -3883,7 +3981,7 @@ function getDescription$1(context) {
3883
3981
  }
3884
3982
  class AreaAlt extends Rule {
3885
3983
  constructor(options) {
3886
- super({ ...defaults$u, ...options });
3984
+ super({ ...defaults$v, ...options });
3887
3985
  }
3888
3986
  static schema() {
3889
3987
  return {
@@ -4052,13 +4150,13 @@ class ConfigError extends UserError {
4052
4150
  }
4053
4151
  }
4054
4152
 
4055
- const defaults$t = {
4153
+ const defaults$u = {
4056
4154
  style: "lowercase",
4057
4155
  ignoreForeign: true,
4058
4156
  };
4059
4157
  class AttrCase extends Rule {
4060
4158
  constructor(options) {
4061
- super({ ...defaults$t, ...options });
4159
+ super({ ...defaults$u, ...options });
4062
4160
  this.style = new rulesHelper.CaseStyle(this.options.style, "attr-case");
4063
4161
  }
4064
4162
  static schema() {
@@ -4168,7 +4266,7 @@ const MATCH_SCRIPT_DATA = /^[^]*?(?=<\/script)/;
4168
4266
  const MATCH_SCRIPT_END = /^<(\/)(script)/;
4169
4267
  const MATCH_STYLE_DATA = /^[^]*?(?=<\/style)/;
4170
4268
  const MATCH_STYLE_END = /^<(\/)(style)/;
4171
- const MATCH_DIRECTIVE = /^<!--\s*(\[)html-validate-([a-z0-9-]+)\s*(.*?)(]?)\s*-->/;
4269
+ const MATCH_DIRECTIVE = /^(<!--\s*\[html-validate-)([a-z0-9-]+)(\s*)(.*?)(]?\s*-->)/;
4172
4270
  const MATCH_COMMENT = /^<!--([^]*?)-->/;
4173
4271
  const MATCH_CONDITIONAL = /^<!\[([^\]]*?)\]>/;
4174
4272
  class InvalidTokenError extends Error {
@@ -4403,7 +4501,7 @@ class AttrDelimiter extends Rule {
4403
4501
  }
4404
4502
 
4405
4503
  const DEFAULT_PATTERN = "[a-z0-9-:]+";
4406
- const defaults$s = {
4504
+ const defaults$t = {
4407
4505
  pattern: DEFAULT_PATTERN,
4408
4506
  ignoreForeign: true,
4409
4507
  };
@@ -4440,7 +4538,7 @@ function generateDescription(name, pattern) {
4440
4538
  }
4441
4539
  class AttrPattern extends Rule {
4442
4540
  constructor(options) {
4443
- super({ ...defaults$s, ...options });
4541
+ super({ ...defaults$t, ...options });
4444
4542
  this.pattern = generateRegexp(this.options.pattern);
4445
4543
  }
4446
4544
  static schema() {
@@ -4501,7 +4599,7 @@ var QuoteStyle;
4501
4599
  QuoteStyle["AUTO_QUOTE"] = "auto";
4502
4600
  QuoteStyle["ANY_QUOTE"] = "any";
4503
4601
  })(QuoteStyle || (QuoteStyle = {}));
4504
- const defaults$r = {
4602
+ const defaults$s = {
4505
4603
  style: "auto",
4506
4604
  unquoted: false,
4507
4605
  };
@@ -4568,7 +4666,7 @@ class AttrQuotes extends Rule {
4568
4666
  };
4569
4667
  }
4570
4668
  constructor(options) {
4571
- super({ ...defaults$r, ...options });
4669
+ super({ ...defaults$s, ...options });
4572
4670
  this.style = parseStyle$4(this.options.style);
4573
4671
  }
4574
4672
  setup() {
@@ -4738,12 +4836,12 @@ class AttributeAllowedValues extends Rule {
4738
4836
  }
4739
4837
  }
4740
4838
 
4741
- const defaults$q = {
4839
+ const defaults$r = {
4742
4840
  style: "omit",
4743
4841
  };
4744
4842
  class AttributeBooleanStyle extends Rule {
4745
4843
  constructor(options) {
4746
- super({ ...defaults$q, ...options });
4844
+ super({ ...defaults$r, ...options });
4747
4845
  this.hasInvalidStyle = parseStyle$3(this.options.style);
4748
4846
  }
4749
4847
  static schema() {
@@ -4819,12 +4917,12 @@ function reportMessage$1(attr, style) {
4819
4917
  return "";
4820
4918
  }
4821
4919
 
4822
- const defaults$p = {
4920
+ const defaults$q = {
4823
4921
  style: "omit",
4824
4922
  };
4825
4923
  class AttributeEmptyStyle extends Rule {
4826
4924
  constructor(options) {
4827
- super({ ...defaults$p, ...options });
4925
+ super({ ...defaults$q, ...options });
4828
4926
  this.hasInvalidStyle = parseStyle$2(this.options.style);
4829
4927
  }
4830
4928
  static schema() {
@@ -4980,12 +5078,12 @@ function describePattern(pattern) {
4980
5078
  }
4981
5079
  }
4982
5080
 
4983
- const defaults$o = {
5081
+ const defaults$p = {
4984
5082
  pattern: "kebabcase",
4985
5083
  };
4986
5084
  class ClassPattern extends Rule {
4987
5085
  constructor(options) {
4988
- super({ ...defaults$o, ...options });
5086
+ super({ ...defaults$p, ...options });
4989
5087
  this.pattern = parsePattern(this.options.pattern);
4990
5088
  }
4991
5089
  static schema() {
@@ -5094,13 +5192,13 @@ class CloseOrder extends Rule {
5094
5192
  }
5095
5193
  }
5096
5194
 
5097
- const defaults$n = {
5195
+ const defaults$o = {
5098
5196
  include: null,
5099
5197
  exclude: null,
5100
5198
  };
5101
5199
  class Deprecated extends Rule {
5102
5200
  constructor(options) {
5103
- super({ ...defaults$n, ...options });
5201
+ super({ ...defaults$o, ...options });
5104
5202
  }
5105
5203
  static schema() {
5106
5204
  return {
@@ -5263,12 +5361,12 @@ let NoStyleTag$1 = class NoStyleTag extends Rule {
5263
5361
  }
5264
5362
  };
5265
5363
 
5266
- const defaults$m = {
5364
+ const defaults$n = {
5267
5365
  style: "uppercase",
5268
5366
  };
5269
5367
  class DoctypeStyle extends Rule {
5270
5368
  constructor(options) {
5271
- super({ ...defaults$m, ...options });
5369
+ super({ ...defaults$n, ...options });
5272
5370
  }
5273
5371
  static schema() {
5274
5372
  return {
@@ -5300,12 +5398,12 @@ class DoctypeStyle extends Rule {
5300
5398
  }
5301
5399
  }
5302
5400
 
5303
- const defaults$l = {
5401
+ const defaults$m = {
5304
5402
  style: "lowercase",
5305
5403
  };
5306
5404
  class ElementCase extends Rule {
5307
5405
  constructor(options) {
5308
- super({ ...defaults$l, ...options });
5406
+ super({ ...defaults$m, ...options });
5309
5407
  this.style = new rulesHelper.CaseStyle(this.options.style, "element-case");
5310
5408
  }
5311
5409
  static schema() {
@@ -5371,14 +5469,14 @@ class ElementCase extends Rule {
5371
5469
  }
5372
5470
  }
5373
5471
 
5374
- const defaults$k = {
5472
+ const defaults$l = {
5375
5473
  pattern: "^[a-z][a-z0-9\\-._]*-[a-z0-9\\-._]*$",
5376
5474
  whitelist: [],
5377
5475
  blacklist: [],
5378
5476
  };
5379
5477
  class ElementName extends Rule {
5380
5478
  constructor(options) {
5381
- super({ ...defaults$k, ...options });
5479
+ super({ ...defaults$l, ...options });
5382
5480
  // eslint-disable-next-line security/detect-non-literal-regexp
5383
5481
  this.pattern = new RegExp(this.options.pattern);
5384
5482
  }
@@ -5419,7 +5517,7 @@ class ElementName extends Rule {
5419
5517
  ...context.blacklist.map((cur) => `- ${cur}`),
5420
5518
  ];
5421
5519
  }
5422
- if (context.pattern !== defaults$k.pattern) {
5520
+ if (context.pattern !== defaults$l.pattern) {
5423
5521
  return [
5424
5522
  `<${context.tagName}> is not a valid element name. This project is configured to only allow names matching the following regular expression:`,
5425
5523
  "",
@@ -5963,29 +6061,70 @@ class EmptyTitle extends Rule {
5963
6061
  }
5964
6062
  }
5965
6063
 
6064
+ const defaults$k = {
6065
+ allowArrayBrackets: true,
6066
+ shared: ["radio"],
6067
+ };
5966
6068
  const UNIQUE_CACHE_KEY = Symbol("form-elements-unique");
5967
6069
  const SHARED_CACHE_KEY = Symbol("form-elements-shared");
5968
6070
  function haveName(name) {
5969
6071
  return typeof name === "string" && name !== "";
5970
6072
  }
5971
- function allowSharedName(node) {
6073
+ function allowSharedName(node, shared) {
5972
6074
  const type = node.getAttribute("type");
5973
- return Boolean(type && type.valueMatches(["radio", "checkbox"], false));
6075
+ return Boolean(type && type.valueMatches(shared, false));
6076
+ }
6077
+ function getDocumentation(context) {
6078
+ const trailer = "Each form control must have a unique name.";
6079
+ if (!context) {
6080
+ return trailer;
6081
+ }
6082
+ else {
6083
+ const { name } = context;
6084
+ switch (context.kind) {
6085
+ case "duplicate":
6086
+ return [`Duplicate form control name "${name}"`, trailer].join("\n");
6087
+ case "mix":
6088
+ return [
6089
+ `Form control name cannot mix regular name "{{ name }}" with array brackets "{{ name }}[]"`,
6090
+ trailer,
6091
+ ].join("\n");
6092
+ }
6093
+ }
5974
6094
  }
5975
6095
  class FormDupName extends Rule {
5976
- documentation() {
6096
+ constructor(options) {
6097
+ super({ ...defaults$k, ...options });
6098
+ }
6099
+ static schema() {
5977
6100
  return {
5978
- description: "Each form control must have a unique name.",
6101
+ allowArrayBrackets: {
6102
+ type: "boolean",
6103
+ },
6104
+ shared: {
6105
+ type: "array",
6106
+ items: {
6107
+ enum: ["radio", "checkbox", "submit"],
6108
+ },
6109
+ },
6110
+ };
6111
+ }
6112
+ documentation(context) {
6113
+ return {
6114
+ description: getDocumentation(context),
5979
6115
  url: "https://html-validate.org/rules/form-dup-name.html",
5980
6116
  };
5981
6117
  }
5982
6118
  setup() {
5983
6119
  const selector = this.getSelector();
6120
+ const { shared } = this.options;
5984
6121
  this.on("dom:ready", (event) => {
5985
6122
  var _a, _b;
5986
6123
  const { document } = event;
5987
6124
  const controls = document.querySelectorAll(selector);
5988
- const [sharedControls, uniqueControls] = rulesHelper.partition(controls, allowSharedName);
6125
+ const [sharedControls, uniqueControls] = rulesHelper.partition(controls, (it) => {
6126
+ return allowSharedName(it, shared);
6127
+ });
5989
6128
  /* validate all form controls which require unique elements first so each
5990
6129
  * form has a populated list of unique names */
5991
6130
  for (const control of uniqueControls) {
@@ -6012,9 +6151,35 @@ class FormDupName extends Rule {
6012
6151
  }
6013
6152
  validateUniqueName(control, form, attr, name) {
6014
6153
  const elements = this.getUniqueElements(form);
6154
+ const { allowArrayBrackets } = this.options;
6155
+ if (allowArrayBrackets) {
6156
+ const isarray = name.endsWith("[]");
6157
+ const basename = isarray ? name.slice(0, -2) : name;
6158
+ const details = elements.get(basename);
6159
+ if (details && details.array !== isarray) {
6160
+ const context = {
6161
+ name: basename,
6162
+ kind: "mix",
6163
+ };
6164
+ this.report({
6165
+ node: control,
6166
+ location: attr.valueLocation,
6167
+ message: 'Cannot mix "{{ name }}[]" and "{{ name }}"',
6168
+ context,
6169
+ });
6170
+ return;
6171
+ }
6172
+ else if (!details && isarray) {
6173
+ elements.set(basename, {
6174
+ array: true,
6175
+ });
6176
+ return;
6177
+ }
6178
+ }
6015
6179
  if (elements.has(name)) {
6016
6180
  const context = {
6017
6181
  name,
6182
+ kind: "duplicate",
6018
6183
  };
6019
6184
  this.report({
6020
6185
  node: control,
@@ -6024,7 +6189,9 @@ class FormDupName extends Rule {
6024
6189
  });
6025
6190
  }
6026
6191
  else {
6027
- elements.add(name);
6192
+ elements.set(name, {
6193
+ array: false,
6194
+ });
6028
6195
  }
6029
6196
  }
6030
6197
  validateSharedName(control, form, attr, name) {
@@ -6037,6 +6204,7 @@ class FormDupName extends Rule {
6037
6204
  (sharedElements.has(name) && sharedElements.get(name) !== type)) {
6038
6205
  const context = {
6039
6206
  name,
6207
+ kind: "duplicate",
6040
6208
  };
6041
6209
  this.report({
6042
6210
  node: control,
@@ -6068,7 +6236,7 @@ class FormDupName extends Rule {
6068
6236
  return existing;
6069
6237
  }
6070
6238
  else {
6071
- const elements = new Set();
6239
+ const elements = new Map();
6072
6240
  form.cacheSet(UNIQUE_CACHE_KEY, elements);
6073
6241
  return elements;
6074
6242
  }
@@ -7598,6 +7766,39 @@ class NoUnknownElements extends Rule {
7598
7766
  }
7599
7767
  }
7600
7768
 
7769
+ class NoUnusedDisable extends Rule {
7770
+ documentation(context) {
7771
+ return {
7772
+ description: context
7773
+ ? `\`${context.ruleId}\` rule is disabled but no error was reported.`
7774
+ : "Rule is disabled but no error was reported.",
7775
+ url: "https://html-validate.org/rules/no-unused-disable.html",
7776
+ };
7777
+ }
7778
+ setup() {
7779
+ /* this is a special rule, the `Engine` class directly emits errors on this
7780
+ * rule, it exists only to be able to configure whenever the rule is enabled
7781
+ * or not and to get the regular documentation and contextual help. */
7782
+ }
7783
+ reportUnused(unused, options, location) {
7784
+ const tokens = new DOMTokenList(options.replace(",", " "), location);
7785
+ for (const ruleId of unused) {
7786
+ const index = tokens.indexOf(ruleId);
7787
+ /* istanbul ignore next: the token should be present or it wouldn't be
7788
+ * reported as unused, this is just a sanity check and fallback */
7789
+ const tokenLocation = index >= 0 ? tokens.location(index) : location;
7790
+ this.report({
7791
+ node: null,
7792
+ message: '"{{ ruleId }}" rule is disabled but no error was reported',
7793
+ location: tokenLocation,
7794
+ context: {
7795
+ ruleId: ruleId,
7796
+ },
7797
+ });
7798
+ }
7799
+ }
7800
+ }
7801
+
7601
7802
  class NoUtf8Bom extends Rule {
7602
7803
  documentation() {
7603
7804
  return {
@@ -9221,6 +9422,7 @@ const bundledRules = {
9221
9422
  "no-style-tag": NoStyleTag,
9222
9423
  "no-trailing-whitespace": NoTrailingWhitespace,
9223
9424
  "no-unknown-elements": NoUnknownElements,
9425
+ "no-unused-disable": NoUnusedDisable,
9224
9426
  "no-utf8-bom": NoUtf8Bom,
9225
9427
  "prefer-button": PreferButton,
9226
9428
  "prefer-native-element": PreferNativeElement,
@@ -9331,6 +9533,7 @@ const config$1 = {
9331
9533
  "no-self-closing": "error",
9332
9534
  "no-trailing-whitespace": "error",
9333
9535
  "no-utf8-bom": "error",
9536
+ "no-unused-disable": "error",
9334
9537
  "prefer-button": "error",
9335
9538
  "prefer-native-element": "error",
9336
9539
  "prefer-tbody": "error",
@@ -9381,6 +9584,7 @@ const config = {
9381
9584
  "no-dup-id": "error",
9382
9585
  "no-multiple-main": "error",
9383
9586
  "no-raw-characters": ["error", { relaxed: true }],
9587
+ "no-unused-disable": "error",
9384
9588
  "script-element": "error",
9385
9589
  "unrecognized-char-ref": "error",
9386
9590
  "valid-id": ["error", { relaxed: true }],
@@ -10180,6 +10384,10 @@ class Parser {
10180
10384
  offset: 0,
10181
10385
  };
10182
10386
  }
10387
+ /* trigger starting event */
10388
+ this.trigger("parse:begin", {
10389
+ location: null,
10390
+ });
10183
10391
  /* reset DOM in case there are multiple calls in the same session */
10184
10392
  this.dom = new DOMTree({
10185
10393
  filename: (_a = source.filename) !== null && _a !== void 0 ? _a : "",
@@ -10214,6 +10422,10 @@ class Parser {
10214
10422
  * instead */
10215
10423
  location: null,
10216
10424
  });
10425
+ /* trigger ending event */
10426
+ this.trigger("parse:end", {
10427
+ location: null,
10428
+ });
10217
10429
  return this.dom.root;
10218
10430
  }
10219
10431
  /**
@@ -10524,11 +10736,11 @@ class Parser {
10524
10736
  };
10525
10737
  }
10526
10738
  consumeDirective(token) {
10527
- const [text, , action, directive, end] = token.data;
10528
- if (end === "") {
10739
+ const [text, preamble, action, separator1, directive, postamble] = token.data;
10740
+ if (!postamble.startsWith("]")) {
10529
10741
  throw new ParserError(token.location, `Missing end bracket "]" on directive "${text}"`);
10530
10742
  }
10531
- const match = directive.match(/^(.*?)(?:\s*(?:--|:)\s*(.*))?$/);
10743
+ const match = directive.match(/^(.*?)(?:(\s*(?:--|:)\s*)(.*))?$/);
10532
10744
  /* istanbul ignore next: should not be possible, would be emitted as comment token */
10533
10745
  if (!match) {
10534
10746
  throw new Error(`Failed to parse directive "${text}"`);
@@ -10536,12 +10748,32 @@ class Parser {
10536
10748
  if (!isValidDirective(action)) {
10537
10749
  throw new ParserError(token.location, `Unknown directive "${action}"`);
10538
10750
  }
10539
- const [, data, comment] = match;
10751
+ const [, data, separator2, comment] = match;
10752
+ const prefix = "html-validate-";
10753
+ /* <!-- [html-validate-action options -- comment] -->
10754
+ * ^ ^ ^--------------- comment offset
10755
+ * | \-------------------------- options offset
10756
+ * \--------------------------------- action offset
10757
+ */
10758
+ const actionOffset = preamble.length;
10759
+ const optionsOffset = actionOffset + action.length + separator1.length;
10760
+ const commentOffset = optionsOffset + data.length + (separator2 || "").length;
10761
+ const location = sliceLocation(token.location, preamble.length - prefix.length - 1, -postamble.length + 1);
10762
+ const actionLocation = sliceLocation(token.location, actionOffset, actionOffset + action.length);
10763
+ const optionsLocation = data
10764
+ ? sliceLocation(token.location, optionsOffset, optionsOffset + data.length)
10765
+ : undefined;
10766
+ const commentLocation = comment
10767
+ ? sliceLocation(token.location, commentOffset, commentOffset + comment.length)
10768
+ : undefined;
10540
10769
  this.trigger("directive", {
10541
10770
  action,
10542
10771
  data,
10543
10772
  comment: comment || "",
10544
- location: token.location,
10773
+ location,
10774
+ actionLocation,
10775
+ optionsLocation,
10776
+ commentLocation,
10545
10777
  });
10546
10778
  }
10547
10779
  /**
@@ -10827,6 +11059,18 @@ function messageSort(a, b) {
10827
11059
  return 0;
10828
11060
  }
10829
11061
 
11062
+ let blockerCounter = 1;
11063
+ /**
11064
+ * Creates a new rule blocker for using when blocking rules from generating
11065
+ * errors.
11066
+ *
11067
+ * @internal
11068
+ */
11069
+ function createBlocker() {
11070
+ const id = blockerCounter++;
11071
+ return id;
11072
+ }
11073
+
10830
11074
  /**
10831
11075
  * @internal
10832
11076
  */
@@ -10854,6 +11098,15 @@ class Engine {
10854
11098
  const parser = this.instantiateParser();
10855
11099
  /* setup plugins and rules */
10856
11100
  const { rules } = this.setupPlugins(source, this.config, parser);
11101
+ const noUnusedDisable = rules["no-unused-disable"];
11102
+ const directiveContext = {
11103
+ rules,
11104
+ reportUnused(unused, options, location) {
11105
+ if (noUnusedDisable) {
11106
+ noUnusedDisable.reportUnused(unused, options, location);
11107
+ }
11108
+ },
11109
+ };
10857
11110
  /* create a faux location at the start of the stream for the next events */
10858
11111
  const location = {
10859
11112
  filename: source.filename,
@@ -10879,7 +11132,7 @@ class Engine {
10879
11132
  parser.trigger("source:ready", sourceEvent);
10880
11133
  /* setup directive handling */
10881
11134
  parser.on("directive", (_, event) => {
10882
- this.processDirective(event, parser, rules);
11135
+ this.processDirective(event, parser, directiveContext);
10883
11136
  });
10884
11137
  /* parse token stream */
10885
11138
  try {
@@ -10986,12 +11239,15 @@ class Engine {
10986
11239
  instantiateParser() {
10987
11240
  return new this.ParserClass(this.config);
10988
11241
  }
10989
- processDirective(event, parser, allRules) {
11242
+ processDirective(event, parser, context) {
11243
+ var _a;
10990
11244
  const rules = event.data
10991
11245
  .split(",")
10992
11246
  .map((name) => name.trim())
10993
- .map((name) => allRules[name])
11247
+ .map((name) => context.rules[name])
10994
11248
  .filter((rule) => rule); /* filter out missing rules */
11249
+ /* istanbul ignore next: option must be present or there would be no rules to disable */
11250
+ const location = (_a = event.optionsLocation) !== null && _a !== void 0 ? _a : event.location;
10995
11251
  switch (event.action) {
10996
11252
  case "enable":
10997
11253
  this.processEnableDirective(rules, parser);
@@ -11000,10 +11256,10 @@ class Engine {
11000
11256
  this.processDisableDirective(rules, parser);
11001
11257
  break;
11002
11258
  case "disable-block":
11003
- this.processDisableBlockDirective(rules, parser);
11259
+ this.processDisableBlockDirective(context, rules, parser, event.data, location);
11004
11260
  break;
11005
11261
  case "disable-next":
11006
- this.processDisableNextDirective(rules, parser);
11262
+ this.processDisableNextDirective(context, rules, parser, event.data, location);
11007
11263
  break;
11008
11264
  }
11009
11265
  }
@@ -11028,10 +11284,13 @@ class Engine {
11028
11284
  data.target.disableRules(rules.map((rule) => rule.name));
11029
11285
  });
11030
11286
  }
11031
- processDisableBlockDirective(rules, parser) {
11287
+ processDisableBlockDirective(context, rules, parser, options, location) {
11288
+ const ruleIds = rules.map((it) => it.name);
11289
+ const blocker = createBlocker();
11290
+ const unused = new Set(ruleIds);
11032
11291
  let directiveBlock = null;
11033
11292
  for (const rule of rules) {
11034
- rule.setEnabled(false);
11293
+ rule.block(blocker);
11035
11294
  }
11036
11295
  const unregisterOpen = parser.on("tag:start", (event, data) => {
11037
11296
  var _a, _b;
@@ -11043,7 +11302,7 @@ class Engine {
11043
11302
  }
11044
11303
  /* disable rules directly on the node so it will be recorded for later,
11045
11304
  * more specifically when using the domtree to trigger errors */
11046
- data.target.disableRules(rules.map((rule) => rule.name));
11305
+ data.target.blockRules(ruleIds, blocker);
11047
11306
  });
11048
11307
  const unregisterClose = parser.on("tag:end", (event, data) => {
11049
11308
  /* if the directive is the last thing in a block no id would be set */
@@ -11056,26 +11315,45 @@ class Engine {
11056
11315
  unregisterClose();
11057
11316
  unregisterOpen();
11058
11317
  for (const rule of rules) {
11059
- rule.setEnabled(true);
11318
+ rule.unblock(blocker);
11060
11319
  }
11061
11320
  }
11062
11321
  });
11322
+ parser.on("rule:error", (event, data) => {
11323
+ if (data.blockers.includes(blocker)) {
11324
+ unused.delete(data.ruleId);
11325
+ }
11326
+ });
11327
+ parser.on("parse:end", () => {
11328
+ context.reportUnused(unused, options, location);
11329
+ });
11063
11330
  }
11064
- processDisableNextDirective(rules, parser) {
11331
+ processDisableNextDirective(context, rules, parser, options, location) {
11332
+ const ruleIds = rules.map((it) => it.name);
11333
+ const blocker = createBlocker();
11334
+ const unused = new Set(ruleIds);
11065
11335
  for (const rule of rules) {
11066
- rule.setEnabled(false);
11336
+ rule.block(blocker);
11067
11337
  }
11068
- /* disable rules directly on the node so it will be recorded for later,
11338
+ /* block rules directly on the node so it will be recorded for later,
11069
11339
  * more specifically when using the domtree to trigger errors */
11070
11340
  const unregister = parser.on("tag:start", (event, data) => {
11071
- data.target.disableRules(rules.map((rule) => rule.name));
11341
+ data.target.blockRules(ruleIds, blocker);
11342
+ });
11343
+ parser.on("rule:error", (event, data) => {
11344
+ if (data.blockers.includes(blocker)) {
11345
+ unused.delete(data.ruleId);
11346
+ }
11347
+ });
11348
+ parser.on("parse:end", () => {
11349
+ context.reportUnused(unused, options, location);
11072
11350
  });
11073
11351
  /* disable directive after next event occurs */
11074
11352
  parser.once("tag:ready, tag:end, attr", () => {
11075
11353
  unregister();
11076
11354
  parser.defer(() => {
11077
11355
  for (const rule of rules) {
11078
- rule.setEnabled(true);
11356
+ rule.unblock(blocker);
11079
11357
  }
11080
11358
  });
11081
11359
  });
@@ -11467,7 +11745,7 @@ class HtmlValidate {
11467
11745
  /** @public */
11468
11746
  const name = "html-validate";
11469
11747
  /** @public */
11470
- const version = "7.12.1";
11748
+ const version = "7.13.0";
11471
11749
  /** @public */
11472
11750
  const bugs = "https://gitlab.com/html-validate/html-validate/issues/new";
11473
11751
 
@@ -11930,6 +12208,7 @@ exports.StaticConfigLoader = StaticConfigLoader;
11930
12208
  exports.TemplateExtractor = TemplateExtractor;
11931
12209
  exports.TextNode = TextNode;
11932
12210
  exports.UserError = UserError;
12211
+ exports.Validator = Validator;
11933
12212
  exports.WrappedError = WrappedError;
11934
12213
  exports.bugs = bugs;
11935
12214
  exports.codeframe = codeframe;