html-validate 7.1.2 → 7.2.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
@@ -3086,7 +3086,7 @@ var TRANSFORMER_API;
3086
3086
  /** @public */
3087
3087
  const name = "html-validate";
3088
3088
  /** @public */
3089
- const version = "7.1.2";
3089
+ const version = "7.2.0";
3090
3090
  /** @public */
3091
3091
  const homepage = "https://html-validate.org";
3092
3092
  /** @public */
@@ -3187,6 +3187,18 @@ function getSchemaValidator(ruleId, properties) {
3187
3187
  };
3188
3188
  return ajv$1.compile(schema);
3189
3189
  }
3190
+ function isErrorDescriptor(value) {
3191
+ return Boolean(value[0] && value[0].message);
3192
+ }
3193
+ function unpackErrorDescriptor(value) {
3194
+ if (isErrorDescriptor(value)) {
3195
+ return value[0];
3196
+ }
3197
+ else {
3198
+ const [node, message, location, context] = value;
3199
+ return { node, message, location, context };
3200
+ }
3201
+ }
3190
3202
  /**
3191
3203
  * @public
3192
3204
  */
@@ -3287,13 +3299,8 @@ class Rule {
3287
3299
  static schema() {
3288
3300
  return null;
3289
3301
  }
3290
- /**
3291
- * Report a new error.
3292
- *
3293
- * Rule must be enabled both globally and on the specific node for this to
3294
- * have any effect.
3295
- */
3296
- report(node, message, location, context) {
3302
+ report(...args) {
3303
+ const { node, message, location, context } = unpackErrorDescriptor(args);
3297
3304
  if (this.isEnabled() && (!node || node.ruleEnabled(this.name))) {
3298
3305
  const where = this.findLocation({ node, location, event: this.event });
3299
3306
  const interpolated = interpolate(message, context !== null && context !== void 0 ? context : {});
@@ -3806,9 +3813,14 @@ class AttrCase extends Rule {
3806
3813
  return;
3807
3814
  }
3808
3815
  const letters = event.key.replace(/[^a-z]+/gi, "");
3809
- if (!this.style.match(letters)) {
3810
- this.report(event.target, `Attribute "${event.key}" should be ${this.style.name}`, event.keyLocation);
3816
+ if (this.style.match(letters)) {
3817
+ return;
3811
3818
  }
3819
+ this.report({
3820
+ node: event.target,
3821
+ message: `Attribute "${event.key}" should be ${this.style.name}`,
3822
+ location: event.keyLocation,
3823
+ });
3812
3824
  });
3813
3825
  }
3814
3826
  isIgnored(node) {
@@ -5108,6 +5120,11 @@ class ElementName extends Rule {
5108
5120
  }
5109
5121
  }
5110
5122
 
5123
+ var ErrorKind;
5124
+ (function (ErrorKind) {
5125
+ ErrorKind["CONTENT"] = "content";
5126
+ ErrorKind["DESCENDANT"] = "descendant";
5127
+ })(ErrorKind || (ErrorKind = {}));
5111
5128
  function getTransparentChildren(node, transparent) {
5112
5129
  if (typeof transparent === "boolean") {
5113
5130
  return node.childElements;
@@ -5121,10 +5138,28 @@ function getTransparentChildren(node, transparent) {
5121
5138
  });
5122
5139
  }
5123
5140
  }
5141
+ function getRuleDescription$1(context) {
5142
+ if (!context) {
5143
+ return [
5144
+ "Some elements has restrictions on what content is allowed.",
5145
+ "This can include both direct children or descendant elements.",
5146
+ ];
5147
+ }
5148
+ switch (context.kind) {
5149
+ case ErrorKind.CONTENT:
5150
+ return [
5151
+ `The \`${context.child}\` element is not permitted as content under the parent \`${context.parent}\` element.`,
5152
+ ];
5153
+ case ErrorKind.DESCENDANT:
5154
+ return [
5155
+ `The \`${context.child}\` element is not permitted as a descendant of the \`${context.ancestor}\` element.`,
5156
+ ];
5157
+ }
5158
+ }
5124
5159
  class ElementPermittedContent extends Rule {
5125
- documentation() {
5160
+ documentation(context) {
5126
5161
  return {
5127
- description: "Some elements has restrictions on what content is allowed. This can include both direct children or descendant elements.",
5162
+ description: getRuleDescription$1(context).join("\n"),
5128
5163
  url: ruleDocumentationUrl("@/rules/element-permitted-content.ts"),
5129
5164
  };
5130
5165
  }
@@ -5133,8 +5168,9 @@ class ElementPermittedContent extends Rule {
5133
5168
  const doc = event.document;
5134
5169
  doc.visitDepthFirst((node) => {
5135
5170
  const parent = node.parent;
5136
- /* dont verify root element, assume any element is allowed */
5137
- if (!parent || parent.isRootElement()) {
5171
+ /* istanbul ignore next: satisfy typescript but will visitDepthFirst()
5172
+ * will not yield nodes without a parent */
5173
+ if (!parent) {
5138
5174
  return;
5139
5175
  }
5140
5176
  /* Run each validation step, stop as soon as any errors are
@@ -5144,7 +5180,6 @@ class ElementPermittedContent extends Rule {
5144
5180
  [
5145
5181
  () => this.validatePermittedContent(node, parent),
5146
5182
  () => this.validatePermittedDescendant(node, parent),
5147
- () => this.validatePermittedAncestors(node),
5148
5183
  ].some((fn) => fn());
5149
5184
  });
5150
5185
  });
@@ -5161,7 +5196,14 @@ class ElementPermittedContent extends Rule {
5161
5196
  }
5162
5197
  validatePermittedContentImpl(cur, parent, rules) {
5163
5198
  if (!Validator.validatePermitted(cur, rules)) {
5164
- this.report(cur, `Element <${cur.tagName}> is not permitted as content in ${parent.annotatedName}`);
5199
+ const child = `<${cur.tagName}>`;
5200
+ const message = `${child} element is not permitted as content under ${parent.annotatedName}`;
5201
+ const context = {
5202
+ kind: ErrorKind.CONTENT,
5203
+ parent: parent.annotatedName,
5204
+ child,
5205
+ };
5206
+ this.report(cur, message, null, context);
5165
5207
  return true;
5166
5208
  }
5167
5209
  /* for transparent elements all/listed children must be validated against
@@ -5192,21 +5234,15 @@ class ElementPermittedContent extends Rule {
5192
5234
  if (Validator.validatePermitted(node, rules)) {
5193
5235
  continue;
5194
5236
  }
5195
- this.report(node, `Element <${node.tagName}> is not permitted as descendant of ${cur.annotatedName}`);
5196
- return true;
5197
- }
5198
- return false;
5199
- }
5200
- validatePermittedAncestors(node) {
5201
- if (!node.meta) {
5202
- return false;
5203
- }
5204
- const rules = node.meta.requiredAncestors;
5205
- if (!rules) {
5206
- return false;
5207
- }
5208
- if (!Validator.validateAncestors(node, rules)) {
5209
- this.report(node, `Element <${node.tagName}> requires an "${rules[0]}" ancestor`);
5237
+ const child = `<${node.tagName}>`;
5238
+ const ancestor = cur.annotatedName;
5239
+ const message = `${child} element is not permitted as a descendant of ${ancestor}`;
5240
+ const context = {
5241
+ kind: ErrorKind.DESCENDANT,
5242
+ ancestor,
5243
+ child,
5244
+ };
5245
+ this.report(node, message, null, context);
5210
5246
  return true;
5211
5247
  }
5212
5248
  return false;
@@ -5273,6 +5309,152 @@ class ElementPermittedOrder extends Rule {
5273
5309
  }
5274
5310
  }
5275
5311
 
5312
+ const CACHE_KEY = Symbol(classifyNodeText.name);
5313
+ var TextClassification;
5314
+ (function (TextClassification) {
5315
+ TextClassification[TextClassification["EMPTY_TEXT"] = 0] = "EMPTY_TEXT";
5316
+ TextClassification[TextClassification["DYNAMIC_TEXT"] = 1] = "DYNAMIC_TEXT";
5317
+ TextClassification[TextClassification["STATIC_TEXT"] = 2] = "STATIC_TEXT";
5318
+ })(TextClassification || (TextClassification = {}));
5319
+ /**
5320
+ * Checks text content of an element.
5321
+ *
5322
+ * Any text is considered including text from descendant elements. Whitespace is
5323
+ * ignored.
5324
+ *
5325
+ * If any text is dynamic `TextClassification.DYNAMIC_TEXT` is returned.
5326
+ */
5327
+ function classifyNodeText(node) {
5328
+ if (node.cacheExists(CACHE_KEY)) {
5329
+ return node.cacheGet(CACHE_KEY);
5330
+ }
5331
+ const text = findTextNodes(node);
5332
+ /* if any text is dynamic classify as dynamic */
5333
+ if (text.some((cur) => cur.isDynamic)) {
5334
+ return node.cacheSet(CACHE_KEY, TextClassification.DYNAMIC_TEXT);
5335
+ }
5336
+ /* if any text has non-whitespace character classify as static */
5337
+ if (text.some((cur) => cur.textContent.match(/\S/) !== null)) {
5338
+ return node.cacheSet(CACHE_KEY, TextClassification.STATIC_TEXT);
5339
+ }
5340
+ /* default to empty */
5341
+ return node.cacheSet(CACHE_KEY, TextClassification.EMPTY_TEXT);
5342
+ }
5343
+ function findTextNodes(node) {
5344
+ let text = [];
5345
+ for (const child of node.childNodes) {
5346
+ switch (child.nodeType) {
5347
+ case NodeType.TEXT_NODE:
5348
+ text.push(child);
5349
+ break;
5350
+ case NodeType.ELEMENT_NODE:
5351
+ text = text.concat(findTextNodes(child));
5352
+ break;
5353
+ }
5354
+ }
5355
+ return text;
5356
+ }
5357
+
5358
+ function hasAltText(image) {
5359
+ const alt = image.getAttribute("alt");
5360
+ /* missing or boolean */
5361
+ if (alt === null || alt.value === null) {
5362
+ return false;
5363
+ }
5364
+ return alt.isDynamic || alt.value.toString() !== "";
5365
+ }
5366
+
5367
+ function hasAriaLabel(node) {
5368
+ const label = node.getAttribute("aria-label");
5369
+ /* missing or boolean */
5370
+ if (label === null || label.value === null) {
5371
+ return false;
5372
+ }
5373
+ return label.isDynamic || label.value.toString() !== "";
5374
+ }
5375
+
5376
+ /**
5377
+ * Joins a list of words into natural language.
5378
+ *
5379
+ * - `["foo"]` becomes `"foo"`
5380
+ * - `["foo", "bar"]` becomes `"foo or bar"`
5381
+ * - `["foo", "bar", "baz"]` becomes `"foo, bar or baz"`
5382
+ * - and so on...
5383
+ *
5384
+ * @internal
5385
+ * @param values - List of words to join
5386
+ * @param conjunction - Conjunction for the last element.
5387
+ * @returns String with the words naturally joined with a conjunction.
5388
+ */
5389
+ function naturalJoin(values, conjunction = "or") {
5390
+ switch (values.length) {
5391
+ case 0:
5392
+ return "";
5393
+ case 1:
5394
+ return values[0];
5395
+ case 2:
5396
+ return `${values[0]} ${conjunction} ${values[1]}`;
5397
+ default:
5398
+ return `${values.slice(0, -1).join(", ")} ${conjunction} ${values.slice(-1)[0]}`;
5399
+ }
5400
+ }
5401
+
5402
+ function isTagnameOnly(value) {
5403
+ return Boolean(value.match(/^[a-zA-Z0-9-]+$/));
5404
+ }
5405
+ function getRuleDescription(context) {
5406
+ if (!context) {
5407
+ return [
5408
+ "Some elements has restrictions on what content is allowed.",
5409
+ "This can include both direct children or descendant elements.",
5410
+ ];
5411
+ }
5412
+ const escaped = context.ancestor.map((it) => `\`${it}\``);
5413
+ return [`The \`${context.child}\` element requires a ${naturalJoin(escaped)} ancestor.`];
5414
+ }
5415
+ class ElementRequiredAncestor extends Rule {
5416
+ documentation(context) {
5417
+ return {
5418
+ description: getRuleDescription(context).join("\n"),
5419
+ url: ruleDocumentationUrl("@/rules/element-required-ancestor.ts"),
5420
+ };
5421
+ }
5422
+ setup() {
5423
+ this.on("dom:ready", (event) => {
5424
+ const doc = event.document;
5425
+ doc.visitDepthFirst((node) => {
5426
+ const parent = node.parent;
5427
+ /* istanbul ignore next: satisfy typescript but will visitDepthFirst()
5428
+ * will not yield nodes without a parent */
5429
+ if (!parent) {
5430
+ return;
5431
+ }
5432
+ this.validateRequiredAncestors(node);
5433
+ });
5434
+ });
5435
+ }
5436
+ validateRequiredAncestors(node) {
5437
+ if (!node.meta) {
5438
+ return;
5439
+ }
5440
+ const rules = node.meta.requiredAncestors;
5441
+ if (!rules) {
5442
+ return;
5443
+ }
5444
+ if (Validator.validateAncestors(node, rules)) {
5445
+ return;
5446
+ }
5447
+ const ancestor = rules.map((it) => (isTagnameOnly(it) ? `<${it}>` : `"${it}"`));
5448
+ const child = `<${node.tagName}>`;
5449
+ const message = `<${node.tagName}> element requires a ${naturalJoin(ancestor)} ancestor`;
5450
+ const context = {
5451
+ ancestor,
5452
+ child,
5453
+ };
5454
+ this.report(node, message, null, context);
5455
+ }
5456
+ }
5457
+
5276
5458
  class ElementRequiredAttributes extends Rule {
5277
5459
  documentation(context) {
5278
5460
  const docs = {
@@ -5350,52 +5532,6 @@ class ElementRequiredContent extends Rule {
5350
5532
  }
5351
5533
  }
5352
5534
 
5353
- const CACHE_KEY = Symbol(classifyNodeText.name);
5354
- var TextClassification;
5355
- (function (TextClassification) {
5356
- TextClassification[TextClassification["EMPTY_TEXT"] = 0] = "EMPTY_TEXT";
5357
- TextClassification[TextClassification["DYNAMIC_TEXT"] = 1] = "DYNAMIC_TEXT";
5358
- TextClassification[TextClassification["STATIC_TEXT"] = 2] = "STATIC_TEXT";
5359
- })(TextClassification || (TextClassification = {}));
5360
- /**
5361
- * Checks text content of an element.
5362
- *
5363
- * Any text is considered including text from descendant elements. Whitespace is
5364
- * ignored.
5365
- *
5366
- * If any text is dynamic `TextClassification.DYNAMIC_TEXT` is returned.
5367
- */
5368
- function classifyNodeText(node) {
5369
- if (node.cacheExists(CACHE_KEY)) {
5370
- return node.cacheGet(CACHE_KEY);
5371
- }
5372
- const text = findTextNodes(node);
5373
- /* if any text is dynamic classify as dynamic */
5374
- if (text.some((cur) => cur.isDynamic)) {
5375
- return node.cacheSet(CACHE_KEY, TextClassification.DYNAMIC_TEXT);
5376
- }
5377
- /* if any text has non-whitespace character classify as static */
5378
- if (text.some((cur) => cur.textContent.match(/\S/) !== null)) {
5379
- return node.cacheSet(CACHE_KEY, TextClassification.STATIC_TEXT);
5380
- }
5381
- /* default to empty */
5382
- return node.cacheSet(CACHE_KEY, TextClassification.EMPTY_TEXT);
5383
- }
5384
- function findTextNodes(node) {
5385
- let text = [];
5386
- for (const child of node.childNodes) {
5387
- switch (child.nodeType) {
5388
- case NodeType.TEXT_NODE:
5389
- text.push(child);
5390
- break;
5391
- case NodeType.ELEMENT_NODE:
5392
- text = text.concat(findTextNodes(child));
5393
- break;
5394
- }
5395
- }
5396
- return text;
5397
- }
5398
-
5399
5535
  const selector = ["h1", "h2", "h3", "h4", "h5", "h6"].join(",");
5400
5536
  class EmptyHeading extends Rule {
5401
5537
  documentation() {
@@ -7576,24 +7712,6 @@ class TelNonBreaking extends Rule {
7576
7712
  }
7577
7713
  }
7578
7714
 
7579
- function hasAltText(image) {
7580
- const alt = image.getAttribute("alt");
7581
- /* missing or boolean */
7582
- if (alt === null || alt.value === null) {
7583
- return false;
7584
- }
7585
- return alt.isDynamic || alt.value.toString() !== "";
7586
- }
7587
-
7588
- function hasAriaLabel(node) {
7589
- const label = node.getAttribute("aria-label");
7590
- /* missing or boolean */
7591
- if (label === null || label.value === null) {
7592
- return false;
7593
- }
7594
- return label.isDynamic || label.value.toString() !== "";
7595
- }
7596
-
7597
7715
  /**
7598
7716
  * Check if attribute is present and non-empty or dynamic.
7599
7717
  */
@@ -9771,13 +9889,13 @@ class VoidStyle extends Rule {
9771
9889
  return;
9772
9890
  }
9773
9891
  if (this.shouldBeOmitted(node)) {
9774
- this.report(node, `Expected omitted end tag <${node.tagName}> instead of self-closing element <${node.tagName}/>`);
9892
+ this.reportError(node, `Expected omitted end tag <${node.tagName}> instead of self-closing element <${node.tagName}/>`);
9775
9893
  }
9776
9894
  if (this.shouldBeSelfClosed(node)) {
9777
- this.report(node, `Expected self-closing element <${node.tagName}/> instead of omitted end-tag <${node.tagName}>`);
9895
+ this.reportError(node, `Expected self-closing element <${node.tagName}/> instead of omitted end-tag <${node.tagName}>`);
9778
9896
  }
9779
9897
  }
9780
- report(node, message) {
9898
+ reportError(node, message) {
9781
9899
  const context = {
9782
9900
  style: this.style,
9783
9901
  tagName: node.tagName,
@@ -10089,6 +10207,7 @@ const bundledRules = {
10089
10207
  "element-permitted-content": ElementPermittedContent,
10090
10208
  "element-permitted-occurrences": ElementPermittedOccurrences,
10091
10209
  "element-permitted-order": ElementPermittedOrder,
10210
+ "element-required-ancestor": ElementRequiredAncestor,
10092
10211
  "element-required-attributes": ElementRequiredAttributes,
10093
10212
  "element-required-content": ElementRequiredContent,
10094
10213
  "empty-heading": EmptyHeading,
@@ -10196,6 +10315,7 @@ const config$1 = {
10196
10315
  "element-permitted-content": "error",
10197
10316
  "element-permitted-occurrences": "error",
10198
10317
  "element-permitted-order": "error",
10318
+ "element-required-ancestor": "error",
10199
10319
  "element-required-attributes": "error",
10200
10320
  "element-required-content": "error",
10201
10321
  "empty-heading": "error",
@@ -10254,6 +10374,7 @@ const config = {
10254
10374
  "element-permitted-content": "error",
10255
10375
  "element-permitted-occurrences": "error",
10256
10376
  "element-permitted-order": "error",
10377
+ "element-required-ancestor": "error",
10257
10378
  "element-required-attributes": "error",
10258
10379
  "element-required-content": "error",
10259
10380
  "multiple-labeled-controls": "error",
@@ -10333,6 +10454,7 @@ class ResolvedConfig {
10333
10454
  });
10334
10455
  }
10335
10456
  catch (err) {
10457
+ /* istanbul ignore next: only used as a fallback */
10336
10458
  const message = err instanceof Error ? err.message : String(err);
10337
10459
  throw new NestedError(`When transforming "${source.filename}": ${message}`, ensureError(err));
10338
10460
  }
@@ -10345,7 +10467,7 @@ class ResolvedConfig {
10345
10467
  * Wrapper around [[transformSource]] which reads a file before passing it
10346
10468
  * as-is to transformSource.
10347
10469
  *
10348
- * @param source - Filename to transform (according to configured
10470
+ * @param filename - Filename to transform (according to configured
10349
10471
  * transformations)
10350
10472
  * @returns A list of transformed sources ready for validation.
10351
10473
  */
@@ -10501,7 +10623,9 @@ class Config {
10501
10623
  var _a;
10502
10624
  const valid = validator(configData);
10503
10625
  if (!valid) {
10504
- throw new SchemaValidationError(filename, `Invalid configuration`, configData, configurationSchema, (_a = validator.errors) !== null && _a !== void 0 ? _a : []);
10626
+ throw new SchemaValidationError(filename, `Invalid configuration`, configData, configurationSchema,
10627
+ /* istanbul ignore next: will be set when a validation error has occurred */
10628
+ (_a = validator.errors) !== null && _a !== void 0 ? _a : []);
10505
10629
  }
10506
10630
  if (configData.rules) {
10507
10631
  const normalizedRules = Config.getRulesObject(configData.rules);
@@ -10610,6 +10734,7 @@ class Config {
10610
10734
  metaTable.loadFromObject(legacyRequire(entry));
10611
10735
  }
10612
10736
  catch (err) {
10737
+ /* istanbul ignore next: only used as a fallback */
10613
10738
  const message = err instanceof Error ? err.message : String(err);
10614
10739
  throw new ConfigError(`Failed to load elements from "${entry}": ${message}`, ensureError(err));
10615
10740
  }
@@ -10631,6 +10756,7 @@ class Config {
10631
10756
  *
10632
10757
  * @internal primary purpose is unittests
10633
10758
  */
10759
+ /* istanbul ignore next: used for testing only */
10634
10760
  get() {
10635
10761
  const config = { ...this.config };
10636
10762
  if (config.elements) {
@@ -10653,6 +10779,7 @@ class Config {
10653
10779
  */
10654
10780
  getRules() {
10655
10781
  var _a;
10782
+ /* istanbul ignore next: only used as a fallback */
10656
10783
  return Config.getRulesObject((_a = this.config.rules) !== null && _a !== void 0 ? _a : {});
10657
10784
  }
10658
10785
  static getRulesObject(src) {
@@ -10687,6 +10814,7 @@ class Config {
10687
10814
  return plugin;
10688
10815
  }
10689
10816
  catch (err) {
10817
+ /* istanbul ignore next: only used as a fallback */
10690
10818
  const message = err instanceof Error ? err.message : String(err);
10691
10819
  throw new ConfigError(`Failed to load plugin "${moduleName}": ${message}`, ensureError(err));
10692
10820
  }
@@ -11602,9 +11730,9 @@ class Reporter {
11602
11730
  if (!(location.filename in this.result)) {
11603
11731
  this.result[location.filename] = [];
11604
11732
  }
11605
- this.result[location.filename].push({
11733
+ const ruleUrl = (_a = rule.documentation(context)) === null || _a === void 0 ? void 0 : _a.url;
11734
+ const entry = {
11606
11735
  ruleId: rule.name,
11607
- ruleUrl: (_a = rule.documentation(context)) === null || _a === void 0 ? void 0 : _a.url,
11608
11736
  severity,
11609
11737
  message,
11610
11738
  offset: location.offset,
@@ -11614,8 +11742,14 @@ class Reporter {
11614
11742
  selector() {
11615
11743
  return node ? node.generateSelector() : null;
11616
11744
  },
11617
- context,
11618
- });
11745
+ };
11746
+ if (ruleUrl) {
11747
+ entry.ruleUrl = ruleUrl;
11748
+ }
11749
+ if (context) {
11750
+ entry.context = context;
11751
+ }
11752
+ this.result[location.filename].push(entry);
11619
11753
  }
11620
11754
  addManual(filename, message) {
11621
11755
  if (!(filename in this.result)) {
@@ -12029,7 +12163,7 @@ class Engine {
12029
12163
  offset: location.offset,
12030
12164
  line: location.line,
12031
12165
  column: location.column,
12032
- size: location.size || 0,
12166
+ size: location.size,
12033
12167
  selector: () => null,
12034
12168
  });
12035
12169
  }
@@ -12459,12 +12593,12 @@ class FileSystemConfigLoader extends ConfigLoader {
12459
12593
  * `null` if no configuration files are found.
12460
12594
  */
12461
12595
  fromFilename(filename) {
12462
- var _a;
12463
12596
  if (filename === "inline") {
12464
12597
  return null;
12465
12598
  }
12466
- if (this.cache.has(filename)) {
12467
- return (_a = this.cache.get(filename)) !== null && _a !== void 0 ? _a : null;
12599
+ const cache = this.cache.get(filename);
12600
+ if (cache) {
12601
+ return cache;
12468
12602
  }
12469
12603
  let found = false;
12470
12604
  let current = path.resolve(path.dirname(filename));