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/es/core.js CHANGED
@@ -511,6 +511,7 @@ class DOMNode {
511
511
  this.nodeName = nodeName !== null && nodeName !== void 0 ? nodeName : DOCUMENT_NODE_NAME;
512
512
  this.location = location;
513
513
  this.disabledRules = new Set();
514
+ this.blockedRules = new Map();
514
515
  this.childNodes = [];
515
516
  this.unique = counter++;
516
517
  this.cache = null;
@@ -586,14 +587,42 @@ class DOMNode {
586
587
  get lastChild() {
587
588
  return this.childNodes[this.childNodes.length - 1] || null;
588
589
  }
590
+ /**
591
+ * Block a rule for this node.
592
+ *
593
+ * @internal
594
+ */
595
+ blockRule(ruleId, blocker) {
596
+ const current = this.blockedRules.get(ruleId);
597
+ if (current) {
598
+ current.push(blocker);
599
+ }
600
+ else {
601
+ this.blockedRules.set(ruleId, [blocker]);
602
+ }
603
+ }
604
+ /**
605
+ * Blocks multiple rules.
606
+ *
607
+ * @internal
608
+ */
609
+ blockRules(rules, blocker) {
610
+ for (const rule of rules) {
611
+ this.blockRule(rule, blocker);
612
+ }
613
+ }
589
614
  /**
590
615
  * Disable a rule for this node.
616
+ *
617
+ * @internal
591
618
  */
592
619
  disableRule(ruleId) {
593
620
  this.disabledRules.add(ruleId);
594
621
  }
595
622
  /**
596
623
  * Disables multiple rules.
624
+ *
625
+ * @internal
597
626
  */
598
627
  disableRules(rules) {
599
628
  for (const rule of rules) {
@@ -616,10 +645,21 @@ class DOMNode {
616
645
  }
617
646
  /**
618
647
  * Test if a rule is enabled for this node.
648
+ *
649
+ * @internal
619
650
  */
620
651
  ruleEnabled(ruleId) {
621
652
  return !this.disabledRules.has(ruleId);
622
653
  }
654
+ /**
655
+ * Test if a rule is blocked for this node.
656
+ *
657
+ * @internal
658
+ */
659
+ ruleBlockers(ruleId) {
660
+ var _a;
661
+ return (_a = this.blockedRules.get(ruleId)) !== null && _a !== void 0 ? _a : [];
662
+ }
623
663
  generateSelector() {
624
664
  return null;
625
665
  }
@@ -2675,6 +2715,8 @@ function matchAttributeFacade(node, match) {
2675
2715
  const allowedKeys = ["exclude"];
2676
2716
  /**
2677
2717
  * Helper class to validate elements against metadata rules.
2718
+ *
2719
+ * @public
2678
2720
  */
2679
2721
  class Validator {
2680
2722
  /**
@@ -3430,6 +3472,7 @@ class Rule {
3430
3472
  this.event = null;
3431
3473
  this.options = options;
3432
3474
  this.enabled = true;
3475
+ this.blockers = [];
3433
3476
  this.severity = 0;
3434
3477
  this.name = "";
3435
3478
  }
@@ -3439,6 +3482,26 @@ class Rule {
3439
3482
  setServerity(severity) {
3440
3483
  this.severity = severity;
3441
3484
  }
3485
+ /**
3486
+ * Block this rule from generating errors. Pass in an id generated by {@link
3487
+ * createBlocker}. Can be unblocked by {@link unblock}.
3488
+ *
3489
+ * A blocked rule is similar to disabling it but it will still receive parser
3490
+ * events. A list of all blockers is passed to the `rule:error` event.
3491
+ *
3492
+ * @internal
3493
+ */
3494
+ block(id) {
3495
+ this.blockers.push(id);
3496
+ }
3497
+ /**
3498
+ * Unblock a rule previously blocked by {@link block}.
3499
+ *
3500
+ * @internal
3501
+ */
3502
+ unblock(id) {
3503
+ this.blockers = this.blockers.filter((it) => it !== id);
3504
+ }
3442
3505
  setEnabled(enabled) {
3443
3506
  this.enabled = enabled;
3444
3507
  }
@@ -3455,9 +3518,36 @@ class Rule {
3455
3518
  *
3456
3519
  * To be considered enabled the enabled flag must be true and the severity at
3457
3520
  * least warning.
3521
+ *
3522
+ * @internal
3523
+ */
3524
+ isEnabled(node) {
3525
+ return this.enabled && this.severity >= Severity.WARN && (!node || node.ruleEnabled(this.name));
3526
+ }
3527
+ /**
3528
+ * Test if rule is enabled.
3529
+ *
3530
+ * To be considered enabled the enabled flag must be true and the severity at
3531
+ * least warning.
3532
+ *
3533
+ * @internal
3458
3534
  */
3459
- isEnabled() {
3460
- return this.enabled && this.severity >= Severity.WARN;
3535
+ isBlocked(node) {
3536
+ if (this.blockers.length > 0) {
3537
+ return true;
3538
+ }
3539
+ if (node && node.ruleBlockers(this.name).length > 0) {
3540
+ return true;
3541
+ }
3542
+ return false;
3543
+ }
3544
+ /**
3545
+ * Get a list of all blockers currently active this rule.
3546
+ *
3547
+ * @internal
3548
+ */
3549
+ getBlockers(node) {
3550
+ return [...this.blockers, ...(node ? node.ruleBlockers(this.name) : [])];
3461
3551
  }
3462
3552
  /**
3463
3553
  * Check if keyword is being ignored by the current rule configuration.
@@ -3521,8 +3611,16 @@ class Rule {
3521
3611
  }
3522
3612
  report(...args) {
3523
3613
  const { node, message, location, context } = unpackErrorDescriptor(args);
3524
- if (this.isEnabled() && (!node || node.ruleEnabled(this.name))) {
3525
- const where = this.findLocation({ node, location, event: this.event });
3614
+ const enabled = this.isEnabled(node);
3615
+ const blocked = this.isBlocked(node);
3616
+ const where = this.findLocation({ node, location, event: this.event });
3617
+ this.parser.trigger("rule:error", {
3618
+ location: where,
3619
+ ruleId: this.name,
3620
+ enabled,
3621
+ blockers: this.getBlockers(node),
3622
+ });
3623
+ if (enabled && !blocked) {
3526
3624
  const interpolated = interpolate(message, context !== null && context !== void 0 ? context : {});
3527
3625
  this.reporter.add(this, interpolated, this.severity, node, where, context);
3528
3626
  }
@@ -3621,7 +3719,7 @@ class Rule {
3621
3719
  }
3622
3720
  }
3623
3721
 
3624
- const defaults$v = {
3722
+ const defaults$w = {
3625
3723
  allowExternal: true,
3626
3724
  allowRelative: true,
3627
3725
  allowAbsolute: true,
@@ -3665,7 +3763,7 @@ function matchList(value, list) {
3665
3763
  }
3666
3764
  class AllowedLinks extends Rule {
3667
3765
  constructor(options) {
3668
- super({ ...defaults$v, ...options });
3766
+ super({ ...defaults$w, ...options });
3669
3767
  this.allowExternal = parseAllow(this.options.allowExternal);
3670
3768
  this.allowRelative = parseAllow(this.options.allowRelative);
3671
3769
  this.allowAbsolute = parseAllow(this.options.allowAbsolute);
@@ -3813,7 +3911,7 @@ var RuleContext$1;
3813
3911
  RuleContext["MISSING_ALT"] = "missing-alt";
3814
3912
  RuleContext["MISSING_HREF"] = "missing-href";
3815
3913
  })(RuleContext$1 || (RuleContext$1 = {}));
3816
- const defaults$u = {
3914
+ const defaults$v = {
3817
3915
  accessible: true,
3818
3916
  };
3819
3917
  function findByTarget(target, siblings) {
@@ -3851,7 +3949,7 @@ function getDescription$1(context) {
3851
3949
  }
3852
3950
  class AreaAlt extends Rule {
3853
3951
  constructor(options) {
3854
- super({ ...defaults$u, ...options });
3952
+ super({ ...defaults$v, ...options });
3855
3953
  }
3856
3954
  static schema() {
3857
3955
  return {
@@ -4020,13 +4118,13 @@ class ConfigError extends UserError {
4020
4118
  }
4021
4119
  }
4022
4120
 
4023
- const defaults$t = {
4121
+ const defaults$u = {
4024
4122
  style: "lowercase",
4025
4123
  ignoreForeign: true,
4026
4124
  };
4027
4125
  class AttrCase extends Rule {
4028
4126
  constructor(options) {
4029
- super({ ...defaults$t, ...options });
4127
+ super({ ...defaults$u, ...options });
4030
4128
  this.style = new CaseStyle(this.options.style, "attr-case");
4031
4129
  }
4032
4130
  static schema() {
@@ -4136,7 +4234,7 @@ const MATCH_SCRIPT_DATA = /^[^]*?(?=<\/script)/;
4136
4234
  const MATCH_SCRIPT_END = /^<(\/)(script)/;
4137
4235
  const MATCH_STYLE_DATA = /^[^]*?(?=<\/style)/;
4138
4236
  const MATCH_STYLE_END = /^<(\/)(style)/;
4139
- const MATCH_DIRECTIVE = /^<!--\s*(\[)html-validate-([a-z0-9-]+)\s*(.*?)(]?)\s*-->/;
4237
+ const MATCH_DIRECTIVE = /^(<!--\s*\[html-validate-)([a-z0-9-]+)(\s*)(.*?)(]?\s*-->)/;
4140
4238
  const MATCH_COMMENT = /^<!--([^]*?)-->/;
4141
4239
  const MATCH_CONDITIONAL = /^<!\[([^\]]*?)\]>/;
4142
4240
  class InvalidTokenError extends Error {
@@ -4371,7 +4469,7 @@ class AttrDelimiter extends Rule {
4371
4469
  }
4372
4470
 
4373
4471
  const DEFAULT_PATTERN = "[a-z0-9-:]+";
4374
- const defaults$s = {
4472
+ const defaults$t = {
4375
4473
  pattern: DEFAULT_PATTERN,
4376
4474
  ignoreForeign: true,
4377
4475
  };
@@ -4408,7 +4506,7 @@ function generateDescription(name, pattern) {
4408
4506
  }
4409
4507
  class AttrPattern extends Rule {
4410
4508
  constructor(options) {
4411
- super({ ...defaults$s, ...options });
4509
+ super({ ...defaults$t, ...options });
4412
4510
  this.pattern = generateRegexp(this.options.pattern);
4413
4511
  }
4414
4512
  static schema() {
@@ -4469,7 +4567,7 @@ var QuoteStyle;
4469
4567
  QuoteStyle["AUTO_QUOTE"] = "auto";
4470
4568
  QuoteStyle["ANY_QUOTE"] = "any";
4471
4569
  })(QuoteStyle || (QuoteStyle = {}));
4472
- const defaults$r = {
4570
+ const defaults$s = {
4473
4571
  style: "auto",
4474
4572
  unquoted: false,
4475
4573
  };
@@ -4536,7 +4634,7 @@ class AttrQuotes extends Rule {
4536
4634
  };
4537
4635
  }
4538
4636
  constructor(options) {
4539
- super({ ...defaults$r, ...options });
4637
+ super({ ...defaults$s, ...options });
4540
4638
  this.style = parseStyle$4(this.options.style);
4541
4639
  }
4542
4640
  setup() {
@@ -4706,12 +4804,12 @@ class AttributeAllowedValues extends Rule {
4706
4804
  }
4707
4805
  }
4708
4806
 
4709
- const defaults$q = {
4807
+ const defaults$r = {
4710
4808
  style: "omit",
4711
4809
  };
4712
4810
  class AttributeBooleanStyle extends Rule {
4713
4811
  constructor(options) {
4714
- super({ ...defaults$q, ...options });
4812
+ super({ ...defaults$r, ...options });
4715
4813
  this.hasInvalidStyle = parseStyle$3(this.options.style);
4716
4814
  }
4717
4815
  static schema() {
@@ -4787,12 +4885,12 @@ function reportMessage$1(attr, style) {
4787
4885
  return "";
4788
4886
  }
4789
4887
 
4790
- const defaults$p = {
4888
+ const defaults$q = {
4791
4889
  style: "omit",
4792
4890
  };
4793
4891
  class AttributeEmptyStyle extends Rule {
4794
4892
  constructor(options) {
4795
- super({ ...defaults$p, ...options });
4893
+ super({ ...defaults$q, ...options });
4796
4894
  this.hasInvalidStyle = parseStyle$2(this.options.style);
4797
4895
  }
4798
4896
  static schema() {
@@ -4948,12 +5046,12 @@ function describePattern(pattern) {
4948
5046
  }
4949
5047
  }
4950
5048
 
4951
- const defaults$o = {
5049
+ const defaults$p = {
4952
5050
  pattern: "kebabcase",
4953
5051
  };
4954
5052
  class ClassPattern extends Rule {
4955
5053
  constructor(options) {
4956
- super({ ...defaults$o, ...options });
5054
+ super({ ...defaults$p, ...options });
4957
5055
  this.pattern = parsePattern(this.options.pattern);
4958
5056
  }
4959
5057
  static schema() {
@@ -5062,13 +5160,13 @@ class CloseOrder extends Rule {
5062
5160
  }
5063
5161
  }
5064
5162
 
5065
- const defaults$n = {
5163
+ const defaults$o = {
5066
5164
  include: null,
5067
5165
  exclude: null,
5068
5166
  };
5069
5167
  class Deprecated extends Rule {
5070
5168
  constructor(options) {
5071
- super({ ...defaults$n, ...options });
5169
+ super({ ...defaults$o, ...options });
5072
5170
  }
5073
5171
  static schema() {
5074
5172
  return {
@@ -5231,12 +5329,12 @@ let NoStyleTag$1 = class NoStyleTag extends Rule {
5231
5329
  }
5232
5330
  };
5233
5331
 
5234
- const defaults$m = {
5332
+ const defaults$n = {
5235
5333
  style: "uppercase",
5236
5334
  };
5237
5335
  class DoctypeStyle extends Rule {
5238
5336
  constructor(options) {
5239
- super({ ...defaults$m, ...options });
5337
+ super({ ...defaults$n, ...options });
5240
5338
  }
5241
5339
  static schema() {
5242
5340
  return {
@@ -5268,12 +5366,12 @@ class DoctypeStyle extends Rule {
5268
5366
  }
5269
5367
  }
5270
5368
 
5271
- const defaults$l = {
5369
+ const defaults$m = {
5272
5370
  style: "lowercase",
5273
5371
  };
5274
5372
  class ElementCase extends Rule {
5275
5373
  constructor(options) {
5276
- super({ ...defaults$l, ...options });
5374
+ super({ ...defaults$m, ...options });
5277
5375
  this.style = new CaseStyle(this.options.style, "element-case");
5278
5376
  }
5279
5377
  static schema() {
@@ -5339,14 +5437,14 @@ class ElementCase extends Rule {
5339
5437
  }
5340
5438
  }
5341
5439
 
5342
- const defaults$k = {
5440
+ const defaults$l = {
5343
5441
  pattern: "^[a-z][a-z0-9\\-._]*-[a-z0-9\\-._]*$",
5344
5442
  whitelist: [],
5345
5443
  blacklist: [],
5346
5444
  };
5347
5445
  class ElementName extends Rule {
5348
5446
  constructor(options) {
5349
- super({ ...defaults$k, ...options });
5447
+ super({ ...defaults$l, ...options });
5350
5448
  // eslint-disable-next-line security/detect-non-literal-regexp
5351
5449
  this.pattern = new RegExp(this.options.pattern);
5352
5450
  }
@@ -5387,7 +5485,7 @@ class ElementName extends Rule {
5387
5485
  ...context.blacklist.map((cur) => `- ${cur}`),
5388
5486
  ];
5389
5487
  }
5390
- if (context.pattern !== defaults$k.pattern) {
5488
+ if (context.pattern !== defaults$l.pattern) {
5391
5489
  return [
5392
5490
  `<${context.tagName}> is not a valid element name. This project is configured to only allow names matching the following regular expression:`,
5393
5491
  "",
@@ -5931,29 +6029,70 @@ class EmptyTitle extends Rule {
5931
6029
  }
5932
6030
  }
5933
6031
 
6032
+ const defaults$k = {
6033
+ allowArrayBrackets: true,
6034
+ shared: ["radio"],
6035
+ };
5934
6036
  const UNIQUE_CACHE_KEY = Symbol("form-elements-unique");
5935
6037
  const SHARED_CACHE_KEY = Symbol("form-elements-shared");
5936
6038
  function haveName(name) {
5937
6039
  return typeof name === "string" && name !== "";
5938
6040
  }
5939
- function allowSharedName(node) {
6041
+ function allowSharedName(node, shared) {
5940
6042
  const type = node.getAttribute("type");
5941
- return Boolean(type && type.valueMatches(["radio", "checkbox"], false));
6043
+ return Boolean(type && type.valueMatches(shared, false));
6044
+ }
6045
+ function getDocumentation(context) {
6046
+ const trailer = "Each form control must have a unique name.";
6047
+ if (!context) {
6048
+ return trailer;
6049
+ }
6050
+ else {
6051
+ const { name } = context;
6052
+ switch (context.kind) {
6053
+ case "duplicate":
6054
+ return [`Duplicate form control name "${name}"`, trailer].join("\n");
6055
+ case "mix":
6056
+ return [
6057
+ `Form control name cannot mix regular name "{{ name }}" with array brackets "{{ name }}[]"`,
6058
+ trailer,
6059
+ ].join("\n");
6060
+ }
6061
+ }
5942
6062
  }
5943
6063
  class FormDupName extends Rule {
5944
- documentation() {
6064
+ constructor(options) {
6065
+ super({ ...defaults$k, ...options });
6066
+ }
6067
+ static schema() {
5945
6068
  return {
5946
- description: "Each form control must have a unique name.",
6069
+ allowArrayBrackets: {
6070
+ type: "boolean",
6071
+ },
6072
+ shared: {
6073
+ type: "array",
6074
+ items: {
6075
+ enum: ["radio", "checkbox", "submit"],
6076
+ },
6077
+ },
6078
+ };
6079
+ }
6080
+ documentation(context) {
6081
+ return {
6082
+ description: getDocumentation(context),
5947
6083
  url: "https://html-validate.org/rules/form-dup-name.html",
5948
6084
  };
5949
6085
  }
5950
6086
  setup() {
5951
6087
  const selector = this.getSelector();
6088
+ const { shared } = this.options;
5952
6089
  this.on("dom:ready", (event) => {
5953
6090
  var _a, _b;
5954
6091
  const { document } = event;
5955
6092
  const controls = document.querySelectorAll(selector);
5956
- const [sharedControls, uniqueControls] = partition(controls, allowSharedName);
6093
+ const [sharedControls, uniqueControls] = partition(controls, (it) => {
6094
+ return allowSharedName(it, shared);
6095
+ });
5957
6096
  /* validate all form controls which require unique elements first so each
5958
6097
  * form has a populated list of unique names */
5959
6098
  for (const control of uniqueControls) {
@@ -5980,9 +6119,35 @@ class FormDupName extends Rule {
5980
6119
  }
5981
6120
  validateUniqueName(control, form, attr, name) {
5982
6121
  const elements = this.getUniqueElements(form);
6122
+ const { allowArrayBrackets } = this.options;
6123
+ if (allowArrayBrackets) {
6124
+ const isarray = name.endsWith("[]");
6125
+ const basename = isarray ? name.slice(0, -2) : name;
6126
+ const details = elements.get(basename);
6127
+ if (details && details.array !== isarray) {
6128
+ const context = {
6129
+ name: basename,
6130
+ kind: "mix",
6131
+ };
6132
+ this.report({
6133
+ node: control,
6134
+ location: attr.valueLocation,
6135
+ message: 'Cannot mix "{{ name }}[]" and "{{ name }}"',
6136
+ context,
6137
+ });
6138
+ return;
6139
+ }
6140
+ else if (!details && isarray) {
6141
+ elements.set(basename, {
6142
+ array: true,
6143
+ });
6144
+ return;
6145
+ }
6146
+ }
5983
6147
  if (elements.has(name)) {
5984
6148
  const context = {
5985
6149
  name,
6150
+ kind: "duplicate",
5986
6151
  };
5987
6152
  this.report({
5988
6153
  node: control,
@@ -5992,7 +6157,9 @@ class FormDupName extends Rule {
5992
6157
  });
5993
6158
  }
5994
6159
  else {
5995
- elements.add(name);
6160
+ elements.set(name, {
6161
+ array: false,
6162
+ });
5996
6163
  }
5997
6164
  }
5998
6165
  validateSharedName(control, form, attr, name) {
@@ -6005,6 +6172,7 @@ class FormDupName extends Rule {
6005
6172
  (sharedElements.has(name) && sharedElements.get(name) !== type)) {
6006
6173
  const context = {
6007
6174
  name,
6175
+ kind: "duplicate",
6008
6176
  };
6009
6177
  this.report({
6010
6178
  node: control,
@@ -6036,7 +6204,7 @@ class FormDupName extends Rule {
6036
6204
  return existing;
6037
6205
  }
6038
6206
  else {
6039
- const elements = new Set();
6207
+ const elements = new Map();
6040
6208
  form.cacheSet(UNIQUE_CACHE_KEY, elements);
6041
6209
  return elements;
6042
6210
  }
@@ -7566,6 +7734,39 @@ class NoUnknownElements extends Rule {
7566
7734
  }
7567
7735
  }
7568
7736
 
7737
+ class NoUnusedDisable extends Rule {
7738
+ documentation(context) {
7739
+ return {
7740
+ description: context
7741
+ ? `\`${context.ruleId}\` rule is disabled but no error was reported.`
7742
+ : "Rule is disabled but no error was reported.",
7743
+ url: "https://html-validate.org/rules/no-unused-disable.html",
7744
+ };
7745
+ }
7746
+ setup() {
7747
+ /* this is a special rule, the `Engine` class directly emits errors on this
7748
+ * rule, it exists only to be able to configure whenever the rule is enabled
7749
+ * or not and to get the regular documentation and contextual help. */
7750
+ }
7751
+ reportUnused(unused, options, location) {
7752
+ const tokens = new DOMTokenList(options.replace(",", " "), location);
7753
+ for (const ruleId of unused) {
7754
+ const index = tokens.indexOf(ruleId);
7755
+ /* istanbul ignore next: the token should be present or it wouldn't be
7756
+ * reported as unused, this is just a sanity check and fallback */
7757
+ const tokenLocation = index >= 0 ? tokens.location(index) : location;
7758
+ this.report({
7759
+ node: null,
7760
+ message: '"{{ ruleId }}" rule is disabled but no error was reported',
7761
+ location: tokenLocation,
7762
+ context: {
7763
+ ruleId: ruleId,
7764
+ },
7765
+ });
7766
+ }
7767
+ }
7768
+ }
7769
+
7569
7770
  class NoUtf8Bom extends Rule {
7570
7771
  documentation() {
7571
7772
  return {
@@ -9189,6 +9390,7 @@ const bundledRules = {
9189
9390
  "no-style-tag": NoStyleTag,
9190
9391
  "no-trailing-whitespace": NoTrailingWhitespace,
9191
9392
  "no-unknown-elements": NoUnknownElements,
9393
+ "no-unused-disable": NoUnusedDisable,
9192
9394
  "no-utf8-bom": NoUtf8Bom,
9193
9395
  "prefer-button": PreferButton,
9194
9396
  "prefer-native-element": PreferNativeElement,
@@ -9299,6 +9501,7 @@ const config$1 = {
9299
9501
  "no-self-closing": "error",
9300
9502
  "no-trailing-whitespace": "error",
9301
9503
  "no-utf8-bom": "error",
9504
+ "no-unused-disable": "error",
9302
9505
  "prefer-button": "error",
9303
9506
  "prefer-native-element": "error",
9304
9507
  "prefer-tbody": "error",
@@ -9349,6 +9552,7 @@ const config = {
9349
9552
  "no-dup-id": "error",
9350
9553
  "no-multiple-main": "error",
9351
9554
  "no-raw-characters": ["error", { relaxed: true }],
9555
+ "no-unused-disable": "error",
9352
9556
  "script-element": "error",
9353
9557
  "unrecognized-char-ref": "error",
9354
9558
  "valid-id": ["error", { relaxed: true }],
@@ -10148,6 +10352,10 @@ class Parser {
10148
10352
  offset: 0,
10149
10353
  };
10150
10354
  }
10355
+ /* trigger starting event */
10356
+ this.trigger("parse:begin", {
10357
+ location: null,
10358
+ });
10151
10359
  /* reset DOM in case there are multiple calls in the same session */
10152
10360
  this.dom = new DOMTree({
10153
10361
  filename: (_a = source.filename) !== null && _a !== void 0 ? _a : "",
@@ -10182,6 +10390,10 @@ class Parser {
10182
10390
  * instead */
10183
10391
  location: null,
10184
10392
  });
10393
+ /* trigger ending event */
10394
+ this.trigger("parse:end", {
10395
+ location: null,
10396
+ });
10185
10397
  return this.dom.root;
10186
10398
  }
10187
10399
  /**
@@ -10492,11 +10704,11 @@ class Parser {
10492
10704
  };
10493
10705
  }
10494
10706
  consumeDirective(token) {
10495
- const [text, , action, directive, end] = token.data;
10496
- if (end === "") {
10707
+ const [text, preamble, action, separator1, directive, postamble] = token.data;
10708
+ if (!postamble.startsWith("]")) {
10497
10709
  throw new ParserError(token.location, `Missing end bracket "]" on directive "${text}"`);
10498
10710
  }
10499
- const match = directive.match(/^(.*?)(?:\s*(?:--|:)\s*(.*))?$/);
10711
+ const match = directive.match(/^(.*?)(?:(\s*(?:--|:)\s*)(.*))?$/);
10500
10712
  /* istanbul ignore next: should not be possible, would be emitted as comment token */
10501
10713
  if (!match) {
10502
10714
  throw new Error(`Failed to parse directive "${text}"`);
@@ -10504,12 +10716,32 @@ class Parser {
10504
10716
  if (!isValidDirective(action)) {
10505
10717
  throw new ParserError(token.location, `Unknown directive "${action}"`);
10506
10718
  }
10507
- const [, data, comment] = match;
10719
+ const [, data, separator2, comment] = match;
10720
+ const prefix = "html-validate-";
10721
+ /* <!-- [html-validate-action options -- comment] -->
10722
+ * ^ ^ ^--------------- comment offset
10723
+ * | \-------------------------- options offset
10724
+ * \--------------------------------- action offset
10725
+ */
10726
+ const actionOffset = preamble.length;
10727
+ const optionsOffset = actionOffset + action.length + separator1.length;
10728
+ const commentOffset = optionsOffset + data.length + (separator2 || "").length;
10729
+ const location = sliceLocation(token.location, preamble.length - prefix.length - 1, -postamble.length + 1);
10730
+ const actionLocation = sliceLocation(token.location, actionOffset, actionOffset + action.length);
10731
+ const optionsLocation = data
10732
+ ? sliceLocation(token.location, optionsOffset, optionsOffset + data.length)
10733
+ : undefined;
10734
+ const commentLocation = comment
10735
+ ? sliceLocation(token.location, commentOffset, commentOffset + comment.length)
10736
+ : undefined;
10508
10737
  this.trigger("directive", {
10509
10738
  action,
10510
10739
  data,
10511
10740
  comment: comment || "",
10512
- location: token.location,
10741
+ location,
10742
+ actionLocation,
10743
+ optionsLocation,
10744
+ commentLocation,
10513
10745
  });
10514
10746
  }
10515
10747
  /**
@@ -10795,6 +11027,18 @@ function messageSort(a, b) {
10795
11027
  return 0;
10796
11028
  }
10797
11029
 
11030
+ let blockerCounter = 1;
11031
+ /**
11032
+ * Creates a new rule blocker for using when blocking rules from generating
11033
+ * errors.
11034
+ *
11035
+ * @internal
11036
+ */
11037
+ function createBlocker() {
11038
+ const id = blockerCounter++;
11039
+ return id;
11040
+ }
11041
+
10798
11042
  /**
10799
11043
  * @internal
10800
11044
  */
@@ -10822,6 +11066,15 @@ class Engine {
10822
11066
  const parser = this.instantiateParser();
10823
11067
  /* setup plugins and rules */
10824
11068
  const { rules } = this.setupPlugins(source, this.config, parser);
11069
+ const noUnusedDisable = rules["no-unused-disable"];
11070
+ const directiveContext = {
11071
+ rules,
11072
+ reportUnused(unused, options, location) {
11073
+ if (noUnusedDisable) {
11074
+ noUnusedDisable.reportUnused(unused, options, location);
11075
+ }
11076
+ },
11077
+ };
10825
11078
  /* create a faux location at the start of the stream for the next events */
10826
11079
  const location = {
10827
11080
  filename: source.filename,
@@ -10847,7 +11100,7 @@ class Engine {
10847
11100
  parser.trigger("source:ready", sourceEvent);
10848
11101
  /* setup directive handling */
10849
11102
  parser.on("directive", (_, event) => {
10850
- this.processDirective(event, parser, rules);
11103
+ this.processDirective(event, parser, directiveContext);
10851
11104
  });
10852
11105
  /* parse token stream */
10853
11106
  try {
@@ -10954,12 +11207,15 @@ class Engine {
10954
11207
  instantiateParser() {
10955
11208
  return new this.ParserClass(this.config);
10956
11209
  }
10957
- processDirective(event, parser, allRules) {
11210
+ processDirective(event, parser, context) {
11211
+ var _a;
10958
11212
  const rules = event.data
10959
11213
  .split(",")
10960
11214
  .map((name) => name.trim())
10961
- .map((name) => allRules[name])
11215
+ .map((name) => context.rules[name])
10962
11216
  .filter((rule) => rule); /* filter out missing rules */
11217
+ /* istanbul ignore next: option must be present or there would be no rules to disable */
11218
+ const location = (_a = event.optionsLocation) !== null && _a !== void 0 ? _a : event.location;
10963
11219
  switch (event.action) {
10964
11220
  case "enable":
10965
11221
  this.processEnableDirective(rules, parser);
@@ -10968,10 +11224,10 @@ class Engine {
10968
11224
  this.processDisableDirective(rules, parser);
10969
11225
  break;
10970
11226
  case "disable-block":
10971
- this.processDisableBlockDirective(rules, parser);
11227
+ this.processDisableBlockDirective(context, rules, parser, event.data, location);
10972
11228
  break;
10973
11229
  case "disable-next":
10974
- this.processDisableNextDirective(rules, parser);
11230
+ this.processDisableNextDirective(context, rules, parser, event.data, location);
10975
11231
  break;
10976
11232
  }
10977
11233
  }
@@ -10996,10 +11252,13 @@ class Engine {
10996
11252
  data.target.disableRules(rules.map((rule) => rule.name));
10997
11253
  });
10998
11254
  }
10999
- processDisableBlockDirective(rules, parser) {
11255
+ processDisableBlockDirective(context, rules, parser, options, location) {
11256
+ const ruleIds = rules.map((it) => it.name);
11257
+ const blocker = createBlocker();
11258
+ const unused = new Set(ruleIds);
11000
11259
  let directiveBlock = null;
11001
11260
  for (const rule of rules) {
11002
- rule.setEnabled(false);
11261
+ rule.block(blocker);
11003
11262
  }
11004
11263
  const unregisterOpen = parser.on("tag:start", (event, data) => {
11005
11264
  var _a, _b;
@@ -11011,7 +11270,7 @@ class Engine {
11011
11270
  }
11012
11271
  /* disable rules directly on the node so it will be recorded for later,
11013
11272
  * more specifically when using the domtree to trigger errors */
11014
- data.target.disableRules(rules.map((rule) => rule.name));
11273
+ data.target.blockRules(ruleIds, blocker);
11015
11274
  });
11016
11275
  const unregisterClose = parser.on("tag:end", (event, data) => {
11017
11276
  /* if the directive is the last thing in a block no id would be set */
@@ -11024,26 +11283,45 @@ class Engine {
11024
11283
  unregisterClose();
11025
11284
  unregisterOpen();
11026
11285
  for (const rule of rules) {
11027
- rule.setEnabled(true);
11286
+ rule.unblock(blocker);
11028
11287
  }
11029
11288
  }
11030
11289
  });
11290
+ parser.on("rule:error", (event, data) => {
11291
+ if (data.blockers.includes(blocker)) {
11292
+ unused.delete(data.ruleId);
11293
+ }
11294
+ });
11295
+ parser.on("parse:end", () => {
11296
+ context.reportUnused(unused, options, location);
11297
+ });
11031
11298
  }
11032
- processDisableNextDirective(rules, parser) {
11299
+ processDisableNextDirective(context, rules, parser, options, location) {
11300
+ const ruleIds = rules.map((it) => it.name);
11301
+ const blocker = createBlocker();
11302
+ const unused = new Set(ruleIds);
11033
11303
  for (const rule of rules) {
11034
- rule.setEnabled(false);
11304
+ rule.block(blocker);
11035
11305
  }
11036
- /* disable rules directly on the node so it will be recorded for later,
11306
+ /* block rules directly on the node so it will be recorded for later,
11037
11307
  * more specifically when using the domtree to trigger errors */
11038
11308
  const unregister = parser.on("tag:start", (event, data) => {
11039
- data.target.disableRules(rules.map((rule) => rule.name));
11309
+ data.target.blockRules(ruleIds, blocker);
11310
+ });
11311
+ parser.on("rule:error", (event, data) => {
11312
+ if (data.blockers.includes(blocker)) {
11313
+ unused.delete(data.ruleId);
11314
+ }
11315
+ });
11316
+ parser.on("parse:end", () => {
11317
+ context.reportUnused(unused, options, location);
11040
11318
  });
11041
11319
  /* disable directive after next event occurs */
11042
11320
  parser.once("tag:ready, tag:end, attr", () => {
11043
11321
  unregister();
11044
11322
  parser.defer(() => {
11045
11323
  for (const rule of rules) {
11046
- rule.setEnabled(true);
11324
+ rule.unblock(blocker);
11047
11325
  }
11048
11326
  });
11049
11327
  });
@@ -11435,7 +11713,7 @@ class HtmlValidate {
11435
11713
  /** @public */
11436
11714
  const name = "html-validate";
11437
11715
  /** @public */
11438
- const version = "7.12.1";
11716
+ const version = "7.13.0";
11439
11717
  /** @public */
11440
11718
  const bugs = "https://gitlab.com/html-validate/html-validate/issues/new";
11441
11719
 
@@ -11878,5 +12156,5 @@ function getFormatter(name) {
11878
12156
  return (_a = availableFormatters[name]) !== null && _a !== void 0 ? _a : null;
11879
12157
  }
11880
12158
 
11881
- export { Attribute as A, Config as C, DynamicValue as D, EventHandler as E, FileSystemConfigLoader as F, HtmlValidate as H, MetaTable as M, NodeClosed as N, Parser as P, Rule as R, Severity as S, TextNode as T, UserError as U, WrappedError as W, ConfigError as a, ConfigLoader as b, StaticConfigLoader as c, HtmlElement as d, SchemaValidationError as e, NestedError as f, MetaCopyableProperty as g, Reporter as h, TemplateExtractor as i, definePlugin as j, getFormatter as k, legacyRequire as l, ensureError as m, configDataFromFile as n, compatibilityCheck as o, presets as p, codeframe as q, ruleExists as r, sliceLocation as s, isTextNode as t, isElementNode as u, version as v, generateIdSelector as w, name as x, bugs as y };
12159
+ export { Attribute as A, Config as C, DynamicValue as D, EventHandler as E, FileSystemConfigLoader as F, HtmlValidate as H, MetaTable as M, NodeClosed as N, Parser as P, Rule as R, Severity as S, TextNode as T, UserError as U, Validator as V, WrappedError as W, ConfigError as a, ConfigLoader as b, StaticConfigLoader as c, HtmlElement as d, SchemaValidationError as e, NestedError as f, MetaCopyableProperty as g, Reporter as h, TemplateExtractor as i, definePlugin as j, getFormatter as k, legacyRequire as l, ensureError as m, configDataFromFile as n, compatibilityCheck as o, presets as p, codeframe as q, ruleExists as r, sliceLocation as s, isTextNode as t, isElementNode as u, version as v, generateIdSelector as w, name as x, bugs as y };
11882
12160
  //# sourceMappingURL=core.js.map