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/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
3458
3523
  */
3459
- isEnabled() {
3460
- return this.enabled && this.severity >= Severity.WARN;
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
3534
+ */
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
  }
@@ -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 {
@@ -7636,6 +7734,39 @@ class NoUnknownElements extends Rule {
7636
7734
  }
7637
7735
  }
7638
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
+
7639
7770
  class NoUtf8Bom extends Rule {
7640
7771
  documentation() {
7641
7772
  return {
@@ -9259,6 +9390,7 @@ const bundledRules = {
9259
9390
  "no-style-tag": NoStyleTag,
9260
9391
  "no-trailing-whitespace": NoTrailingWhitespace,
9261
9392
  "no-unknown-elements": NoUnknownElements,
9393
+ "no-unused-disable": NoUnusedDisable,
9262
9394
  "no-utf8-bom": NoUtf8Bom,
9263
9395
  "prefer-button": PreferButton,
9264
9396
  "prefer-native-element": PreferNativeElement,
@@ -9369,6 +9501,7 @@ const config$1 = {
9369
9501
  "no-self-closing": "error",
9370
9502
  "no-trailing-whitespace": "error",
9371
9503
  "no-utf8-bom": "error",
9504
+ "no-unused-disable": "error",
9372
9505
  "prefer-button": "error",
9373
9506
  "prefer-native-element": "error",
9374
9507
  "prefer-tbody": "error",
@@ -9419,6 +9552,7 @@ const config = {
9419
9552
  "no-dup-id": "error",
9420
9553
  "no-multiple-main": "error",
9421
9554
  "no-raw-characters": ["error", { relaxed: true }],
9555
+ "no-unused-disable": "error",
9422
9556
  "script-element": "error",
9423
9557
  "unrecognized-char-ref": "error",
9424
9558
  "valid-id": ["error", { relaxed: true }],
@@ -10218,6 +10352,10 @@ class Parser {
10218
10352
  offset: 0,
10219
10353
  };
10220
10354
  }
10355
+ /* trigger starting event */
10356
+ this.trigger("parse:begin", {
10357
+ location: null,
10358
+ });
10221
10359
  /* reset DOM in case there are multiple calls in the same session */
10222
10360
  this.dom = new DOMTree({
10223
10361
  filename: (_a = source.filename) !== null && _a !== void 0 ? _a : "",
@@ -10252,6 +10390,10 @@ class Parser {
10252
10390
  * instead */
10253
10391
  location: null,
10254
10392
  });
10393
+ /* trigger ending event */
10394
+ this.trigger("parse:end", {
10395
+ location: null,
10396
+ });
10255
10397
  return this.dom.root;
10256
10398
  }
10257
10399
  /**
@@ -10562,11 +10704,11 @@ class Parser {
10562
10704
  };
10563
10705
  }
10564
10706
  consumeDirective(token) {
10565
- const [text, , action, directive, end] = token.data;
10566
- if (end === "") {
10707
+ const [text, preamble, action, separator1, directive, postamble] = token.data;
10708
+ if (!postamble.startsWith("]")) {
10567
10709
  throw new ParserError(token.location, `Missing end bracket "]" on directive "${text}"`);
10568
10710
  }
10569
- const match = directive.match(/^(.*?)(?:\s*(?:--|:)\s*(.*))?$/);
10711
+ const match = directive.match(/^(.*?)(?:(\s*(?:--|:)\s*)(.*))?$/);
10570
10712
  /* istanbul ignore next: should not be possible, would be emitted as comment token */
10571
10713
  if (!match) {
10572
10714
  throw new Error(`Failed to parse directive "${text}"`);
@@ -10574,12 +10716,32 @@ class Parser {
10574
10716
  if (!isValidDirective(action)) {
10575
10717
  throw new ParserError(token.location, `Unknown directive "${action}"`);
10576
10718
  }
10577
- 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;
10578
10737
  this.trigger("directive", {
10579
10738
  action,
10580
10739
  data,
10581
10740
  comment: comment || "",
10582
- location: token.location,
10741
+ location,
10742
+ actionLocation,
10743
+ optionsLocation,
10744
+ commentLocation,
10583
10745
  });
10584
10746
  }
10585
10747
  /**
@@ -10865,6 +11027,18 @@ function messageSort(a, b) {
10865
11027
  return 0;
10866
11028
  }
10867
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
+
10868
11042
  /**
10869
11043
  * @internal
10870
11044
  */
@@ -10892,6 +11066,15 @@ class Engine {
10892
11066
  const parser = this.instantiateParser();
10893
11067
  /* setup plugins and rules */
10894
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
+ };
10895
11078
  /* create a faux location at the start of the stream for the next events */
10896
11079
  const location = {
10897
11080
  filename: source.filename,
@@ -10917,7 +11100,7 @@ class Engine {
10917
11100
  parser.trigger("source:ready", sourceEvent);
10918
11101
  /* setup directive handling */
10919
11102
  parser.on("directive", (_, event) => {
10920
- this.processDirective(event, parser, rules);
11103
+ this.processDirective(event, parser, directiveContext);
10921
11104
  });
10922
11105
  /* parse token stream */
10923
11106
  try {
@@ -11024,12 +11207,15 @@ class Engine {
11024
11207
  instantiateParser() {
11025
11208
  return new this.ParserClass(this.config);
11026
11209
  }
11027
- processDirective(event, parser, allRules) {
11210
+ processDirective(event, parser, context) {
11211
+ var _a;
11028
11212
  const rules = event.data
11029
11213
  .split(",")
11030
11214
  .map((name) => name.trim())
11031
- .map((name) => allRules[name])
11215
+ .map((name) => context.rules[name])
11032
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;
11033
11219
  switch (event.action) {
11034
11220
  case "enable":
11035
11221
  this.processEnableDirective(rules, parser);
@@ -11038,10 +11224,10 @@ class Engine {
11038
11224
  this.processDisableDirective(rules, parser);
11039
11225
  break;
11040
11226
  case "disable-block":
11041
- this.processDisableBlockDirective(rules, parser);
11227
+ this.processDisableBlockDirective(context, rules, parser, event.data, location);
11042
11228
  break;
11043
11229
  case "disable-next":
11044
- this.processDisableNextDirective(rules, parser);
11230
+ this.processDisableNextDirective(context, rules, parser, event.data, location);
11045
11231
  break;
11046
11232
  }
11047
11233
  }
@@ -11066,10 +11252,13 @@ class Engine {
11066
11252
  data.target.disableRules(rules.map((rule) => rule.name));
11067
11253
  });
11068
11254
  }
11069
- 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);
11070
11259
  let directiveBlock = null;
11071
11260
  for (const rule of rules) {
11072
- rule.setEnabled(false);
11261
+ rule.block(blocker);
11073
11262
  }
11074
11263
  const unregisterOpen = parser.on("tag:start", (event, data) => {
11075
11264
  var _a, _b;
@@ -11081,7 +11270,7 @@ class Engine {
11081
11270
  }
11082
11271
  /* disable rules directly on the node so it will be recorded for later,
11083
11272
  * more specifically when using the domtree to trigger errors */
11084
- data.target.disableRules(rules.map((rule) => rule.name));
11273
+ data.target.blockRules(ruleIds, blocker);
11085
11274
  });
11086
11275
  const unregisterClose = parser.on("tag:end", (event, data) => {
11087
11276
  /* if the directive is the last thing in a block no id would be set */
@@ -11094,26 +11283,45 @@ class Engine {
11094
11283
  unregisterClose();
11095
11284
  unregisterOpen();
11096
11285
  for (const rule of rules) {
11097
- rule.setEnabled(true);
11286
+ rule.unblock(blocker);
11098
11287
  }
11099
11288
  }
11100
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
+ });
11101
11298
  }
11102
- 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);
11103
11303
  for (const rule of rules) {
11104
- rule.setEnabled(false);
11304
+ rule.block(blocker);
11105
11305
  }
11106
- /* 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,
11107
11307
  * more specifically when using the domtree to trigger errors */
11108
11308
  const unregister = parser.on("tag:start", (event, data) => {
11109
- 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);
11110
11318
  });
11111
11319
  /* disable directive after next event occurs */
11112
11320
  parser.once("tag:ready, tag:end, attr", () => {
11113
11321
  unregister();
11114
11322
  parser.defer(() => {
11115
11323
  for (const rule of rules) {
11116
- rule.setEnabled(true);
11324
+ rule.unblock(blocker);
11117
11325
  }
11118
11326
  });
11119
11327
  });
@@ -11505,7 +11713,7 @@ class HtmlValidate {
11505
11713
  /** @public */
11506
11714
  const name = "html-validate";
11507
11715
  /** @public */
11508
- const version = "7.12.2";
11716
+ const version = "7.13.0";
11509
11717
  /** @public */
11510
11718
  const bugs = "https://gitlab.com/html-validate/html-validate/issues/new";
11511
11719
 
@@ -11948,5 +12156,5 @@ function getFormatter(name) {
11948
12156
  return (_a = availableFormatters[name]) !== null && _a !== void 0 ? _a : null;
11949
12157
  }
11950
12158
 
11951
- 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 };
11952
12160
  //# sourceMappingURL=core.js.map