html-validate 10.1.2 → 10.3.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
@@ -1486,7 +1486,7 @@ function sliceLocation(location, begin, end, wrap) {
1486
1486
  if (wrap) {
1487
1487
  let index = -1;
1488
1488
  const col = sliced.column;
1489
- do {
1489
+ for (; ; ) {
1490
1490
  index = wrap.indexOf("\n", index + 1);
1491
1491
  if (index >= 0 && index < begin) {
1492
1492
  sliced.column = col - (index + 1);
@@ -1494,7 +1494,7 @@ function sliceLocation(location, begin, end, wrap) {
1494
1494
  } else {
1495
1495
  break;
1496
1496
  }
1497
- } while (true);
1497
+ }
1498
1498
  }
1499
1499
  return sliced;
1500
1500
  }
@@ -1509,6 +1509,7 @@ var State = /* @__PURE__ */ ((State2) => {
1509
1509
  State2[State2["SCRIPT"] = 7] = "SCRIPT";
1510
1510
  State2[State2["STYLE"] = 8] = "STYLE";
1511
1511
  State2[State2["TEXTAREA"] = 9] = "TEXTAREA";
1512
+ State2[State2["TITLE"] = 10] = "TITLE";
1512
1513
  return State2;
1513
1514
  })(State || {});
1514
1515
 
@@ -1517,6 +1518,7 @@ var ContentModel = /* @__PURE__ */ ((ContentModel2) => {
1517
1518
  ContentModel2[ContentModel2["SCRIPT"] = 2] = "SCRIPT";
1518
1519
  ContentModel2[ContentModel2["STYLE"] = 3] = "STYLE";
1519
1520
  ContentModel2[ContentModel2["TEXTAREA"] = 4] = "TEXTAREA";
1521
+ ContentModel2[ContentModel2["TITLE"] = 5] = "TITLE";
1520
1522
  return ContentModel2;
1521
1523
  })(ContentModel || {});
1522
1524
  class Context {
@@ -3270,7 +3272,7 @@ class Validator {
3270
3272
  static validatePermittedCategory(node, category, defaultMatch) {
3271
3273
  const [, rawCategory] = /^(@?.*?)([?*]?)$/.exec(category);
3272
3274
  if (!rawCategory.startsWith("@")) {
3273
- return node.tagName === rawCategory;
3275
+ return node.matches(rawCategory);
3274
3276
  }
3275
3277
  if (!node.meta) {
3276
3278
  return defaultMatch;
@@ -3471,7 +3473,7 @@ function inAccessibilityTree(node) {
3471
3473
  function isAriaHiddenImpl(node) {
3472
3474
  const getAriaHiddenAttr = (node2) => {
3473
3475
  const ariaHidden = node2.getAttribute("aria-hidden");
3474
- return Boolean(ariaHidden && ariaHidden.value === "true");
3476
+ return ariaHidden?.value === "true";
3475
3477
  };
3476
3478
  return {
3477
3479
  byParent: node.parent ? isAriaHidden(node.parent) : false,
@@ -3706,7 +3708,7 @@ function getSchemaValidator(ruleId, properties) {
3706
3708
  return ajv$1.compile(schema);
3707
3709
  }
3708
3710
  function isErrorDescriptor(value) {
3709
- return Boolean(value[0] && value[0].message);
3711
+ return Boolean(value[0] && value[0]["message"]);
3710
3712
  }
3711
3713
  function unpackErrorDescriptor(value) {
3712
3714
  if (isErrorDescriptor(value)) {
@@ -3977,8 +3979,7 @@ class Rule {
3977
3979
  * @returns Rule documentation and url with additional details or `null` if no
3978
3980
  * additional documentation is available.
3979
3981
  */
3980
- /* eslint-disable-next-line @typescript-eslint/no-unused-vars -- technical debt, prototype should be moved to interface */
3981
- documentation(context) {
3982
+ documentation(_context) {
3982
3983
  return null;
3983
3984
  }
3984
3985
  }
@@ -4307,7 +4308,7 @@ class AriaHiddenBody extends Rule {
4307
4308
  const defaults$w = {
4308
4309
  allowAnyNamable: false
4309
4310
  };
4310
- const whitelisted = [
4311
+ const allowlist = /* @__PURE__ */ new Set([
4311
4312
  "main",
4312
4313
  "nav",
4313
4314
  "table",
@@ -4320,18 +4321,19 @@ const whitelisted = [
4320
4321
  "article",
4321
4322
  "dialog",
4322
4323
  "form",
4324
+ "iframe",
4323
4325
  "img",
4324
4326
  "area",
4325
4327
  "fieldset",
4326
4328
  "summary",
4327
4329
  "figure"
4328
- ];
4330
+ ]);
4329
4331
  function isValidUsage(target, meta) {
4330
4332
  const explicit = meta.attributes["aria-label"];
4331
4333
  if (explicit) {
4332
4334
  return true;
4333
4335
  }
4334
- if (whitelisted.includes(target.tagName)) {
4336
+ if (allowlist.has(target.tagName)) {
4335
4337
  return true;
4336
4338
  }
4337
4339
  if (target.hasAttribute("role")) {
@@ -4349,7 +4351,7 @@ class AriaLabelMisuse extends Rule {
4349
4351
  constructor(options) {
4350
4352
  super({ ...defaults$w, ...options });
4351
4353
  }
4352
- documentation() {
4354
+ documentation(context) {
4353
4355
  const valid = [
4354
4356
  "Interactive elements",
4355
4357
  "Labelable elements",
@@ -4363,25 +4365,41 @@ class AriaLabelMisuse extends Rule {
4363
4365
  "`<summary>`",
4364
4366
  "`<table>`, `<td>` and `<th>`"
4365
4367
  ];
4366
- const lines = valid.map((it) => `- ${it}
4367
- `).join("");
4368
- return {
4369
- description: `\`aria-label\` can only be used on:
4370
-
4371
- ${lines}`,
4372
- url: "https://html-validate.org/rules/aria-label-misuse.html"
4373
- };
4368
+ const lines = valid.map((it) => `- ${it}`);
4369
+ const url = "https://html-validate.org/rules/aria-label-misuse.html";
4370
+ if (context.allowsNaming) {
4371
+ return {
4372
+ description: [
4373
+ `\`${context.attr}\` is strictly allowed but is not recommended to be used on this element.`,
4374
+ `\`${context.attr}\` can only be used on:`,
4375
+ "",
4376
+ ...lines
4377
+ ].join("\n"),
4378
+ url
4379
+ };
4380
+ } else {
4381
+ return {
4382
+ description: [`\`${context.attr}\` can only be used on:`, "", ...lines].join("\n"),
4383
+ url
4384
+ };
4385
+ }
4374
4386
  }
4375
4387
  setup() {
4376
4388
  this.on("dom:ready", (event) => {
4377
4389
  const { document } = event;
4378
- for (const target of document.querySelectorAll("[aria-label]")) {
4379
- this.validateElement(target);
4390
+ for (const target of document.querySelectorAll("[aria-label], [aria-labelledby]")) {
4391
+ const ariaLabel = target.getAttribute("aria-label");
4392
+ if (ariaLabel) {
4393
+ this.validateElement(target, ariaLabel, "aria-label");
4394
+ }
4395
+ const ariaLabelledby = target.getAttribute("aria-labelledby");
4396
+ if (ariaLabelledby) {
4397
+ this.validateElement(target, ariaLabelledby, "aria-labelledby");
4398
+ }
4380
4399
  }
4381
4400
  });
4382
4401
  }
4383
- validateElement(target) {
4384
- const attr = target.getAttribute("aria-label");
4402
+ validateElement(target, attr, key) {
4385
4403
  if (!attr.value || attr.valueMatches("", false)) {
4386
4404
  return;
4387
4405
  }
@@ -4392,10 +4410,26 @@ ${lines}`,
4392
4410
  if (isValidUsage(target, meta)) {
4393
4411
  return;
4394
4412
  }
4395
- if (this.options.allowAnyNamable && ariaNaming(target) === "allowed") {
4413
+ const allowsNaming = ariaNaming(target) === "allowed";
4414
+ if (allowsNaming && this.options.allowAnyNamable) {
4396
4415
  return;
4397
4416
  }
4398
- this.report(target, `"aria-label" cannot be used on this element`, attr.keyLocation);
4417
+ const context = { attr: key, allowsNaming };
4418
+ if (allowsNaming) {
4419
+ this.report({
4420
+ node: target,
4421
+ location: attr.keyLocation,
4422
+ context,
4423
+ message: `"{{ attr }}" is strictly allowed but is not recommended to be used on this element`
4424
+ });
4425
+ } else {
4426
+ this.report({
4427
+ node: target,
4428
+ location: attr.keyLocation,
4429
+ context,
4430
+ message: `"{{ attr }}" cannot be used on this element`
4431
+ });
4432
+ }
4399
4433
  }
4400
4434
  }
4401
4435
 
@@ -4571,6 +4605,8 @@ const MATCH_STYLE_DATA = /^[^]*?(?=<\/style)/;
4571
4605
  const MATCH_STYLE_END = /^<(\/)(style)/;
4572
4606
  const MATCH_TEXTAREA_DATA = /^[^]*?(?=<\/textarea)/;
4573
4607
  const MATCH_TEXTAREA_END = /^<(\/)(textarea)/;
4608
+ const MATCH_TITLE_DATA = /^[^]*?(?=<\/title)/;
4609
+ const MATCH_TITLE_END = /^<(\/)(title)/;
4574
4610
  const MATCH_DIRECTIVE = /^(<!--\s*\[html-validate-)([a-z0-9-]+)(\s*)(.*?)(]?\s*-->)/;
4575
4611
  const MATCH_COMMENT = /^<!--([^]*?)-->/;
4576
4612
  const MATCH_CONDITIONAL = /^<!\[([^\]]*?)\]>/;
@@ -4616,6 +4652,9 @@ class Lexer {
4616
4652
  case State.TEXTAREA:
4617
4653
  yield* this.tokenizeTextarea(context);
4618
4654
  break;
4655
+ case State.TITLE:
4656
+ yield* this.tokenizeTitle(context);
4657
+ break;
4619
4658
  /* istanbul ignore next: sanity check: should not happen unless adding new states */
4620
4659
  default:
4621
4660
  this.unhandled(context);
@@ -4690,6 +4729,8 @@ class Lexer {
4690
4729
  context.contentModel = ContentModel.STYLE;
4691
4730
  } else if (data[0] === "<textarea") {
4692
4731
  context.contentModel = ContentModel.TEXTAREA;
4732
+ } else if (data[0] === "<title") {
4733
+ context.contentModel = ContentModel.TITLE;
4693
4734
  } else {
4694
4735
  context.contentModel = ContentModel.TEXT;
4695
4736
  }
@@ -4747,6 +4788,12 @@ class Lexer {
4747
4788
  } else {
4748
4789
  return State.TEXT;
4749
4790
  }
4791
+ case ContentModel.TITLE:
4792
+ if (selfClosed) {
4793
+ return State.TITLE;
4794
+ } else {
4795
+ return State.TEXT;
4796
+ }
4750
4797
  }
4751
4798
  }
4752
4799
  yield* this.match(
@@ -4821,6 +4868,16 @@ class Lexer {
4821
4868
  "expected </textarea>"
4822
4869
  );
4823
4870
  }
4871
+ *tokenizeTitle(context) {
4872
+ yield* this.match(
4873
+ context,
4874
+ [
4875
+ [MATCH_TITLE_END, State.TAG, TokenType.TAG_OPEN],
4876
+ [MATCH_TITLE_DATA, State.TITLE, TokenType.TEXT]
4877
+ ],
4878
+ "expected </title>"
4879
+ );
4880
+ }
4824
4881
  }
4825
4882
 
4826
4883
  const whitespace = /(\s+)/;
@@ -5090,9 +5147,6 @@ class AttributeAllowedValues extends Rule {
5090
5147
  description: "Attribute has invalid value.",
5091
5148
  url: "https://html-validate.org/rules/attribute-allowed-values.html"
5092
5149
  };
5093
- if (!context) {
5094
- return docs;
5095
- }
5096
5150
  const { allowed, attribute, element, value } = context;
5097
5151
  if (allowed.enum) {
5098
5152
  const allowedList = allowed.enum.map((value2) => {
@@ -5927,7 +5981,10 @@ class ElementName extends Rule {
5927
5981
 
5928
5982
  function isNativeTemplate(node) {
5929
5983
  const { tagName, meta } = node;
5930
- return Boolean(tagName === "template" && meta?.templateRoot && meta?.scriptSupporting);
5984
+ if (!meta) {
5985
+ return false;
5986
+ }
5987
+ return Boolean(tagName === "template" && meta.templateRoot && meta.scriptSupporting);
5931
5988
  }
5932
5989
  function getTransparentChildren(node, transparent) {
5933
5990
  if (typeof transparent === "boolean") {
@@ -6156,7 +6213,7 @@ class ElementPermittedParent extends Rule {
6156
6213
  }
6157
6214
  const rules = node.meta?.permittedParent;
6158
6215
  if (!rules) {
6159
- return false;
6216
+ return;
6160
6217
  }
6161
6218
  if (Validator.validatePermitted(parent, rules)) {
6162
6219
  return;
@@ -6223,14 +6280,10 @@ class ElementRequiredAncestor extends Rule {
6223
6280
 
6224
6281
  class ElementRequiredAttributes extends Rule {
6225
6282
  documentation(context) {
6226
- const docs = {
6227
- description: "Element is missing a required attribute",
6283
+ return {
6284
+ description: `The \`<${context.element}>\` element is required to have a \`${context.attribute}\` attribute.`,
6228
6285
  url: "https://html-validate.org/rules/element-required-attributes.html"
6229
6286
  };
6230
- if (context) {
6231
- docs.description = `The <${context.element}> element is required to have a "${context.attribute}" attribute.`;
6232
- }
6233
- return docs;
6234
6287
  }
6235
6288
  setup() {
6236
6289
  this.on("tag:end", (event) => {
@@ -7737,6 +7790,9 @@ class NoImplicitButtonType extends Rule {
7737
7790
  setup() {
7738
7791
  this.on("element:ready", isRelevant$2, (event) => {
7739
7792
  const { target } = event;
7793
+ if (target.parent?.is("select")) {
7794
+ return;
7795
+ }
7740
7796
  const attr = target.getAttribute("type");
7741
7797
  if (!attr) {
7742
7798
  this.report({
@@ -8233,10 +8289,9 @@ class NoSelfClosing extends Rule {
8233
8289
  }
8234
8290
  };
8235
8291
  }
8236
- documentation(tagName) {
8237
- tagName = tagName || "element";
8292
+ documentation(context) {
8238
8293
  return {
8239
- description: `Self-closing elements are disallowed. Use regular end tag <${tagName}></${tagName}> instead of self-closing <${tagName}/>.`,
8294
+ description: `Self-closing elements are disallowed. Use regular end tag <${context}></${context}> instead of self-closing <${context}/>.`,
8240
8295
  url: "https://html-validate.org/rules/no-self-closing.html"
8241
8296
  };
8242
8297
  }
@@ -8814,7 +8869,7 @@ class ScriptElement extends Rule {
8814
8869
  setup() {
8815
8870
  this.on("tag:end", (event) => {
8816
8871
  const node = event.target;
8817
- if (!node || node.tagName !== "script") {
8872
+ if (node?.tagName !== "script") {
8818
8873
  return;
8819
8874
  }
8820
8875
  if (node.closed !== NodeClosed.EndTag) {
@@ -9077,6 +9132,9 @@ function haveAccessibleText(node) {
9077
9132
  if (node.is("img") && hasNonEmptyAttribute(node, "alt")) {
9078
9133
  return true;
9079
9134
  }
9135
+ if (node.is("selectedcontent")) {
9136
+ return true;
9137
+ }
9080
9138
  if (hasDefaultText(node)) {
9081
9139
  return true;
9082
9140
  }
@@ -10027,13 +10085,13 @@ class ValidID extends Rule {
10027
10085
  }
10028
10086
 
10029
10087
  class VoidContent extends Rule {
10030
- documentation(tagName) {
10088
+ documentation(context) {
10031
10089
  const doc = {
10032
10090
  description: "HTML void elements cannot have any content and must not have content or end tag.",
10033
10091
  url: "https://html-validate.org/rules/void-content.html"
10034
10092
  };
10035
- if (tagName) {
10036
- doc.description = `<${tagName}> is a void element and must not have content or end tag.`;
10093
+ if (context) {
10094
+ doc.description = `<${context}> is a void element and must not have content or end tag.`;
10037
10095
  }
10038
10096
  return doc;
10039
10097
  }
@@ -10336,7 +10394,7 @@ class H37 extends Rule {
10336
10394
  const defaults$1 = {
10337
10395
  strict: false
10338
10396
  };
10339
- const { enum: validScopes } = html5.th.attributes?.scope;
10397
+ const { enum: validScopes } = html5.th.attributes.scope;
10340
10398
  const joinedScopes = naturalJoin(validScopes);
10341
10399
  const td = 0;
10342
10400
  const th = 1;
@@ -10434,7 +10492,7 @@ class H67 extends Rule {
10434
10492
  setup() {
10435
10493
  this.on("tag:end", (event) => {
10436
10494
  const node = event.target;
10437
- if (!node || node.tagName !== "img") {
10495
+ if (node?.tagName !== "img") {
10438
10496
  return;
10439
10497
  }
10440
10498
  const title = node.getAttribute("title");
@@ -11878,7 +11936,7 @@ class EventHandler {
11878
11936
  }
11879
11937
 
11880
11938
  const name = "html-validate";
11881
- const version = "10.1.2";
11939
+ const version = "10.3.0";
11882
11940
  const bugs = "https://gitlab.com/html-validate/html-validate/issues/new";
11883
11941
 
11884
11942
  function freeze(src) {
@@ -12611,9 +12669,9 @@ class Parser {
12611
12669
  * Trigger close events for any still open elements.
12612
12670
  */
12613
12671
  closeTree(source, location) {
12614
- let active;
12615
12672
  const documentElement = this.dom.root;
12616
- while ((active = this.dom.getActive()) && !active.isRootElement()) {
12673
+ let active = this.dom.getActive();
12674
+ while (!active.isRootElement()) {
12617
12675
  if (active.meta?.implicitClosed) {
12618
12676
  active.closed = NodeClosed.ImplicitClosed;
12619
12677
  this.closeElement(source, documentElement, active, location);
@@ -12621,6 +12679,7 @@ class Parser {
12621
12679
  this.closeElement(source, null, active, location);
12622
12680
  }
12623
12681
  this.dom.popActive();
12682
+ active = this.dom.getActive();
12624
12683
  }
12625
12684
  }
12626
12685
  }
@@ -12987,8 +13046,8 @@ function getUnnamedTransformerFromPlugin(name, plugin) {
12987
13046
  throw new ConfigError(`Plugin does not expose any transformers`);
12988
13047
  }
12989
13048
  if (typeof plugin.transformer !== "function") {
12990
- if (plugin.transformer.default) {
12991
- return plugin.transformer.default;
13049
+ if (plugin.transformer["default"]) {
13050
+ return plugin.transformer["default"];
12992
13051
  }
12993
13052
  throw new ConfigError(
12994
13053
  `Transformer "${name}" refers to unnamed transformer but plugin exposes only named.`
@@ -13217,7 +13276,7 @@ function checkstyleFormatter(results) {
13217
13276
  output += "</checkstyle>\n";
13218
13277
  return output;
13219
13278
  }
13220
- const formatter$3 = checkstyleFormatter;
13279
+ const formatter$2 = checkstyleFormatter;
13221
13280
 
13222
13281
  const defaults = {
13223
13282
  showLink: true,
@@ -13392,7 +13451,7 @@ function codeframe(results, options) {
13392
13451
  function jsonFormatter(results) {
13393
13452
  return JSON.stringify(results);
13394
13453
  }
13395
- const formatter$2 = jsonFormatter;
13454
+ const formatter$1 = jsonFormatter;
13396
13455
 
13397
13456
  function linkSummary(results) {
13398
13457
  const urls = results.reduce((result, it) => {
@@ -13421,7 +13480,6 @@ function stylish(results) {
13421
13480
  const links = linkSummary(results);
13422
13481
  return `${errors}${links}`;
13423
13482
  }
13424
- const formatter$1 = stylish;
13425
13483
 
13426
13484
  function textFormatter(results) {
13427
13485
  let output = "";
@@ -13451,10 +13509,10 @@ function textFormatter(results) {
13451
13509
  const formatter = textFormatter;
13452
13510
 
13453
13511
  const availableFormatters = {
13454
- checkstyle: formatter$3,
13512
+ checkstyle: formatter$2,
13455
13513
  codeframe,
13456
- json: formatter$2,
13457
- stylish: formatter$1,
13514
+ json: formatter$1,
13515
+ stylish,
13458
13516
  text: formatter
13459
13517
  };
13460
13518
  function getFormatter(name) {