html-validate 10.5.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/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;
@@ -5189,7 +5197,9 @@ class AttributeAllowedValues extends Rule {
5189
5197
  const doc = event.document;
5190
5198
  walk.depthFirst(doc, (node) => {
5191
5199
  const meta = node.meta;
5192
- if (!meta?.attributes) return;
5200
+ if (!meta?.attributes) {
5201
+ return;
5202
+ }
5193
5203
  for (const attr of node.attributes) {
5194
5204
  if (Validator.validateAttribute(attr, meta.attributes)) {
5195
5205
  continue;
@@ -5249,9 +5259,13 @@ class AttributeBooleanStyle extends Rule {
5249
5259
  const doc = event.document;
5250
5260
  walk.depthFirst(doc, (node) => {
5251
5261
  const meta = node.meta;
5252
- if (!meta?.attributes) return;
5262
+ if (!meta?.attributes) {
5263
+ return;
5264
+ }
5253
5265
  for (const attr of node.attributes) {
5254
- if (!this.isBoolean(attr, meta.attributes)) continue;
5266
+ if (!this.isBoolean(attr, meta.attributes)) {
5267
+ continue;
5268
+ }
5255
5269
  if (attr.originalAttribute) {
5256
5270
  continue;
5257
5271
  }
@@ -5321,7 +5335,9 @@ class AttributeEmptyStyle extends Rule {
5321
5335
  const doc = event.document;
5322
5336
  walk.depthFirst(doc, (node) => {
5323
5337
  const meta = node.meta;
5324
- if (!meta?.attributes) return;
5338
+ if (!meta?.attributes) {
5339
+ return;
5340
+ }
5325
5341
  for (const attr of node.attributes) {
5326
5342
  if (!allowsEmpty(attr, meta.attributes)) {
5327
5343
  continue;
@@ -5414,7 +5430,22 @@ class AttributeMisuse extends Rule {
5414
5430
  }
5415
5431
  }
5416
5432
 
5433
+ const patternNamesValues = [
5434
+ "kebabcase",
5435
+ "camelcase",
5436
+ "underscore",
5437
+ "snakecase",
5438
+ "bem",
5439
+ "tailwind"
5440
+ ];
5441
+ const patternNames = new Set(patternNamesValues);
5442
+ function isNamedPattern(value) {
5443
+ return typeof value === "string" && patternNames.has(value);
5444
+ }
5417
5445
  function parsePattern(pattern) {
5446
+ if (pattern instanceof RegExp) {
5447
+ return { regexp: pattern, description: pattern.toString() };
5448
+ }
5418
5449
  switch (pattern) {
5419
5450
  case "kebabcase":
5420
5451
  return { regexp: /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/, description: pattern };
@@ -5432,7 +5463,21 @@ function parsePattern(pattern) {
5432
5463
  description: pattern
5433
5464
  };
5434
5465
  }
5466
+ case "tailwind": {
5467
+ return {
5468
+ regexp: /^!?(?:[-a-z[]|\d+xl:)[\w\-:./\\[\]()#'&>,!=%]*$/,
5469
+ description: "tailwind"
5470
+ };
5471
+ }
5435
5472
  default: {
5473
+ if (pattern.startsWith("/") && pattern.endsWith("/")) {
5474
+ const regexpSource = pattern.slice(1, -1);
5475
+ const regexp2 = new RegExp(regexpSource);
5476
+ return { regexp: regexp2, description: regexp2.toString() };
5477
+ }
5478
+ console.warn(
5479
+ `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.`
5480
+ );
5436
5481
  const regexp = new RegExp(pattern);
5437
5482
  return { regexp, description: regexp.toString() };
5438
5483
  }
@@ -5440,27 +5485,49 @@ function parsePattern(pattern) {
5440
5485
  }
5441
5486
 
5442
5487
  function toArray$2(value) {
5443
- return Array.isArray(value) ? value : [value];
5488
+ if (Array.isArray(value)) {
5489
+ return value;
5490
+ } else {
5491
+ return [value];
5492
+ }
5493
+ }
5494
+ function validateAllowedPatterns(patterns, allowedPatterns, ruleId) {
5495
+ const extraneous = patterns.filter(isNamedPattern).filter((p) => !allowedPatterns.has(p));
5496
+ if (extraneous.length > 0) {
5497
+ const quote = (it) => `"${it}"`;
5498
+ const disallowed = utils_naturalJoin.naturalJoin(extraneous.map(quote), "and");
5499
+ const allowed = utils_naturalJoin.naturalJoin(Array.from(allowedPatterns, quote), "and");
5500
+ throw new Error(
5501
+ `Pattern ${disallowed} cannot be used with "${ruleId}". Allowed patterns: ${allowed}`
5502
+ );
5503
+ }
5444
5504
  }
5445
5505
  class BasePatternRule extends Rule {
5446
5506
  /** Attribute being tested */
5447
5507
  attr;
5448
5508
  /** Parsed configured patterns */
5449
5509
  patterns;
5450
- /**
5451
- * @param attr - Attribute holding the value.
5452
- * @param options - Rule options with defaults expanded.
5453
- */
5454
- constructor(attr, options) {
5510
+ constructor({
5511
+ ruleId,
5512
+ attr,
5513
+ options,
5514
+ allowedPatterns
5515
+ }) {
5455
5516
  super(options);
5456
5517
  const { pattern } = this.options;
5457
5518
  this.attr = attr;
5458
- this.patterns = toArray$2(pattern).map((it) => parsePattern(it));
5519
+ const patterns = toArray$2(pattern);
5520
+ validateAllowedPatterns(patterns, allowedPatterns, ruleId);
5521
+ this.patterns = patterns.map((it) => parsePattern(it));
5459
5522
  }
5460
5523
  static schema() {
5461
5524
  return {
5462
5525
  pattern: {
5463
- oneOf: [{ type: "array", items: { type: "string" }, minItems: 1 }, { type: "string" }]
5526
+ anyOf: [
5527
+ { type: "array", items: { anyOf: [{ type: "string" }, { regexp: true }] }, minItems: 1 },
5528
+ { type: "string" },
5529
+ { regexp: true }
5530
+ ]
5464
5531
  }
5465
5532
  };
5466
5533
  }
@@ -5499,7 +5566,13 @@ const defaults$q = {
5499
5566
  };
5500
5567
  class ClassPattern extends BasePatternRule {
5501
5568
  constructor(options) {
5502
- super("class", { ...defaults$q, ...options });
5569
+ super({
5570
+ ruleId: "class-pattern",
5571
+ attr: "class",
5572
+ options: { ...defaults$q, ...options },
5573
+ allowedPatterns: patternNames
5574
+ // allow all patterns
5575
+ });
5503
5576
  }
5504
5577
  static schema() {
5505
5578
  return BasePatternRule.schema();
@@ -6449,7 +6522,9 @@ class EmptyTitle extends Rule {
6449
6522
  setup() {
6450
6523
  this.on("tag:end", (event) => {
6451
6524
  const node = event.previous;
6452
- if (node.tagName !== "title") return;
6525
+ if (node.tagName !== "title") {
6526
+ return;
6527
+ }
6453
6528
  switch (classifyNodeText(node)) {
6454
6529
  case TextClassification.DYNAMIC_TEXT:
6455
6530
  case TextClassification.STATIC_TEXT:
@@ -6770,7 +6845,9 @@ class HeadingLevel extends Rule {
6770
6845
  }
6771
6846
  onTagStart(event) {
6772
6847
  const level = extractLevel(event.target);
6773
- if (!level) return;
6848
+ if (!level) {
6849
+ return;
6850
+ }
6774
6851
  const root = this.getCurrentRoot();
6775
6852
  if (!this.options.allowMultipleH1 && level === 1) {
6776
6853
  if (root.h1Count >= 1) {
@@ -6956,9 +7033,22 @@ class HiddenFocusable extends Rule {
6956
7033
  const defaults$j = {
6957
7034
  pattern: "kebabcase"
6958
7035
  };
7036
+ function exclude$1(set, ...values) {
7037
+ const result = new Set(set);
7038
+ for (const value of values) {
7039
+ result.delete(value);
7040
+ }
7041
+ return result;
7042
+ }
6959
7043
  class IdPattern extends BasePatternRule {
6960
7044
  constructor(options) {
6961
- super("id", { ...defaults$j, ...options });
7045
+ const allowedPatterns = exclude$1(patternNames, "tailwind");
7046
+ super({
7047
+ ruleId: "id-pattern",
7048
+ attr: "id",
7049
+ options: { ...defaults$j, ...options },
7050
+ allowedPatterns
7051
+ });
6962
7052
  }
6963
7053
  static schema() {
6964
7054
  return BasePatternRule.schema();
@@ -7262,7 +7352,9 @@ class InputMissingLabel extends Rule {
7262
7352
  }
7263
7353
  }
7264
7354
  function findLabelById(root, id) {
7265
- if (!id) return [];
7355
+ if (!id) {
7356
+ return [];
7357
+ }
7266
7358
  return root.querySelectorAll(`label[for="${id}"]`);
7267
7359
  }
7268
7360
  function findLabelByParent(el) {
@@ -7301,7 +7393,9 @@ class LongTitle extends Rule {
7301
7393
  setup() {
7302
7394
  this.on("tag:end", (event) => {
7303
7395
  const node = event.previous;
7304
- if (node.tagName !== "title") return;
7396
+ if (node.tagName !== "title") {
7397
+ return;
7398
+ }
7305
7399
  const text = node.textContent;
7306
7400
  if (text.length > this.maxlength) {
7307
7401
  this.report(node, `title text cannot be longer than ${String(this.maxlength)} characters`);
@@ -7503,9 +7597,22 @@ class MultipleLabeledControls extends Rule {
7503
7597
  const defaults$g = {
7504
7598
  pattern: "camelcase"
7505
7599
  };
7600
+ function exclude(set, ...values) {
7601
+ const result = new Set(set);
7602
+ for (const value of values) {
7603
+ result.delete(value);
7604
+ }
7605
+ return result;
7606
+ }
7506
7607
  class NamePattern extends BasePatternRule {
7507
7608
  constructor(options) {
7508
- super("name", { ...defaults$g, ...options });
7609
+ const allowedPatterns = exclude(patternNames, "tailwind");
7610
+ super({
7611
+ ruleId: "name-pattern",
7612
+ attr: "name",
7613
+ options: { ...defaults$g, ...options },
7614
+ allowedPatterns
7615
+ });
7509
7616
  }
7510
7617
  static schema() {
7511
7618
  return BasePatternRule.schema();
@@ -7866,7 +7973,7 @@ Omitted end tags can be ambigious for humans to read and many editors have troub
7866
7973
  return;
7867
7974
  }
7868
7975
  const parent = closed.parent;
7869
- const closedByParent = parent && parent.tagName === by.tagName;
7976
+ const closedByParent = parent?.tagName === by.tagName;
7870
7977
  const closedByDocument = closedByParent && parent.isRootElement();
7871
7978
  const sameTag = closed.tagName === by.tagName;
7872
7979
  if (closedByDocument) {
@@ -8228,7 +8335,7 @@ class NoRedundantAriaLabel extends Rule {
8228
8335
  continue;
8229
8336
  }
8230
8337
  const label = document.querySelector(`label[for="${id}"]`);
8231
- if (!ariaLabel || !label || label.textContent.trim() !== ariaLabel.value) {
8338
+ if (!ariaLabel || label?.textContent.trim() !== ariaLabel.value) {
8232
8339
  continue;
8233
8340
  }
8234
8341
  const message = "aria-label is redundant when label containing same text exists";
@@ -10047,6 +10154,46 @@ class ValidAutocomplete extends Rule {
10047
10154
  }
10048
10155
  }
10049
10156
 
10157
+ function isLabelable(target) {
10158
+ const { meta } = target;
10159
+ if (!meta) {
10160
+ return true;
10161
+ }
10162
+ return Boolean(meta.labelable);
10163
+ }
10164
+ class ValidFor extends Rule {
10165
+ documentation() {
10166
+ return {
10167
+ description: `The \`<label>\` \`for\` attribute must reference a labelable form control.`,
10168
+ url: "https://html-validate.org/rules/valid-for.html"
10169
+ };
10170
+ }
10171
+ setup() {
10172
+ this.on("dom:ready", (event) => {
10173
+ const { document } = event;
10174
+ for (const node of document.querySelectorAll("label[for]")) {
10175
+ const attr = node.getAttribute("for");
10176
+ if (!isStaticAttribute(attr) || !attr.value) {
10177
+ continue;
10178
+ }
10179
+ const selector = generateIdSelector(attr.value);
10180
+ const target = document.querySelector(selector);
10181
+ if (!target) {
10182
+ continue;
10183
+ }
10184
+ if (isLabelable(target)) {
10185
+ continue;
10186
+ }
10187
+ this.report({
10188
+ node,
10189
+ message: '<label> "for" attribute must reference a labelable form control',
10190
+ location: attr.valueLocation
10191
+ });
10192
+ }
10193
+ });
10194
+ }
10195
+ }
10196
+
10050
10197
  const defaults$4 = {
10051
10198
  relaxed: false
10052
10199
  };
@@ -10345,7 +10492,9 @@ class H36 extends Rule {
10345
10492
  setup() {
10346
10493
  this.on("tag:end", (event) => {
10347
10494
  const node = event.previous;
10348
- if (node.tagName !== "input") return;
10495
+ if (node.tagName !== "input") {
10496
+ return;
10497
+ }
10349
10498
  if (node.getAttributeValue("type") !== "image") {
10350
10499
  return;
10351
10500
  }
@@ -10671,6 +10820,7 @@ const bundledRules = {
10671
10820
  "unique-landmark": UniqueLandmark,
10672
10821
  "unrecognized-char-ref": UnknownCharReference,
10673
10822
  "valid-autocomplete": ValidAutocomplete,
10823
+ "valid-for": ValidFor,
10674
10824
  "valid-id": ValidID,
10675
10825
  "void-content": VoidContent,
10676
10826
  "void-style": VoidStyle,
@@ -11024,6 +11174,7 @@ const config$1 = {
11024
11174
  "unique-landmark": "error",
11025
11175
  "unrecognized-char-ref": "error",
11026
11176
  "valid-autocomplete": "error",
11177
+ "valid-for": "error",
11027
11178
  "valid-id": ["error", { relaxed: false }],
11028
11179
  void: "off",
11029
11180
  "void-content": "error",
@@ -11071,6 +11222,7 @@ const config = {
11071
11222
  "script-element": "error",
11072
11223
  "unrecognized-char-ref": "error",
11073
11224
  "valid-autocomplete": "error",
11225
+ "valid-for": "error",
11074
11226
  "valid-id": ["error", { relaxed: true }],
11075
11227
  "void-content": "error"
11076
11228
  }
@@ -11677,7 +11829,9 @@ class Config {
11677
11829
  }
11678
11830
  for (const plugin of plugins) {
11679
11831
  for (const [name, config] of Object.entries(plugin.configs ?? {})) {
11680
- if (!config) continue;
11832
+ if (!config) {
11833
+ continue;
11834
+ }
11681
11835
  Config.validate(config, name);
11682
11836
  configs.set(`${plugin.name}:${name}`, config);
11683
11837
  if (plugin.name !== plugin.originalName) {
@@ -11978,7 +12132,7 @@ class EventHandler {
11978
12132
  }
11979
12133
 
11980
12134
  const name = "html-validate";
11981
- const version = "10.5.0";
12135
+ const version = "10.6.0";
11982
12136
  const bugs = "https://gitlab.com/html-validate/html-validate/issues/new";
11983
12137
 
11984
12138
  function freeze(src) {
@@ -12029,6 +12183,7 @@ class Reporter {
12029
12183
  warningCount: sumWarnings(results)
12030
12184
  };
12031
12185
  }
12186
+ /* eslint-disable-next-line @typescript-eslint/max-params -- technical debt */
12032
12187
  add(rule, message, severity, node, location, context) {
12033
12188
  if (!(location.filename in this.result)) {
12034
12189
  this.result[location.filename] = [];
@@ -12148,7 +12303,7 @@ class ParserError extends Error {
12148
12303
  }
12149
12304
 
12150
12305
  function isAttrValueToken(token) {
12151
- return Boolean(token && token.type === TokenType.ATTR_VALUE);
12306
+ return token?.type === TokenType.ATTR_VALUE;
12152
12307
  }
12153
12308
  function svgShouldRetainTag(foreignTagName, tagName) {
12154
12309
  return foreignTagName === "svg" && ["title", "desc"].includes(tagName);
@@ -12494,7 +12649,7 @@ class Parser {
12494
12649
  * ^^^ ^^^ ^^^ (null) (null)
12495
12650
  */
12496
12651
  getAttributeValueLocation(token) {
12497
- if (!token || token.type !== TokenType.ATTR_VALUE || token.data[2] === "") {
12652
+ if (token?.type !== TokenType.ATTR_VALUE || token.data[2] === "") {
12498
12653
  return null;
12499
12654
  }
12500
12655
  const quote = token.data[3];
@@ -12510,7 +12665,7 @@ class Parser {
12510
12665
  */
12511
12666
  getAttributeLocation(key, value) {
12512
12667
  const begin = key.location;
12513
- const end = value && value.type === TokenType.ATTR_VALUE ? value.location : void 0;
12668
+ const end = value?.type === TokenType.ATTR_VALUE ? value.location : void 0;
12514
12669
  return {
12515
12670
  filename: begin.filename,
12516
12671
  line: begin.line,
@@ -12532,7 +12687,11 @@ class Parser {
12532
12687
  throw new Error(`Failed to parse directive "${text}"`);
12533
12688
  }
12534
12689
  if (!isValidDirective(action)) {
12535
- throw new ParserError(token.location, `Unknown directive "${action}"`);
12690
+ this.trigger("parse:error", {
12691
+ location: token.location,
12692
+ message: `Unknown directive "${action}"`
12693
+ });
12694
+ return;
12536
12695
  }
12537
12696
  const [, data, separator2, comment] = match;
12538
12697
  const prefix = "html-validate-";
@@ -12625,7 +12784,9 @@ class Parser {
12625
12784
  while (!it.done) {
12626
12785
  const token = it.value;
12627
12786
  yield token;
12628
- if (token.type === search) return;
12787
+ if (token.type === search) {
12788
+ return;
12789
+ }
12629
12790
  it = this.next(tokenStream);
12630
12791
  }
12631
12792
  throw new ParserError(
@@ -12788,6 +12949,9 @@ class Engine {
12788
12949
  parser.on("directive", (_2, event) => {
12789
12950
  this.processDirective(event, parser, directiveContext);
12790
12951
  });
12952
+ parser.on("parse:error", (_2, event) => {
12953
+ this.reportError("parser-error", event.message, event.location);
12954
+ });
12791
12955
  try {
12792
12956
  parser.parseHtml(source);
12793
12957
  } catch (e) {
@@ -12985,7 +13149,9 @@ class Engine {
12985
13149
  const availableRules = {};
12986
13150
  for (const plugin of config.getPlugins()) {
12987
13151
  for (const [name, rule] of Object.entries(plugin.rules ?? {})) {
12988
- if (!rule) continue;
13152
+ if (!rule) {
13153
+ continue;
13154
+ }
12989
13155
  availableRules[name] = rule;
12990
13156
  }
12991
13157
  }
@@ -13018,6 +13184,7 @@ class Engine {
13018
13184
  /**
13019
13185
  * Load and setup a rule using current config.
13020
13186
  */
13187
+ /* eslint-disable-next-line @typescript-eslint/max-params -- technical debt */
13021
13188
  loadRule(ruleId, config, severity, options, parser, report) {
13022
13189
  const meta = config.getMetaTable();
13023
13190
  const rule = this.instantiateRule(ruleId, options);