html-validate 10.4.0 → 10.6.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/esm/core.js CHANGED
@@ -1207,7 +1207,9 @@ class MetaTable {
1207
1207
  );
1208
1208
  }
1209
1209
  for (const [key, value] of Object.entries(obj)) {
1210
- if (key === "$schema") continue;
1210
+ if (key === "$schema") {
1211
+ continue;
1212
+ }
1211
1213
  this.addEntry(key, migrateElement(value));
1212
1214
  }
1213
1215
  } catch (err) {
@@ -1307,7 +1309,9 @@ class MetaTable {
1307
1309
  * global, e.g. to assign global attributes.
1308
1310
  */
1309
1311
  resolveGlobal() {
1310
- if (!this.elements["*"]) return;
1312
+ if (!this.elements["*"]) {
1313
+ return;
1314
+ }
1311
1315
  const global = this.elements["*"];
1312
1316
  delete this.elements["*"];
1313
1317
  delete global.tagName;
@@ -1481,7 +1485,9 @@ function sliceSize(size, begin, end) {
1481
1485
  return Math.min(size, end - begin);
1482
1486
  }
1483
1487
  function sliceLocation(location, begin, end, wrap) {
1484
- if (!location) return null;
1488
+ if (!location) {
1489
+ return null;
1490
+ }
1485
1491
  const size = sliceSize(location.size, begin, end);
1486
1492
  const sliced = {
1487
1493
  filename: location.filename,
@@ -1590,7 +1596,7 @@ var NodeType = /* @__PURE__ */ ((NodeType2) => {
1590
1596
  })(NodeType || {});
1591
1597
 
1592
1598
  const DOCUMENT_NODE_NAME = "#document";
1593
- const TEXT_CONTENT = Symbol("textContent");
1599
+ const TEXT_CONTENT = /* @__PURE__ */ Symbol("textContent");
1594
1600
  let counter = 0;
1595
1601
  class DOMNode {
1596
1602
  nodeName;
@@ -2360,7 +2366,7 @@ class Selector {
2360
2366
 
2361
2367
  const TEXT_NODE_NAME = "#text";
2362
2368
  function isTextNode(node) {
2363
- return Boolean(node && node.nodeType === NodeType.TEXT_NODE);
2369
+ return node?.nodeType === NodeType.TEXT_NODE;
2364
2370
  }
2365
2371
  class TextNode extends DOMNode {
2366
2372
  text;
@@ -2393,8 +2399,8 @@ class TextNode extends DOMNode {
2393
2399
  }
2394
2400
  }
2395
2401
 
2396
- const ROLE = Symbol("role");
2397
- const TABINDEX = Symbol("tabindex");
2402
+ const ROLE = /* @__PURE__ */ Symbol("role");
2403
+ const TABINDEX = /* @__PURE__ */ Symbol("tabindex");
2398
2404
  var NodeClosed = /* @__PURE__ */ ((NodeClosed2) => {
2399
2405
  NodeClosed2[NodeClosed2["Open"] = 0] = "Open";
2400
2406
  NodeClosed2[NodeClosed2["EndTag"] = 1] = "EndTag";
@@ -2404,7 +2410,7 @@ var NodeClosed = /* @__PURE__ */ ((NodeClosed2) => {
2404
2410
  return NodeClosed2;
2405
2411
  })(NodeClosed || {});
2406
2412
  function isElementNode(node) {
2407
- return Boolean(node && node.nodeType === NodeType.ELEMENT_NODE);
2413
+ return node?.nodeType === NodeType.ELEMENT_NODE;
2408
2414
  }
2409
2415
  function isInvalidTagName(tagName) {
2410
2416
  return tagName === "" || tagName === "*";
@@ -3350,7 +3356,7 @@ function parseSeverity(value) {
3350
3356
  }
3351
3357
  }
3352
3358
 
3353
- const cacheKey = Symbol("aria-naming");
3359
+ const cacheKey = /* @__PURE__ */ Symbol("aria-naming");
3354
3360
  const defaultValue = "allowed";
3355
3361
  const prohibitedRoles = [
3356
3362
  "caption",
@@ -3569,10 +3575,10 @@ function isPresentation(node) {
3569
3575
  }
3570
3576
 
3571
3577
  const cachePrefix = classifyNodeText.name;
3572
- const HTML_CACHE_KEY = Symbol(`${cachePrefix}|html`);
3573
- const A11Y_CACHE_KEY = Symbol(`${cachePrefix}|a11y`);
3574
- const IGNORE_HIDDEN_ROOT_HTML_CACHE_KEY = Symbol(`${cachePrefix}|html|ignore-hidden-root`);
3575
- const IGNORE_HIDDEN_ROOT_A11Y_CACHE_KEY = Symbol(`${cachePrefix}|a11y|ignore-hidden-root`);
3578
+ const HTML_CACHE_KEY = /* @__PURE__ */ Symbol(`${cachePrefix}|html`);
3579
+ const A11Y_CACHE_KEY = /* @__PURE__ */ Symbol(`${cachePrefix}|a11y`);
3580
+ const IGNORE_HIDDEN_ROOT_HTML_CACHE_KEY = /* @__PURE__ */ Symbol(`${cachePrefix}|html|ignore-hidden-root`);
3581
+ const IGNORE_HIDDEN_ROOT_A11Y_CACHE_KEY = /* @__PURE__ */ Symbol(`${cachePrefix}|a11y|ignore-hidden-root`);
3576
3582
  var TextClassification = /* @__PURE__ */ ((TextClassification2) => {
3577
3583
  TextClassification2[TextClassification2["EMPTY_TEXT"] = 0] = "EMPTY_TEXT";
3578
3584
  TextClassification2[TextClassification2["DYNAMIC_TEXT"] = 1] = "DYNAMIC_TEXT";
@@ -3700,6 +3706,7 @@ function interpolate(text, data) {
3700
3706
 
3701
3707
  const ajv$1 = new Ajv({ strict: true, strictTuples: true, strictTypes: true });
3702
3708
  ajv$1.addMetaSchema(ajvSchemaDraft);
3709
+ ajv$1.addKeyword(ajvRegexpKeyword);
3703
3710
  function getSchemaValidator(ruleId, properties) {
3704
3711
  const $id = `rule/${ruleId}`;
3705
3712
  const cached = ajv$1.getSchema($id);
@@ -3956,6 +3963,7 @@ class Rule {
3956
3963
  *
3957
3964
  * @internal
3958
3965
  */
3966
+ /* eslint-disable-next-line @typescript-eslint/max-params -- technical debt */
3959
3967
  static validateOptions(cls, ruleId, jsonPath, options, filename, config) {
3960
3968
  if (!cls) {
3961
3969
  return;
@@ -5180,7 +5188,9 @@ class AttributeAllowedValues extends Rule {
5180
5188
  const doc = event.document;
5181
5189
  walk.depthFirst(doc, (node) => {
5182
5190
  const meta = node.meta;
5183
- if (!meta?.attributes) return;
5191
+ if (!meta?.attributes) {
5192
+ return;
5193
+ }
5184
5194
  for (const attr of node.attributes) {
5185
5195
  if (Validator.validateAttribute(attr, meta.attributes)) {
5186
5196
  continue;
@@ -5240,9 +5250,13 @@ class AttributeBooleanStyle extends Rule {
5240
5250
  const doc = event.document;
5241
5251
  walk.depthFirst(doc, (node) => {
5242
5252
  const meta = node.meta;
5243
- if (!meta?.attributes) return;
5253
+ if (!meta?.attributes) {
5254
+ return;
5255
+ }
5244
5256
  for (const attr of node.attributes) {
5245
- if (!this.isBoolean(attr, meta.attributes)) continue;
5257
+ if (!this.isBoolean(attr, meta.attributes)) {
5258
+ continue;
5259
+ }
5246
5260
  if (attr.originalAttribute) {
5247
5261
  continue;
5248
5262
  }
@@ -5312,7 +5326,9 @@ class AttributeEmptyStyle extends Rule {
5312
5326
  const doc = event.document;
5313
5327
  walk.depthFirst(doc, (node) => {
5314
5328
  const meta = node.meta;
5315
- if (!meta?.attributes) return;
5329
+ if (!meta?.attributes) {
5330
+ return;
5331
+ }
5316
5332
  for (const attr of node.attributes) {
5317
5333
  if (!allowsEmpty(attr, meta.attributes)) {
5318
5334
  continue;
@@ -5405,7 +5421,22 @@ class AttributeMisuse extends Rule {
5405
5421
  }
5406
5422
  }
5407
5423
 
5424
+ const patternNamesValues = [
5425
+ "kebabcase",
5426
+ "camelcase",
5427
+ "underscore",
5428
+ "snakecase",
5429
+ "bem",
5430
+ "tailwind"
5431
+ ];
5432
+ const patternNames = new Set(patternNamesValues);
5433
+ function isNamedPattern(value) {
5434
+ return typeof value === "string" && patternNames.has(value);
5435
+ }
5408
5436
  function parsePattern(pattern) {
5437
+ if (pattern instanceof RegExp) {
5438
+ return { regexp: pattern, description: pattern.toString() };
5439
+ }
5409
5440
  switch (pattern) {
5410
5441
  case "kebabcase":
5411
5442
  return { regexp: /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/, description: pattern };
@@ -5423,7 +5454,21 @@ function parsePattern(pattern) {
5423
5454
  description: pattern
5424
5455
  };
5425
5456
  }
5457
+ case "tailwind": {
5458
+ return {
5459
+ regexp: /^!?(?:[-a-z[]|\d+xl:)[\w\-:./\\[\]()#'&>,!=%]*$/,
5460
+ description: "tailwind"
5461
+ };
5462
+ }
5426
5463
  default: {
5464
+ if (pattern.startsWith("/") && pattern.endsWith("/")) {
5465
+ const regexpSource = pattern.slice(1, -1);
5466
+ const regexp2 = new RegExp(regexpSource);
5467
+ return { regexp: regexp2, description: regexp2.toString() };
5468
+ }
5469
+ console.warn(
5470
+ `Custom pattern "${pattern}" should be wrapped in forward slashes, e.g., "/${pattern}/". Support for unwrapped patterns is deprecated and will be removed in a future version.`
5471
+ );
5427
5472
  const regexp = new RegExp(pattern);
5428
5473
  return { regexp, description: regexp.toString() };
5429
5474
  }
@@ -5431,27 +5476,49 @@ function parsePattern(pattern) {
5431
5476
  }
5432
5477
 
5433
5478
  function toArray$2(value) {
5434
- return Array.isArray(value) ? value : [value];
5479
+ if (Array.isArray(value)) {
5480
+ return value;
5481
+ } else {
5482
+ return [value];
5483
+ }
5484
+ }
5485
+ function validateAllowedPatterns(patterns, allowedPatterns, ruleId) {
5486
+ const extraneous = patterns.filter(isNamedPattern).filter((p) => !allowedPatterns.has(p));
5487
+ if (extraneous.length > 0) {
5488
+ const quote = (it) => `"${it}"`;
5489
+ const disallowed = naturalJoin(extraneous.map(quote), "and");
5490
+ const allowed = naturalJoin(Array.from(allowedPatterns, quote), "and");
5491
+ throw new Error(
5492
+ `Pattern ${disallowed} cannot be used with "${ruleId}". Allowed patterns: ${allowed}`
5493
+ );
5494
+ }
5435
5495
  }
5436
5496
  class BasePatternRule extends Rule {
5437
5497
  /** Attribute being tested */
5438
5498
  attr;
5439
5499
  /** Parsed configured patterns */
5440
5500
  patterns;
5441
- /**
5442
- * @param attr - Attribute holding the value.
5443
- * @param options - Rule options with defaults expanded.
5444
- */
5445
- constructor(attr, options) {
5501
+ constructor({
5502
+ ruleId,
5503
+ attr,
5504
+ options,
5505
+ allowedPatterns
5506
+ }) {
5446
5507
  super(options);
5447
5508
  const { pattern } = this.options;
5448
5509
  this.attr = attr;
5449
- this.patterns = toArray$2(pattern).map((it) => parsePattern(it));
5510
+ const patterns = toArray$2(pattern);
5511
+ validateAllowedPatterns(patterns, allowedPatterns, ruleId);
5512
+ this.patterns = patterns.map((it) => parsePattern(it));
5450
5513
  }
5451
5514
  static schema() {
5452
5515
  return {
5453
5516
  pattern: {
5454
- oneOf: [{ type: "array", items: { type: "string" }, minItems: 1 }, { type: "string" }]
5517
+ anyOf: [
5518
+ { type: "array", items: { anyOf: [{ type: "string" }, { regexp: true }] }, minItems: 1 },
5519
+ { type: "string" },
5520
+ { regexp: true }
5521
+ ]
5455
5522
  }
5456
5523
  };
5457
5524
  }
@@ -5490,7 +5557,13 @@ const defaults$q = {
5490
5557
  };
5491
5558
  class ClassPattern extends BasePatternRule {
5492
5559
  constructor(options) {
5493
- super("class", { ...defaults$q, ...options });
5560
+ super({
5561
+ ruleId: "class-pattern",
5562
+ attr: "class",
5563
+ options: { ...defaults$q, ...options },
5564
+ allowedPatterns: patternNames
5565
+ // allow all patterns
5566
+ });
5494
5567
  }
5495
5568
  static schema() {
5496
5569
  return BasePatternRule.schema();
@@ -6440,7 +6513,9 @@ class EmptyTitle extends Rule {
6440
6513
  setup() {
6441
6514
  this.on("tag:end", (event) => {
6442
6515
  const node = event.previous;
6443
- if (node.tagName !== "title") return;
6516
+ if (node.tagName !== "title") {
6517
+ return;
6518
+ }
6444
6519
  switch (classifyNodeText(node)) {
6445
6520
  case TextClassification.DYNAMIC_TEXT:
6446
6521
  case TextClassification.STATIC_TEXT:
@@ -6461,8 +6536,8 @@ const defaults$l = {
6461
6536
  allowCheckboxDefault: true,
6462
6537
  shared: ["radio", "button", "reset", "submit"]
6463
6538
  };
6464
- const UNIQUE_CACHE_KEY = Symbol("form-elements-unique");
6465
- const SHARED_CACHE_KEY = Symbol("form-elements-shared");
6539
+ const UNIQUE_CACHE_KEY = /* @__PURE__ */ Symbol("form-elements-unique");
6540
+ const SHARED_CACHE_KEY = /* @__PURE__ */ Symbol("form-elements-shared");
6466
6541
  function isEnabled(element) {
6467
6542
  if (isHTMLHidden(element)) {
6468
6543
  return false;
@@ -6761,7 +6836,9 @@ class HeadingLevel extends Rule {
6761
6836
  }
6762
6837
  onTagStart(event) {
6763
6838
  const level = extractLevel(event.target);
6764
- if (!level) return;
6839
+ if (!level) {
6840
+ return;
6841
+ }
6765
6842
  const root = this.getCurrentRoot();
6766
6843
  if (!this.options.allowMultipleH1 && level === 1) {
6767
6844
  if (root.h1Count >= 1) {
@@ -6947,9 +7024,22 @@ class HiddenFocusable extends Rule {
6947
7024
  const defaults$j = {
6948
7025
  pattern: "kebabcase"
6949
7026
  };
7027
+ function exclude$1(set, ...values) {
7028
+ const result = new Set(set);
7029
+ for (const value of values) {
7030
+ result.delete(value);
7031
+ }
7032
+ return result;
7033
+ }
6950
7034
  class IdPattern extends BasePatternRule {
6951
7035
  constructor(options) {
6952
- super("id", { ...defaults$j, ...options });
7036
+ const allowedPatterns = exclude$1(patternNames, "tailwind");
7037
+ super({
7038
+ ruleId: "id-pattern",
7039
+ attr: "id",
7040
+ options: { ...defaults$j, ...options },
7041
+ allowedPatterns
7042
+ });
6953
7043
  }
6954
7044
  static schema() {
6955
7045
  return BasePatternRule.schema();
@@ -7253,7 +7343,9 @@ class InputMissingLabel extends Rule {
7253
7343
  }
7254
7344
  }
7255
7345
  function findLabelById(root, id) {
7256
- if (!id) return [];
7346
+ if (!id) {
7347
+ return [];
7348
+ }
7257
7349
  return root.querySelectorAll(`label[for="${id}"]`);
7258
7350
  }
7259
7351
  function findLabelByParent(el) {
@@ -7292,7 +7384,9 @@ class LongTitle extends Rule {
7292
7384
  setup() {
7293
7385
  this.on("tag:end", (event) => {
7294
7386
  const node = event.previous;
7295
- if (node.tagName !== "title") return;
7387
+ if (node.tagName !== "title") {
7388
+ return;
7389
+ }
7296
7390
  const text = node.textContent;
7297
7391
  if (text.length > this.maxlength) {
7298
7392
  this.report(node, `title text cannot be longer than ${String(this.maxlength)} characters`);
@@ -7494,9 +7588,22 @@ class MultipleLabeledControls extends Rule {
7494
7588
  const defaults$g = {
7495
7589
  pattern: "camelcase"
7496
7590
  };
7591
+ function exclude(set, ...values) {
7592
+ const result = new Set(set);
7593
+ for (const value of values) {
7594
+ result.delete(value);
7595
+ }
7596
+ return result;
7597
+ }
7497
7598
  class NamePattern extends BasePatternRule {
7498
7599
  constructor(options) {
7499
- super("name", { ...defaults$g, ...options });
7600
+ const allowedPatterns = exclude(patternNames, "tailwind");
7601
+ super({
7602
+ ruleId: "name-pattern",
7603
+ attr: "name",
7604
+ options: { ...defaults$g, ...options },
7605
+ allowedPatterns
7606
+ });
7500
7607
  }
7501
7608
  static schema() {
7502
7609
  return BasePatternRule.schema();
@@ -7752,7 +7859,7 @@ class NoDupClass extends Rule {
7752
7859
  }
7753
7860
  }
7754
7861
 
7755
- const CACHE_KEY = Symbol("no-dup-id");
7862
+ const CACHE_KEY = /* @__PURE__ */ Symbol("no-dup-id");
7756
7863
  class NoDupID extends Rule {
7757
7864
  documentation() {
7758
7865
  return {
@@ -7857,7 +7964,7 @@ Omitted end tags can be ambigious for humans to read and many editors have troub
7857
7964
  return;
7858
7965
  }
7859
7966
  const parent = closed.parent;
7860
- const closedByParent = parent && parent.tagName === by.tagName;
7967
+ const closedByParent = parent?.tagName === by.tagName;
7861
7968
  const closedByDocument = closedByParent && parent.isRootElement();
7862
7969
  const sameTag = closed.tagName === by.tagName;
7863
7970
  if (closedByDocument) {
@@ -8219,7 +8326,7 @@ class NoRedundantAriaLabel extends Rule {
8219
8326
  continue;
8220
8327
  }
8221
8328
  const label = document.querySelector(`label[for="${id}"]`);
8222
- if (!ariaLabel || !label || label.textContent.trim() !== ariaLabel.value) {
8329
+ if (!ariaLabel || label?.textContent.trim() !== ariaLabel.value) {
8223
8330
  continue;
8224
8331
  }
8225
8332
  const message = "aria-label is redundant when label containing same text exists";
@@ -10038,6 +10145,46 @@ class ValidAutocomplete extends Rule {
10038
10145
  }
10039
10146
  }
10040
10147
 
10148
+ function isLabelable(target) {
10149
+ const { meta } = target;
10150
+ if (!meta) {
10151
+ return true;
10152
+ }
10153
+ return Boolean(meta.labelable);
10154
+ }
10155
+ class ValidFor extends Rule {
10156
+ documentation() {
10157
+ return {
10158
+ description: `The \`<label>\` \`for\` attribute must reference a labelable form control.`,
10159
+ url: "https://html-validate.org/rules/valid-for.html"
10160
+ };
10161
+ }
10162
+ setup() {
10163
+ this.on("dom:ready", (event) => {
10164
+ const { document } = event;
10165
+ for (const node of document.querySelectorAll("label[for]")) {
10166
+ const attr = node.getAttribute("for");
10167
+ if (!isStaticAttribute(attr) || !attr.value) {
10168
+ continue;
10169
+ }
10170
+ const selector = generateIdSelector(attr.value);
10171
+ const target = document.querySelector(selector);
10172
+ if (!target) {
10173
+ continue;
10174
+ }
10175
+ if (isLabelable(target)) {
10176
+ continue;
10177
+ }
10178
+ this.report({
10179
+ node,
10180
+ message: '<label> "for" attribute must reference a labelable form control',
10181
+ location: attr.valueLocation
10182
+ });
10183
+ }
10184
+ });
10185
+ }
10186
+ }
10187
+
10041
10188
  const defaults$4 = {
10042
10189
  relaxed: false
10043
10190
  };
@@ -10336,7 +10483,9 @@ class H36 extends Rule {
10336
10483
  setup() {
10337
10484
  this.on("tag:end", (event) => {
10338
10485
  const node = event.previous;
10339
- if (node.tagName !== "input") return;
10486
+ if (node.tagName !== "input") {
10487
+ return;
10488
+ }
10340
10489
  if (node.getAttributeValue("type") !== "image") {
10341
10490
  return;
10342
10491
  }
@@ -10662,6 +10811,7 @@ const bundledRules = {
10662
10811
  "unique-landmark": UniqueLandmark,
10663
10812
  "unrecognized-char-ref": UnknownCharReference,
10664
10813
  "valid-autocomplete": ValidAutocomplete,
10814
+ "valid-for": ValidFor,
10665
10815
  "valid-id": ValidID,
10666
10816
  "void-content": VoidContent,
10667
10817
  "void-style": VoidStyle,
@@ -11015,6 +11165,7 @@ const config$1 = {
11015
11165
  "unique-landmark": "error",
11016
11166
  "unrecognized-char-ref": "error",
11017
11167
  "valid-autocomplete": "error",
11168
+ "valid-for": "error",
11018
11169
  "valid-id": ["error", { relaxed: false }],
11019
11170
  void: "off",
11020
11171
  "void-content": "error",
@@ -11062,6 +11213,7 @@ const config = {
11062
11213
  "script-element": "error",
11063
11214
  "unrecognized-char-ref": "error",
11064
11215
  "valid-autocomplete": "error",
11216
+ "valid-for": "error",
11065
11217
  "valid-id": ["error", { relaxed: true }],
11066
11218
  "void-content": "error"
11067
11219
  }
@@ -11668,7 +11820,9 @@ class Config {
11668
11820
  }
11669
11821
  for (const plugin of plugins) {
11670
11822
  for (const [name, config] of Object.entries(plugin.configs ?? {})) {
11671
- if (!config) continue;
11823
+ if (!config) {
11824
+ continue;
11825
+ }
11672
11826
  Config.validate(config, name);
11673
11827
  configs.set(`${plugin.name}:${name}`, config);
11674
11828
  if (plugin.name !== plugin.originalName) {
@@ -11969,7 +12123,7 @@ class EventHandler {
11969
12123
  }
11970
12124
 
11971
12125
  const name = "html-validate";
11972
- const version = "10.4.0";
12126
+ const version = "10.6.0";
11973
12127
  const bugs = "https://gitlab.com/html-validate/html-validate/issues/new";
11974
12128
 
11975
12129
  function freeze(src) {
@@ -12020,6 +12174,7 @@ class Reporter {
12020
12174
  warningCount: sumWarnings(results)
12021
12175
  };
12022
12176
  }
12177
+ /* eslint-disable-next-line @typescript-eslint/max-params -- technical debt */
12023
12178
  add(rule, message, severity, node, location, context) {
12024
12179
  if (!(location.filename in this.result)) {
12025
12180
  this.result[location.filename] = [];
@@ -12139,7 +12294,7 @@ class ParserError extends Error {
12139
12294
  }
12140
12295
 
12141
12296
  function isAttrValueToken(token) {
12142
- return Boolean(token && token.type === TokenType.ATTR_VALUE);
12297
+ return token?.type === TokenType.ATTR_VALUE;
12143
12298
  }
12144
12299
  function svgShouldRetainTag(foreignTagName, tagName) {
12145
12300
  return foreignTagName === "svg" && ["title", "desc"].includes(tagName);
@@ -12485,7 +12640,7 @@ class Parser {
12485
12640
  * ^^^ ^^^ ^^^ (null) (null)
12486
12641
  */
12487
12642
  getAttributeValueLocation(token) {
12488
- if (!token || token.type !== TokenType.ATTR_VALUE || token.data[2] === "") {
12643
+ if (token?.type !== TokenType.ATTR_VALUE || token.data[2] === "") {
12489
12644
  return null;
12490
12645
  }
12491
12646
  const quote = token.data[3];
@@ -12501,7 +12656,7 @@ class Parser {
12501
12656
  */
12502
12657
  getAttributeLocation(key, value) {
12503
12658
  const begin = key.location;
12504
- const end = value && value.type === TokenType.ATTR_VALUE ? value.location : void 0;
12659
+ const end = value?.type === TokenType.ATTR_VALUE ? value.location : void 0;
12505
12660
  return {
12506
12661
  filename: begin.filename,
12507
12662
  line: begin.line,
@@ -12523,7 +12678,11 @@ class Parser {
12523
12678
  throw new Error(`Failed to parse directive "${text}"`);
12524
12679
  }
12525
12680
  if (!isValidDirective(action)) {
12526
- throw new ParserError(token.location, `Unknown directive "${action}"`);
12681
+ this.trigger("parse:error", {
12682
+ location: token.location,
12683
+ message: `Unknown directive "${action}"`
12684
+ });
12685
+ return;
12527
12686
  }
12528
12687
  const [, data, separator2, comment] = match;
12529
12688
  const prefix = "html-validate-";
@@ -12616,7 +12775,9 @@ class Parser {
12616
12775
  while (!it.done) {
12617
12776
  const token = it.value;
12618
12777
  yield token;
12619
- if (token.type === search) return;
12778
+ if (token.type === search) {
12779
+ return;
12780
+ }
12620
12781
  it = this.next(tokenStream);
12621
12782
  }
12622
12783
  throw new ParserError(
@@ -12779,6 +12940,9 @@ class Engine {
12779
12940
  parser.on("directive", (_2, event) => {
12780
12941
  this.processDirective(event, parser, directiveContext);
12781
12942
  });
12943
+ parser.on("parse:error", (_2, event) => {
12944
+ this.reportError("parser-error", event.message, event.location);
12945
+ });
12782
12946
  try {
12783
12947
  parser.parseHtml(source);
12784
12948
  } catch (e) {
@@ -12976,7 +13140,9 @@ class Engine {
12976
13140
  const availableRules = {};
12977
13141
  for (const plugin of config.getPlugins()) {
12978
13142
  for (const [name, rule] of Object.entries(plugin.rules ?? {})) {
12979
- if (!rule) continue;
13143
+ if (!rule) {
13144
+ continue;
13145
+ }
12980
13146
  availableRules[name] = rule;
12981
13147
  }
12982
13148
  }
@@ -13009,6 +13175,7 @@ class Engine {
13009
13175
  /**
13010
13176
  * Load and setup a rule using current config.
13011
13177
  */
13178
+ /* eslint-disable-next-line @typescript-eslint/max-params -- technical debt */
13012
13179
  loadRule(ruleId, config, severity, options, parser, report) {
13013
13180
  const meta = config.getMetaTable();
13014
13181
  const rule = this.instantiateRule(ruleId, options);