html-validate 10.5.0 → 10.7.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
@@ -1216,7 +1216,9 @@ class MetaTable {
1216
1216
  );
1217
1217
  }
1218
1218
  for (const [key, value] of Object.entries(obj)) {
1219
- if (key === "$schema") continue;
1219
+ if (key === "$schema") {
1220
+ continue;
1221
+ }
1220
1222
  this.addEntry(key, migrateElement(value));
1221
1223
  }
1222
1224
  } catch (err) {
@@ -1316,7 +1318,9 @@ class MetaTable {
1316
1318
  * global, e.g. to assign global attributes.
1317
1319
  */
1318
1320
  resolveGlobal() {
1319
- if (!this.elements["*"]) return;
1321
+ if (!this.elements["*"]) {
1322
+ return;
1323
+ }
1320
1324
  const global = this.elements["*"];
1321
1325
  delete this.elements["*"];
1322
1326
  delete global.tagName;
@@ -1490,7 +1494,9 @@ function sliceSize(size, begin, end) {
1490
1494
  return Math.min(size, end - begin);
1491
1495
  }
1492
1496
  function sliceLocation(location, begin, end, wrap) {
1493
- if (!location) return null;
1497
+ if (!location) {
1498
+ return null;
1499
+ }
1494
1500
  const size = sliceSize(location.size, begin, end);
1495
1501
  const sliced = {
1496
1502
  filename: location.filename,
@@ -2369,7 +2375,7 @@ class Selector {
2369
2375
 
2370
2376
  const TEXT_NODE_NAME = "#text";
2371
2377
  function isTextNode(node) {
2372
- return Boolean(node && node.nodeType === NodeType.TEXT_NODE);
2378
+ return node?.nodeType === NodeType.TEXT_NODE;
2373
2379
  }
2374
2380
  class TextNode extends DOMNode {
2375
2381
  text;
@@ -2413,7 +2419,7 @@ var NodeClosed = /* @__PURE__ */ ((NodeClosed2) => {
2413
2419
  return NodeClosed2;
2414
2420
  })(NodeClosed || {});
2415
2421
  function isElementNode(node) {
2416
- return Boolean(node && node.nodeType === NodeType.ELEMENT_NODE);
2422
+ return node?.nodeType === NodeType.ELEMENT_NODE;
2417
2423
  }
2418
2424
  function isInvalidTagName(tagName) {
2419
2425
  return tagName === "" || tagName === "*";
@@ -3709,6 +3715,7 @@ function interpolate(text, data) {
3709
3715
 
3710
3716
  const ajv$1 = new Ajv__default.default({ strict: true, strictTuples: true, strictTypes: true });
3711
3717
  ajv$1.addMetaSchema(ajvSchemaDraft);
3718
+ ajv$1.addKeyword(ajvRegexpKeyword);
3712
3719
  function getSchemaValidator(ruleId, properties) {
3713
3720
  const $id = `rule/${ruleId}`;
3714
3721
  const cached = ajv$1.getSchema($id);
@@ -3965,6 +3972,7 @@ class Rule {
3965
3972
  *
3966
3973
  * @internal
3967
3974
  */
3975
+ /* eslint-disable-next-line @typescript-eslint/max-params -- technical debt */
3968
3976
  static validateOptions(cls, ruleId, jsonPath, options, filename, config) {
3969
3977
  if (!cls) {
3970
3978
  return;
@@ -4000,7 +4008,7 @@ class Rule {
4000
4008
  }
4001
4009
  }
4002
4010
 
4003
- const defaults$y = {
4011
+ const defaults$z = {
4004
4012
  allowExternal: true,
4005
4013
  allowRelative: true,
4006
4014
  allowAbsolute: true,
@@ -4044,7 +4052,7 @@ class AllowedLinks extends Rule {
4044
4052
  allowRelative;
4045
4053
  allowAbsolute;
4046
4054
  constructor(options) {
4047
- super({ ...defaults$y, ...options });
4055
+ super({ ...defaults$z, ...options });
4048
4056
  this.allowExternal = parseAllow(this.options.allowExternal);
4049
4057
  this.allowRelative = parseAllow(this.options.allowRelative);
4050
4058
  this.allowAbsolute = parseAllow(this.options.allowAbsolute);
@@ -4212,7 +4220,7 @@ class AllowedLinks extends Rule {
4212
4220
  }
4213
4221
  }
4214
4222
 
4215
- const defaults$x = {
4223
+ const defaults$y = {
4216
4224
  accessible: true
4217
4225
  };
4218
4226
  function findByTarget(target, siblings) {
@@ -4242,7 +4250,7 @@ function getDescription$1(context) {
4242
4250
  }
4243
4251
  class AreaAlt extends Rule {
4244
4252
  constructor(options) {
4245
- super({ ...defaults$x, ...options });
4253
+ super({ ...defaults$y, ...options });
4246
4254
  }
4247
4255
  static schema() {
4248
4256
  return {
@@ -4321,8 +4329,12 @@ class AriaHiddenBody extends Rule {
4321
4329
  }
4322
4330
  }
4323
4331
 
4324
- const defaults$w = {
4325
- allowAnyNamable: false
4332
+ const defaults$x = {
4333
+ allowAnyNamable: false,
4334
+ elements: {
4335
+ include: null,
4336
+ exclude: null
4337
+ }
4326
4338
  };
4327
4339
  const allowlist = /* @__PURE__ */ new Set([
4328
4340
  "main",
@@ -4365,7 +4377,26 @@ function isValidUsage(target, meta) {
4365
4377
  }
4366
4378
  class AriaLabelMisuse extends Rule {
4367
4379
  constructor(options) {
4368
- super({ ...defaults$w, ...options });
4380
+ super({ ...defaults$x, ...options });
4381
+ }
4382
+ static schema() {
4383
+ return {
4384
+ allowAnyNamable: {
4385
+ type: "boolean"
4386
+ },
4387
+ elements: {
4388
+ type: "object",
4389
+ properties: {
4390
+ include: {
4391
+ anyOf: [{ type: "array", items: { type: "string" } }, { type: "null" }]
4392
+ },
4393
+ exclude: {
4394
+ anyOf: [{ type: "array", items: { type: "string" } }, { type: "null" }]
4395
+ }
4396
+ },
4397
+ additionalProperties: false
4398
+ }
4399
+ };
4369
4400
  }
4370
4401
  documentation(context) {
4371
4402
  const valid = [
@@ -4423,6 +4454,9 @@ class AriaLabelMisuse extends Rule {
4423
4454
  if (!meta) {
4424
4455
  return;
4425
4456
  }
4457
+ if (this.shouldIgnoreElement(target)) {
4458
+ return;
4459
+ }
4426
4460
  if (isValidUsage(target, meta)) {
4427
4461
  return;
4428
4462
  }
@@ -4447,6 +4481,9 @@ class AriaLabelMisuse extends Rule {
4447
4481
  });
4448
4482
  }
4449
4483
  }
4484
+ shouldIgnoreElement(target) {
4485
+ return isKeywordIgnored(this.options.elements, target.tagName, keywordPatternMatcher);
4486
+ }
4450
4487
  }
4451
4488
 
4452
4489
  class ConfigError extends UserError {
@@ -4509,14 +4546,14 @@ class CaseStyle {
4509
4546
  }
4510
4547
  }
4511
4548
 
4512
- const defaults$v = {
4549
+ const defaults$w = {
4513
4550
  style: "lowercase",
4514
4551
  ignoreForeign: true
4515
4552
  };
4516
4553
  class AttrCase extends Rule {
4517
4554
  style;
4518
4555
  constructor(options) {
4519
- super({ ...defaults$v, ...options });
4556
+ super({ ...defaults$w, ...options });
4520
4557
  this.style = new CaseStyle(this.options.style, "attr-case");
4521
4558
  }
4522
4559
  static schema() {
@@ -4623,7 +4660,7 @@ const MATCH_TEXTAREA_DATA = /^[^]*?(?=<\/textarea)/;
4623
4660
  const MATCH_TEXTAREA_END = /^<(\/)(textarea)/;
4624
4661
  const MATCH_TITLE_DATA = /^[^]*?(?=<\/title)/;
4625
4662
  const MATCH_TITLE_END = /^<(\/)(title)/;
4626
- const MATCH_DIRECTIVE = /^(<!--\s*\[html-validate-)([a-z0-9-]+)(\s*)(.*?)(]?\s*-->)/;
4663
+ const MATCH_DIRECTIVE = /^(<!--\s*\[?)(html-validate-)([a-z0-9-]+)(\s*)(.*?)(]?\s*-->)/;
4627
4664
  const MATCH_COMMENT = /^<!--([^]*?)-->/;
4628
4665
  const MATCH_CONDITIONAL = /^<!\[([^\]]*?)\]>/;
4629
4666
  class InvalidTokenError extends Error {
@@ -4921,7 +4958,7 @@ class AttrDelimiter extends Rule {
4921
4958
  }
4922
4959
 
4923
4960
  const DEFAULT_PATTERN = "[a-z0-9-:]+";
4924
- const defaults$u = {
4961
+ const defaults$v = {
4925
4962
  pattern: DEFAULT_PATTERN,
4926
4963
  ignoreForeign: true
4927
4964
  };
@@ -4954,7 +4991,7 @@ function generateDescription(name, pattern) {
4954
4991
  class AttrPattern extends Rule {
4955
4992
  pattern;
4956
4993
  constructor(options) {
4957
- super({ ...defaults$u, ...options });
4994
+ super({ ...defaults$v, ...options });
4958
4995
  this.pattern = generateRegexp(this.options.pattern);
4959
4996
  }
4960
4997
  static schema() {
@@ -5001,7 +5038,7 @@ class AttrPattern extends Rule {
5001
5038
  }
5002
5039
  }
5003
5040
 
5004
- const defaults$t = {
5041
+ const defaults$u = {
5005
5042
  style: "auto",
5006
5043
  unquoted: false
5007
5044
  };
@@ -5067,7 +5104,7 @@ class AttrQuotes extends Rule {
5067
5104
  };
5068
5105
  }
5069
5106
  constructor(options) {
5070
- super({ ...defaults$t, ...options });
5107
+ super({ ...defaults$u, ...options });
5071
5108
  this.style = parseStyle$3(this.options.style);
5072
5109
  }
5073
5110
  setup() {
@@ -5189,7 +5226,9 @@ class AttributeAllowedValues extends Rule {
5189
5226
  const doc = event.document;
5190
5227
  walk.depthFirst(doc, (node) => {
5191
5228
  const meta = node.meta;
5192
- if (!meta?.attributes) return;
5229
+ if (!meta?.attributes) {
5230
+ return;
5231
+ }
5193
5232
  for (const attr of node.attributes) {
5194
5233
  if (Validator.validateAttribute(attr, meta.attributes)) {
5195
5234
  continue;
@@ -5221,13 +5260,13 @@ class AttributeAllowedValues extends Rule {
5221
5260
  }
5222
5261
  }
5223
5262
 
5224
- const defaults$s = {
5263
+ const defaults$t = {
5225
5264
  style: "omit"
5226
5265
  };
5227
5266
  class AttributeBooleanStyle extends Rule {
5228
5267
  hasInvalidStyle;
5229
5268
  constructor(options) {
5230
- super({ ...defaults$s, ...options });
5269
+ super({ ...defaults$t, ...options });
5231
5270
  this.hasInvalidStyle = parseStyle$2(this.options.style);
5232
5271
  }
5233
5272
  static schema() {
@@ -5249,9 +5288,13 @@ class AttributeBooleanStyle extends Rule {
5249
5288
  const doc = event.document;
5250
5289
  walk.depthFirst(doc, (node) => {
5251
5290
  const meta = node.meta;
5252
- if (!meta?.attributes) return;
5291
+ if (!meta?.attributes) {
5292
+ return;
5293
+ }
5253
5294
  for (const attr of node.attributes) {
5254
- if (!this.isBoolean(attr, meta.attributes)) continue;
5295
+ if (!this.isBoolean(attr, meta.attributes)) {
5296
+ continue;
5297
+ }
5255
5298
  if (attr.originalAttribute) {
5256
5299
  continue;
5257
5300
  }
@@ -5293,13 +5336,13 @@ function reportMessage$1(attr, style) {
5293
5336
  return "";
5294
5337
  }
5295
5338
 
5296
- const defaults$r = {
5339
+ const defaults$s = {
5297
5340
  style: "omit"
5298
5341
  };
5299
5342
  class AttributeEmptyStyle extends Rule {
5300
5343
  hasInvalidStyle;
5301
5344
  constructor(options) {
5302
- super({ ...defaults$r, ...options });
5345
+ super({ ...defaults$s, ...options });
5303
5346
  this.hasInvalidStyle = parseStyle$1(this.options.style);
5304
5347
  }
5305
5348
  static schema() {
@@ -5321,7 +5364,9 @@ class AttributeEmptyStyle extends Rule {
5321
5364
  const doc = event.document;
5322
5365
  walk.depthFirst(doc, (node) => {
5323
5366
  const meta = node.meta;
5324
- if (!meta?.attributes) return;
5367
+ if (!meta?.attributes) {
5368
+ return;
5369
+ }
5325
5370
  for (const attr of node.attributes) {
5326
5371
  if (!allowsEmpty(attr, meta.attributes)) {
5327
5372
  continue;
@@ -5414,7 +5459,22 @@ class AttributeMisuse extends Rule {
5414
5459
  }
5415
5460
  }
5416
5461
 
5462
+ const patternNamesValues = [
5463
+ "kebabcase",
5464
+ "camelcase",
5465
+ "underscore",
5466
+ "snakecase",
5467
+ "bem",
5468
+ "tailwind"
5469
+ ];
5470
+ const patternNames = new Set(patternNamesValues);
5471
+ function isNamedPattern(value) {
5472
+ return typeof value === "string" && patternNames.has(value);
5473
+ }
5417
5474
  function parsePattern(pattern) {
5475
+ if (pattern instanceof RegExp) {
5476
+ return { regexp: pattern, description: pattern.toString() };
5477
+ }
5418
5478
  switch (pattern) {
5419
5479
  case "kebabcase":
5420
5480
  return { regexp: /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/, description: pattern };
@@ -5432,7 +5492,21 @@ function parsePattern(pattern) {
5432
5492
  description: pattern
5433
5493
  };
5434
5494
  }
5495
+ case "tailwind": {
5496
+ return {
5497
+ regexp: /^!?(?:[-a-z[]|\d+xl:)[\w\-:./\\[\]()#'&>,!=%]*$/,
5498
+ description: "tailwind"
5499
+ };
5500
+ }
5435
5501
  default: {
5502
+ if (pattern.startsWith("/") && pattern.endsWith("/")) {
5503
+ const regexpSource = pattern.slice(1, -1);
5504
+ const regexp2 = new RegExp(regexpSource);
5505
+ return { regexp: regexp2, description: regexp2.toString() };
5506
+ }
5507
+ console.warn(
5508
+ `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.`
5509
+ );
5436
5510
  const regexp = new RegExp(pattern);
5437
5511
  return { regexp, description: regexp.toString() };
5438
5512
  }
@@ -5440,27 +5514,49 @@ function parsePattern(pattern) {
5440
5514
  }
5441
5515
 
5442
5516
  function toArray$2(value) {
5443
- return Array.isArray(value) ? value : [value];
5517
+ if (Array.isArray(value)) {
5518
+ return value;
5519
+ } else {
5520
+ return [value];
5521
+ }
5522
+ }
5523
+ function validateAllowedPatterns(patterns, allowedPatterns, ruleId) {
5524
+ const extraneous = patterns.filter(isNamedPattern).filter((p) => !allowedPatterns.has(p));
5525
+ if (extraneous.length > 0) {
5526
+ const quote = (it) => `"${it}"`;
5527
+ const disallowed = utils_naturalJoin.naturalJoin(extraneous.map(quote), "and");
5528
+ const allowed = utils_naturalJoin.naturalJoin(Array.from(allowedPatterns, quote), "and");
5529
+ throw new Error(
5530
+ `Pattern ${disallowed} cannot be used with "${ruleId}". Allowed patterns: ${allowed}`
5531
+ );
5532
+ }
5444
5533
  }
5445
5534
  class BasePatternRule extends Rule {
5446
5535
  /** Attribute being tested */
5447
5536
  attr;
5448
5537
  /** Parsed configured patterns */
5449
5538
  patterns;
5450
- /**
5451
- * @param attr - Attribute holding the value.
5452
- * @param options - Rule options with defaults expanded.
5453
- */
5454
- constructor(attr, options) {
5539
+ constructor({
5540
+ ruleId,
5541
+ attr,
5542
+ options,
5543
+ allowedPatterns
5544
+ }) {
5455
5545
  super(options);
5456
5546
  const { pattern } = this.options;
5457
5547
  this.attr = attr;
5458
- this.patterns = toArray$2(pattern).map((it) => parsePattern(it));
5548
+ const patterns = toArray$2(pattern);
5549
+ validateAllowedPatterns(patterns, allowedPatterns, ruleId);
5550
+ this.patterns = patterns.map((it) => parsePattern(it));
5459
5551
  }
5460
5552
  static schema() {
5461
5553
  return {
5462
5554
  pattern: {
5463
- oneOf: [{ type: "array", items: { type: "string" }, minItems: 1 }, { type: "string" }]
5555
+ anyOf: [
5556
+ { type: "array", items: { anyOf: [{ type: "string" }, { regexp: true }] }, minItems: 1 },
5557
+ { type: "string" },
5558
+ { regexp: true }
5559
+ ]
5464
5560
  }
5465
5561
  };
5466
5562
  }
@@ -5494,12 +5590,18 @@ class BasePatternRule extends Rule {
5494
5590
  }
5495
5591
  }
5496
5592
 
5497
- const defaults$q = {
5593
+ const defaults$r = {
5498
5594
  pattern: "kebabcase"
5499
5595
  };
5500
5596
  class ClassPattern extends BasePatternRule {
5501
5597
  constructor(options) {
5502
- super("class", { ...defaults$q, ...options });
5598
+ super({
5599
+ ruleId: "class-pattern",
5600
+ attr: "class",
5601
+ options: { ...defaults$r, ...options },
5602
+ allowedPatterns: patternNames
5603
+ // allow all patterns
5604
+ });
5503
5605
  }
5504
5606
  static schema() {
5505
5607
  return BasePatternRule.schema();
@@ -5646,13 +5748,13 @@ class CloseOrder extends Rule {
5646
5748
  }
5647
5749
  }
5648
5750
 
5649
- const defaults$p = {
5751
+ const defaults$q = {
5650
5752
  include: null,
5651
5753
  exclude: null
5652
5754
  };
5653
5755
  class Deprecated extends Rule {
5654
5756
  constructor(options) {
5655
- super({ ...defaults$p, ...options });
5757
+ super({ ...defaults$q, ...options });
5656
5758
  }
5657
5759
  static schema() {
5658
5760
  return {
@@ -5806,12 +5908,12 @@ let NoStyleTag$1 = class NoStyleTag extends Rule {
5806
5908
  }
5807
5909
  };
5808
5910
 
5809
- const defaults$o = {
5911
+ const defaults$p = {
5810
5912
  style: "uppercase"
5811
5913
  };
5812
5914
  class DoctypeStyle extends Rule {
5813
5915
  constructor(options) {
5814
- super({ ...defaults$o, ...options });
5916
+ super({ ...defaults$p, ...options });
5815
5917
  }
5816
5918
  static schema() {
5817
5919
  return {
@@ -5839,13 +5941,13 @@ class DoctypeStyle extends Rule {
5839
5941
  }
5840
5942
  }
5841
5943
 
5842
- const defaults$n = {
5944
+ const defaults$o = {
5843
5945
  style: "lowercase"
5844
5946
  };
5845
5947
  class ElementCase extends Rule {
5846
5948
  style;
5847
5949
  constructor(options) {
5848
- super({ ...defaults$n, ...options });
5950
+ super({ ...defaults$o, ...options });
5849
5951
  this.style = new CaseStyle(this.options.style, "element-case");
5850
5952
  }
5851
5953
  static schema() {
@@ -5905,7 +6007,7 @@ class ElementCase extends Rule {
5905
6007
  }
5906
6008
  }
5907
6009
 
5908
- const defaults$m = {
6010
+ const defaults$n = {
5909
6011
  pattern: "^[a-z][a-z0-9\\-._]*-[a-z0-9\\-._]*$",
5910
6012
  whitelist: [],
5911
6013
  blacklist: []
@@ -5913,7 +6015,7 @@ const defaults$m = {
5913
6015
  class ElementName extends Rule {
5914
6016
  pattern;
5915
6017
  constructor(options) {
5916
- super({ ...defaults$m, ...options });
6018
+ super({ ...defaults$n, ...options });
5917
6019
  this.pattern = new RegExp(this.options.pattern);
5918
6020
  }
5919
6021
  static schema() {
@@ -5950,7 +6052,7 @@ class ElementName extends Rule {
5950
6052
  ...context.blacklist.map((cur) => `- ${cur}`)
5951
6053
  ];
5952
6054
  }
5953
- if (context.pattern !== defaults$m.pattern) {
6055
+ if (context.pattern !== defaults$n.pattern) {
5954
6056
  return [
5955
6057
  `<${context.tagName}> is not a valid element name. This project is configured to only allow names matching the following regular expression:`,
5956
6058
  "",
@@ -6449,7 +6551,9 @@ class EmptyTitle extends Rule {
6449
6551
  setup() {
6450
6552
  this.on("tag:end", (event) => {
6451
6553
  const node = event.previous;
6452
- if (node.tagName !== "title") return;
6554
+ if (node.tagName !== "title") {
6555
+ return;
6556
+ }
6453
6557
  switch (classifyNodeText(node)) {
6454
6558
  case TextClassification.DYNAMIC_TEXT:
6455
6559
  case TextClassification.STATIC_TEXT:
@@ -6465,7 +6569,7 @@ class EmptyTitle extends Rule {
6465
6569
  }
6466
6570
  }
6467
6571
 
6468
- const defaults$l = {
6572
+ const defaults$m = {
6469
6573
  allowArrayBrackets: true,
6470
6574
  allowCheckboxDefault: true,
6471
6575
  shared: ["radio", "button", "reset", "submit"]
@@ -6525,7 +6629,7 @@ function getDocumentation(context) {
6525
6629
  }
6526
6630
  class FormDupName extends Rule {
6527
6631
  constructor(options) {
6528
- super({ ...defaults$l, ...options });
6632
+ super({ ...defaults$m, ...options });
6529
6633
  }
6530
6634
  static schema() {
6531
6635
  return {
@@ -6684,7 +6788,7 @@ class FormDupName extends Rule {
6684
6788
  }
6685
6789
  }
6686
6790
 
6687
- const defaults$k = {
6791
+ const defaults$l = {
6688
6792
  allowMultipleH1: false,
6689
6793
  minInitialRank: "h1",
6690
6794
  sectioningRoots: ["dialog", '[role="dialog"]', '[role="alertdialog"]']
@@ -6716,7 +6820,7 @@ class HeadingLevel extends Rule {
6716
6820
  sectionRoots;
6717
6821
  stack = [];
6718
6822
  constructor(options) {
6719
- super({ ...defaults$k, ...options });
6823
+ super({ ...defaults$l, ...options });
6720
6824
  this.minInitialRank = parseMaxInitial(this.options.minInitialRank);
6721
6825
  this.sectionRoots = this.options.sectioningRoots.map((it) => new Compound(it));
6722
6826
  this.stack.push({
@@ -6770,7 +6874,9 @@ class HeadingLevel extends Rule {
6770
6874
  }
6771
6875
  onTagStart(event) {
6772
6876
  const level = extractLevel(event.target);
6773
- if (!level) return;
6877
+ if (!level) {
6878
+ return;
6879
+ }
6774
6880
  const root = this.getCurrentRoot();
6775
6881
  if (!this.options.allowMultipleH1 && level === 1) {
6776
6882
  if (root.h1Count >= 1) {
@@ -6953,12 +7059,25 @@ class HiddenFocusable extends Rule {
6953
7059
  }
6954
7060
  }
6955
7061
 
6956
- const defaults$j = {
7062
+ const defaults$k = {
6957
7063
  pattern: "kebabcase"
6958
7064
  };
7065
+ function exclude$1(set, ...values) {
7066
+ const result = new Set(set);
7067
+ for (const value of values) {
7068
+ result.delete(value);
7069
+ }
7070
+ return result;
7071
+ }
6959
7072
  class IdPattern extends BasePatternRule {
6960
7073
  constructor(options) {
6961
- super("id", { ...defaults$j, ...options });
7074
+ const allowedPatterns = exclude$1(patternNames, "tailwind");
7075
+ super({
7076
+ ruleId: "id-pattern",
7077
+ attr: "id",
7078
+ options: { ...defaults$k, ...options },
7079
+ allowedPatterns
7080
+ });
6962
7081
  }
6963
7082
  static schema() {
6964
7083
  return BasePatternRule.schema();
@@ -7262,7 +7381,9 @@ class InputMissingLabel extends Rule {
7262
7381
  }
7263
7382
  }
7264
7383
  function findLabelById(root, id) {
7265
- if (!id) return [];
7384
+ if (!id) {
7385
+ return [];
7386
+ }
7266
7387
  return root.querySelectorAll(`label[for="${id}"]`);
7267
7388
  }
7268
7389
  function findLabelByParent(el) {
@@ -7276,13 +7397,13 @@ function findLabelByParent(el) {
7276
7397
  return [];
7277
7398
  }
7278
7399
 
7279
- const defaults$i = {
7400
+ const defaults$j = {
7280
7401
  maxlength: 70
7281
7402
  };
7282
7403
  class LongTitle extends Rule {
7283
7404
  maxlength;
7284
7405
  constructor(options) {
7285
- super({ ...defaults$i, ...options });
7406
+ super({ ...defaults$j, ...options });
7286
7407
  this.maxlength = this.options.maxlength;
7287
7408
  }
7288
7409
  static schema() {
@@ -7301,7 +7422,9 @@ class LongTitle extends Rule {
7301
7422
  setup() {
7302
7423
  this.on("tag:end", (event) => {
7303
7424
  const node = event.previous;
7304
- if (node.tagName !== "title") return;
7425
+ if (node.tagName !== "title") {
7426
+ return;
7427
+ }
7305
7428
  const text = node.textContent;
7306
7429
  if (text.length > this.maxlength) {
7307
7430
  this.report(node, `title text cannot be longer than ${String(this.maxlength)} characters`);
@@ -7384,12 +7507,12 @@ class MapIdName extends Rule {
7384
7507
  }
7385
7508
  }
7386
7509
 
7387
- const defaults$h = {
7510
+ const defaults$i = {
7388
7511
  allowLongDelay: false
7389
7512
  };
7390
7513
  class MetaRefresh extends Rule {
7391
7514
  constructor(options) {
7392
- super({ ...defaults$h, ...options });
7515
+ super({ ...defaults$i, ...options });
7393
7516
  }
7394
7517
  documentation() {
7395
7518
  return {
@@ -7500,12 +7623,25 @@ class MultipleLabeledControls extends Rule {
7500
7623
  }
7501
7624
  }
7502
7625
 
7503
- const defaults$g = {
7626
+ const defaults$h = {
7504
7627
  pattern: "camelcase"
7505
7628
  };
7629
+ function exclude(set, ...values) {
7630
+ const result = new Set(set);
7631
+ for (const value of values) {
7632
+ result.delete(value);
7633
+ }
7634
+ return result;
7635
+ }
7506
7636
  class NamePattern extends BasePatternRule {
7507
7637
  constructor(options) {
7508
- super("name", { ...defaults$g, ...options });
7638
+ const allowedPatterns = exclude(patternNames, "tailwind");
7639
+ super({
7640
+ ruleId: "name-pattern",
7641
+ attr: "name",
7642
+ options: { ...defaults$h, ...options },
7643
+ allowedPatterns
7644
+ });
7509
7645
  }
7510
7646
  static schema() {
7511
7647
  return BasePatternRule.schema();
@@ -7594,13 +7730,13 @@ class NoAbstractRole extends Rule {
7594
7730
  }
7595
7731
  }
7596
7732
 
7597
- const defaults$f = {
7733
+ const defaults$g = {
7598
7734
  include: null,
7599
7735
  exclude: null
7600
7736
  };
7601
7737
  class NoAutoplay extends Rule {
7602
7738
  constructor(options) {
7603
- super({ ...defaults$f, ...options });
7739
+ super({ ...defaults$g, ...options });
7604
7740
  }
7605
7741
  documentation(context) {
7606
7742
  return {
@@ -7866,7 +8002,7 @@ Omitted end tags can be ambigious for humans to read and many editors have troub
7866
8002
  return;
7867
8003
  }
7868
8004
  const parent = closed.parent;
7869
- const closedByParent = parent && parent.tagName === by.tagName;
8005
+ const closedByParent = parent?.tagName === by.tagName;
7870
8006
  const closedByDocument = closedByParent && parent.isRootElement();
7871
8007
  const sameTag = closed.tagName === by.tagName;
7872
8008
  if (closedByDocument) {
@@ -7922,14 +8058,14 @@ class NoImplicitInputType extends Rule {
7922
8058
  }
7923
8059
  }
7924
8060
 
7925
- const defaults$e = {
8061
+ const defaults$f = {
7926
8062
  include: null,
7927
8063
  exclude: null,
7928
8064
  allowedProperties: ["display"]
7929
8065
  };
7930
8066
  class NoInlineStyle extends Rule {
7931
8067
  constructor(options) {
7932
- super({ ...defaults$e, ...options });
8068
+ super({ ...defaults$f, ...options });
7933
8069
  }
7934
8070
  static schema() {
7935
8071
  return {
@@ -8115,7 +8251,7 @@ class NoMultipleMain extends Rule {
8115
8251
  }
8116
8252
  }
8117
8253
 
8118
- const defaults$d = {
8254
+ const defaults$e = {
8119
8255
  relaxed: false
8120
8256
  };
8121
8257
  const textRegexp = /([<>]|&(?![a-zA-Z0-9#]+;))/g;
@@ -8133,7 +8269,7 @@ const replacementTable = {
8133
8269
  class NoRawCharacters extends Rule {
8134
8270
  relaxed;
8135
8271
  constructor(options) {
8136
- super({ ...defaults$d, ...options });
8272
+ super({ ...defaults$e, ...options });
8137
8273
  this.relaxed = this.options.relaxed;
8138
8274
  }
8139
8275
  static schema() {
@@ -8228,7 +8364,7 @@ class NoRedundantAriaLabel extends Rule {
8228
8364
  continue;
8229
8365
  }
8230
8366
  const label = document.querySelector(`label[for="${id}"]`);
8231
- if (!ariaLabel || !label || label.textContent.trim() !== ariaLabel.value) {
8367
+ if (!ariaLabel || label?.textContent.trim() !== ariaLabel.value) {
8232
8368
  continue;
8233
8369
  }
8234
8370
  const message = "aria-label is redundant when label containing same text exists";
@@ -8313,13 +8449,13 @@ class NoRedundantRole extends Rule {
8313
8449
  }
8314
8450
 
8315
8451
  const xmlns = /^(.+):.+$/;
8316
- const defaults$c = {
8452
+ const defaults$d = {
8317
8453
  ignoreForeign: true,
8318
8454
  ignoreXML: true
8319
8455
  };
8320
8456
  class NoSelfClosing extends Rule {
8321
8457
  constructor(options) {
8322
- super({ ...defaults$c, ...options });
8458
+ super({ ...defaults$d, ...options });
8323
8459
  }
8324
8460
  static schema() {
8325
8461
  return {
@@ -8369,7 +8505,20 @@ function isRelevant(node, options) {
8369
8505
  return true;
8370
8506
  }
8371
8507
 
8508
+ const defaults$c = {
8509
+ allowTemplate: true
8510
+ };
8372
8511
  class NoStyleTag extends Rule {
8512
+ constructor(options) {
8513
+ super({ ...defaults$c, ...options });
8514
+ }
8515
+ static schema() {
8516
+ return {
8517
+ allowTemplate: {
8518
+ type: "boolean"
8519
+ }
8520
+ };
8521
+ }
8373
8522
  documentation() {
8374
8523
  return {
8375
8524
  description: "Prefer to use external stylesheets with the `<link>` tag instead of inlining the styling.",
@@ -8377,9 +8526,13 @@ class NoStyleTag extends Rule {
8377
8526
  };
8378
8527
  }
8379
8528
  setup() {
8529
+ const { allowTemplate } = this.options;
8380
8530
  this.on("tag:start", (event) => {
8381
8531
  const node = event.target;
8382
8532
  if (node.tagName === "style") {
8533
+ if (allowTemplate && node.parent?.is("template")) {
8534
+ return;
8535
+ }
8383
8536
  this.report(node, "Use external stylesheet with <link> instead of <style> tag");
8384
8537
  }
8385
8538
  });
@@ -9286,7 +9439,7 @@ function getTextFromReference(document, id) {
9286
9439
  if (!id || id instanceof DynamicValue) {
9287
9440
  return id;
9288
9441
  }
9289
- const selector = `#${id}`;
9442
+ const selector = generateIdSelector(id);
9290
9443
  const ref = document.querySelector(selector);
9291
9444
  if (ref) {
9292
9445
  return ref.textContent;
@@ -10047,6 +10200,46 @@ class ValidAutocomplete extends Rule {
10047
10200
  }
10048
10201
  }
10049
10202
 
10203
+ function isLabelable(target) {
10204
+ const { meta } = target;
10205
+ if (!meta) {
10206
+ return true;
10207
+ }
10208
+ return Boolean(meta.labelable);
10209
+ }
10210
+ class ValidFor extends Rule {
10211
+ documentation() {
10212
+ return {
10213
+ description: `The \`<label>\` \`for\` attribute must reference a labelable form control.`,
10214
+ url: "https://html-validate.org/rules/valid-for.html"
10215
+ };
10216
+ }
10217
+ setup() {
10218
+ this.on("dom:ready", (event) => {
10219
+ const { document } = event;
10220
+ for (const node of document.querySelectorAll("label[for]")) {
10221
+ const attr = node.getAttribute("for");
10222
+ if (!isStaticAttribute(attr) || !attr.value) {
10223
+ continue;
10224
+ }
10225
+ const selector = generateIdSelector(attr.value);
10226
+ const target = document.querySelector(selector);
10227
+ if (!target) {
10228
+ continue;
10229
+ }
10230
+ if (isLabelable(target)) {
10231
+ continue;
10232
+ }
10233
+ this.report({
10234
+ node,
10235
+ message: '<label> "for" attribute must reference a labelable form control',
10236
+ location: attr.valueLocation
10237
+ });
10238
+ }
10239
+ });
10240
+ }
10241
+ }
10242
+
10050
10243
  const defaults$4 = {
10051
10244
  relaxed: false
10052
10245
  };
@@ -10345,7 +10538,9 @@ class H36 extends Rule {
10345
10538
  setup() {
10346
10539
  this.on("tag:end", (event) => {
10347
10540
  const node = event.previous;
10348
- if (node.tagName !== "input") return;
10541
+ if (node.tagName !== "input") {
10542
+ return;
10543
+ }
10349
10544
  if (node.getAttributeValue("type") !== "image") {
10350
10545
  return;
10351
10546
  }
@@ -10671,6 +10866,7 @@ const bundledRules = {
10671
10866
  "unique-landmark": UniqueLandmark,
10672
10867
  "unrecognized-char-ref": UnknownCharReference,
10673
10868
  "valid-autocomplete": ValidAutocomplete,
10869
+ "valid-for": ValidFor,
10674
10870
  "valid-id": ValidID,
10675
10871
  "void-content": VoidContent,
10676
10872
  "void-style": VoidStyle,
@@ -11024,6 +11220,7 @@ const config$1 = {
11024
11220
  "unique-landmark": "error",
11025
11221
  "unrecognized-char-ref": "error",
11026
11222
  "valid-autocomplete": "error",
11223
+ "valid-for": "error",
11027
11224
  "valid-id": ["error", { relaxed: false }],
11028
11225
  void: "off",
11029
11226
  "void-content": "error",
@@ -11071,6 +11268,7 @@ const config = {
11071
11268
  "script-element": "error",
11072
11269
  "unrecognized-char-ref": "error",
11073
11270
  "valid-autocomplete": "error",
11271
+ "valid-for": "error",
11074
11272
  "valid-id": ["error", { relaxed: true }],
11075
11273
  "void-content": "error"
11076
11274
  }
@@ -11677,7 +11875,9 @@ class Config {
11677
11875
  }
11678
11876
  for (const plugin of plugins) {
11679
11877
  for (const [name, config] of Object.entries(plugin.configs ?? {})) {
11680
- if (!config) continue;
11878
+ if (!config) {
11879
+ continue;
11880
+ }
11681
11881
  Config.validate(config, name);
11682
11882
  configs.set(`${plugin.name}:${name}`, config);
11683
11883
  if (plugin.name !== plugin.originalName) {
@@ -11978,7 +12178,7 @@ class EventHandler {
11978
12178
  }
11979
12179
 
11980
12180
  const name = "html-validate";
11981
- const version = "10.5.0";
12181
+ const version = "10.7.0";
11982
12182
  const bugs = "https://gitlab.com/html-validate/html-validate/issues/new";
11983
12183
 
11984
12184
  function freeze(src) {
@@ -12029,6 +12229,7 @@ class Reporter {
12029
12229
  warningCount: sumWarnings(results)
12030
12230
  };
12031
12231
  }
12232
+ /* eslint-disable-next-line @typescript-eslint/max-params -- technical debt */
12032
12233
  add(rule, message, severity, node, location, context) {
12033
12234
  if (!(location.filename in this.result)) {
12034
12235
  this.result[location.filename] = [];
@@ -12148,7 +12349,7 @@ class ParserError extends Error {
12148
12349
  }
12149
12350
 
12150
12351
  function isAttrValueToken(token) {
12151
- return Boolean(token && token.type === TokenType.ATTR_VALUE);
12352
+ return token?.type === TokenType.ATTR_VALUE;
12152
12353
  }
12153
12354
  function svgShouldRetainTag(foreignTagName, tagName) {
12154
12355
  return foreignTagName === "svg" && ["title", "desc"].includes(tagName);
@@ -12494,7 +12695,7 @@ class Parser {
12494
12695
  * ^^^ ^^^ ^^^ (null) (null)
12495
12696
  */
12496
12697
  getAttributeValueLocation(token) {
12497
- if (!token || token.type !== TokenType.ATTR_VALUE || token.data[2] === "") {
12698
+ if (token?.type !== TokenType.ATTR_VALUE || token.data[2] === "") {
12498
12699
  return null;
12499
12700
  }
12500
12701
  const quote = token.data[3];
@@ -12510,7 +12711,7 @@ class Parser {
12510
12711
  */
12511
12712
  getAttributeLocation(key, value) {
12512
12713
  const begin = key.location;
12513
- const end = value && value.type === TokenType.ATTR_VALUE ? value.location : void 0;
12714
+ const end = value?.type === TokenType.ATTR_VALUE ? value.location : void 0;
12514
12715
  return {
12515
12716
  filename: begin.filename,
12516
12717
  line: begin.line,
@@ -12523,27 +12724,34 @@ class Parser {
12523
12724
  * @internal
12524
12725
  */
12525
12726
  consumeDirective(token) {
12526
- const [text, preamble, action, separator1, directive, postamble] = token.data;
12527
- if (!postamble.startsWith("]")) {
12528
- throw new ParserError(token.location, `Missing end bracket "]" on directive "${text}"`);
12727
+ const [text, preamble, prefix, action, separator1, directive, postamble] = token.data;
12728
+ const hasStartBracket = preamble.includes("[");
12729
+ const hasEndBracket = postamble.startsWith("]");
12730
+ if (hasStartBracket && !hasEndBracket) {
12731
+ this.trigger("parse:error", {
12732
+ location: sliceLocation(token.location, preamble.length - 1, -postamble.length),
12733
+ message: `Missing end bracket "]" on directive "${text}"`
12734
+ });
12735
+ return;
12529
12736
  }
12530
12737
  const match = /^(.*?)(?:(\s*(?:--|:)\s*)(.*))?$/.exec(directive);
12531
12738
  if (!match) {
12532
12739
  throw new Error(`Failed to parse directive "${text}"`);
12533
12740
  }
12534
12741
  if (!isValidDirective(action)) {
12535
- throw new ParserError(token.location, `Unknown directive "${action}"`);
12742
+ const begin = preamble.length;
12743
+ const end = preamble.length + prefix.length + action.length;
12744
+ this.trigger("parse:error", {
12745
+ location: sliceLocation(token.location, begin, -text.length + end),
12746
+ message: `Unknown directive "${action}"`
12747
+ });
12748
+ return;
12536
12749
  }
12537
12750
  const [, data, separator2, comment] = match;
12538
- const prefix = "html-validate-";
12539
- const actionOffset = preamble.length;
12751
+ const actionOffset = preamble.length + prefix.length;
12540
12752
  const optionsOffset = actionOffset + action.length + separator1.length;
12541
12753
  const commentOffset = optionsOffset + data.length + (separator2 || "").length;
12542
- const location = sliceLocation(
12543
- token.location,
12544
- preamble.length - prefix.length - 1,
12545
- -postamble.length + 1
12546
- );
12754
+ const location = sliceLocation(token.location, preamble.length - 1, -postamble.length + 1);
12547
12755
  const actionLocation = sliceLocation(
12548
12756
  token.location,
12549
12757
  actionOffset,
@@ -12625,7 +12833,9 @@ class Parser {
12625
12833
  while (!it.done) {
12626
12834
  const token = it.value;
12627
12835
  yield token;
12628
- if (token.type === search) return;
12836
+ if (token.type === search) {
12837
+ return;
12838
+ }
12629
12839
  it = this.next(tokenStream);
12630
12840
  }
12631
12841
  throw new ParserError(
@@ -12788,6 +12998,9 @@ class Engine {
12788
12998
  parser.on("directive", (_2, event) => {
12789
12999
  this.processDirective(event, parser, directiveContext);
12790
13000
  });
13001
+ parser.on("parse:error", (_2, event) => {
13002
+ this.reportError("parser-error", event.message, event.location);
13003
+ });
12791
13004
  try {
12792
13005
  parser.parseHtml(source);
12793
13006
  } catch (e) {
@@ -12985,7 +13198,9 @@ class Engine {
12985
13198
  const availableRules = {};
12986
13199
  for (const plugin of config.getPlugins()) {
12987
13200
  for (const [name, rule] of Object.entries(plugin.rules ?? {})) {
12988
- if (!rule) continue;
13201
+ if (!rule) {
13202
+ continue;
13203
+ }
12989
13204
  availableRules[name] = rule;
12990
13205
  }
12991
13206
  }
@@ -13018,6 +13233,7 @@ class Engine {
13018
13233
  /**
13019
13234
  * Load and setup a rule using current config.
13020
13235
  */
13236
+ /* eslint-disable-next-line @typescript-eslint/max-params -- technical debt */
13021
13237
  loadRule(ruleId, config, severity, options, parser, report) {
13022
13238
  const meta = config.getMetaTable();
13023
13239
  const rule = this.instantiateRule(ruleId, options);