html-validate 7.12.2 → 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
3490
3555
  */
3491
- isEnabled() {
3492
- return this.enabled && this.severity >= exports.Severity.WARN;
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
3566
+ */
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
  }
@@ -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 {
@@ -7668,6 +7766,39 @@ class NoUnknownElements extends Rule {
7668
7766
  }
7669
7767
  }
7670
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
+
7671
7802
  class NoUtf8Bom extends Rule {
7672
7803
  documentation() {
7673
7804
  return {
@@ -9291,6 +9422,7 @@ const bundledRules = {
9291
9422
  "no-style-tag": NoStyleTag,
9292
9423
  "no-trailing-whitespace": NoTrailingWhitespace,
9293
9424
  "no-unknown-elements": NoUnknownElements,
9425
+ "no-unused-disable": NoUnusedDisable,
9294
9426
  "no-utf8-bom": NoUtf8Bom,
9295
9427
  "prefer-button": PreferButton,
9296
9428
  "prefer-native-element": PreferNativeElement,
@@ -9401,6 +9533,7 @@ const config$1 = {
9401
9533
  "no-self-closing": "error",
9402
9534
  "no-trailing-whitespace": "error",
9403
9535
  "no-utf8-bom": "error",
9536
+ "no-unused-disable": "error",
9404
9537
  "prefer-button": "error",
9405
9538
  "prefer-native-element": "error",
9406
9539
  "prefer-tbody": "error",
@@ -9451,6 +9584,7 @@ const config = {
9451
9584
  "no-dup-id": "error",
9452
9585
  "no-multiple-main": "error",
9453
9586
  "no-raw-characters": ["error", { relaxed: true }],
9587
+ "no-unused-disable": "error",
9454
9588
  "script-element": "error",
9455
9589
  "unrecognized-char-ref": "error",
9456
9590
  "valid-id": ["error", { relaxed: true }],
@@ -10250,6 +10384,10 @@ class Parser {
10250
10384
  offset: 0,
10251
10385
  };
10252
10386
  }
10387
+ /* trigger starting event */
10388
+ this.trigger("parse:begin", {
10389
+ location: null,
10390
+ });
10253
10391
  /* reset DOM in case there are multiple calls in the same session */
10254
10392
  this.dom = new DOMTree({
10255
10393
  filename: (_a = source.filename) !== null && _a !== void 0 ? _a : "",
@@ -10284,6 +10422,10 @@ class Parser {
10284
10422
  * instead */
10285
10423
  location: null,
10286
10424
  });
10425
+ /* trigger ending event */
10426
+ this.trigger("parse:end", {
10427
+ location: null,
10428
+ });
10287
10429
  return this.dom.root;
10288
10430
  }
10289
10431
  /**
@@ -10594,11 +10736,11 @@ class Parser {
10594
10736
  };
10595
10737
  }
10596
10738
  consumeDirective(token) {
10597
- const [text, , action, directive, end] = token.data;
10598
- if (end === "") {
10739
+ const [text, preamble, action, separator1, directive, postamble] = token.data;
10740
+ if (!postamble.startsWith("]")) {
10599
10741
  throw new ParserError(token.location, `Missing end bracket "]" on directive "${text}"`);
10600
10742
  }
10601
- const match = directive.match(/^(.*?)(?:\s*(?:--|:)\s*(.*))?$/);
10743
+ const match = directive.match(/^(.*?)(?:(\s*(?:--|:)\s*)(.*))?$/);
10602
10744
  /* istanbul ignore next: should not be possible, would be emitted as comment token */
10603
10745
  if (!match) {
10604
10746
  throw new Error(`Failed to parse directive "${text}"`);
@@ -10606,12 +10748,32 @@ class Parser {
10606
10748
  if (!isValidDirective(action)) {
10607
10749
  throw new ParserError(token.location, `Unknown directive "${action}"`);
10608
10750
  }
10609
- 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;
10610
10769
  this.trigger("directive", {
10611
10770
  action,
10612
10771
  data,
10613
10772
  comment: comment || "",
10614
- location: token.location,
10773
+ location,
10774
+ actionLocation,
10775
+ optionsLocation,
10776
+ commentLocation,
10615
10777
  });
10616
10778
  }
10617
10779
  /**
@@ -10897,6 +11059,18 @@ function messageSort(a, b) {
10897
11059
  return 0;
10898
11060
  }
10899
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
+
10900
11074
  /**
10901
11075
  * @internal
10902
11076
  */
@@ -10924,6 +11098,15 @@ class Engine {
10924
11098
  const parser = this.instantiateParser();
10925
11099
  /* setup plugins and rules */
10926
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
+ };
10927
11110
  /* create a faux location at the start of the stream for the next events */
10928
11111
  const location = {
10929
11112
  filename: source.filename,
@@ -10949,7 +11132,7 @@ class Engine {
10949
11132
  parser.trigger("source:ready", sourceEvent);
10950
11133
  /* setup directive handling */
10951
11134
  parser.on("directive", (_, event) => {
10952
- this.processDirective(event, parser, rules);
11135
+ this.processDirective(event, parser, directiveContext);
10953
11136
  });
10954
11137
  /* parse token stream */
10955
11138
  try {
@@ -11056,12 +11239,15 @@ class Engine {
11056
11239
  instantiateParser() {
11057
11240
  return new this.ParserClass(this.config);
11058
11241
  }
11059
- processDirective(event, parser, allRules) {
11242
+ processDirective(event, parser, context) {
11243
+ var _a;
11060
11244
  const rules = event.data
11061
11245
  .split(",")
11062
11246
  .map((name) => name.trim())
11063
- .map((name) => allRules[name])
11247
+ .map((name) => context.rules[name])
11064
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;
11065
11251
  switch (event.action) {
11066
11252
  case "enable":
11067
11253
  this.processEnableDirective(rules, parser);
@@ -11070,10 +11256,10 @@ class Engine {
11070
11256
  this.processDisableDirective(rules, parser);
11071
11257
  break;
11072
11258
  case "disable-block":
11073
- this.processDisableBlockDirective(rules, parser);
11259
+ this.processDisableBlockDirective(context, rules, parser, event.data, location);
11074
11260
  break;
11075
11261
  case "disable-next":
11076
- this.processDisableNextDirective(rules, parser);
11262
+ this.processDisableNextDirective(context, rules, parser, event.data, location);
11077
11263
  break;
11078
11264
  }
11079
11265
  }
@@ -11098,10 +11284,13 @@ class Engine {
11098
11284
  data.target.disableRules(rules.map((rule) => rule.name));
11099
11285
  });
11100
11286
  }
11101
- 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);
11102
11291
  let directiveBlock = null;
11103
11292
  for (const rule of rules) {
11104
- rule.setEnabled(false);
11293
+ rule.block(blocker);
11105
11294
  }
11106
11295
  const unregisterOpen = parser.on("tag:start", (event, data) => {
11107
11296
  var _a, _b;
@@ -11113,7 +11302,7 @@ class Engine {
11113
11302
  }
11114
11303
  /* disable rules directly on the node so it will be recorded for later,
11115
11304
  * more specifically when using the domtree to trigger errors */
11116
- data.target.disableRules(rules.map((rule) => rule.name));
11305
+ data.target.blockRules(ruleIds, blocker);
11117
11306
  });
11118
11307
  const unregisterClose = parser.on("tag:end", (event, data) => {
11119
11308
  /* if the directive is the last thing in a block no id would be set */
@@ -11126,26 +11315,45 @@ class Engine {
11126
11315
  unregisterClose();
11127
11316
  unregisterOpen();
11128
11317
  for (const rule of rules) {
11129
- rule.setEnabled(true);
11318
+ rule.unblock(blocker);
11130
11319
  }
11131
11320
  }
11132
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
+ });
11133
11330
  }
11134
- 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);
11135
11335
  for (const rule of rules) {
11136
- rule.setEnabled(false);
11336
+ rule.block(blocker);
11137
11337
  }
11138
- /* 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,
11139
11339
  * more specifically when using the domtree to trigger errors */
11140
11340
  const unregister = parser.on("tag:start", (event, data) => {
11141
- 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);
11142
11350
  });
11143
11351
  /* disable directive after next event occurs */
11144
11352
  parser.once("tag:ready, tag:end, attr", () => {
11145
11353
  unregister();
11146
11354
  parser.defer(() => {
11147
11355
  for (const rule of rules) {
11148
- rule.setEnabled(true);
11356
+ rule.unblock(blocker);
11149
11357
  }
11150
11358
  });
11151
11359
  });
@@ -11537,7 +11745,7 @@ class HtmlValidate {
11537
11745
  /** @public */
11538
11746
  const name = "html-validate";
11539
11747
  /** @public */
11540
- const version = "7.12.2";
11748
+ const version = "7.13.0";
11541
11749
  /** @public */
11542
11750
  const bugs = "https://gitlab.com/html-validate/html-validate/issues/new";
11543
11751
 
@@ -12000,6 +12208,7 @@ exports.StaticConfigLoader = StaticConfigLoader;
12000
12208
  exports.TemplateExtractor = TemplateExtractor;
12001
12209
  exports.TextNode = TextNode;
12002
12210
  exports.UserError = UserError;
12211
+ exports.Validator = Validator;
12003
12212
  exports.WrappedError = WrappedError;
12004
12213
  exports.bugs = bugs;
12005
12214
  exports.codeframe = codeframe;