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/cjs/core.js CHANGED
@@ -1495,7 +1495,7 @@ function sliceLocation(location, begin, end, wrap) {
1495
1495
  if (wrap) {
1496
1496
  let index = -1;
1497
1497
  const col = sliced.column;
1498
- do {
1498
+ for (; ; ) {
1499
1499
  index = wrap.indexOf("\n", index + 1);
1500
1500
  if (index >= 0 && index < begin) {
1501
1501
  sliced.column = col - (index + 1);
@@ -1503,7 +1503,7 @@ function sliceLocation(location, begin, end, wrap) {
1503
1503
  } else {
1504
1504
  break;
1505
1505
  }
1506
- } while (true);
1506
+ }
1507
1507
  }
1508
1508
  return sliced;
1509
1509
  }
@@ -1518,6 +1518,7 @@ var State = /* @__PURE__ */ ((State2) => {
1518
1518
  State2[State2["SCRIPT"] = 7] = "SCRIPT";
1519
1519
  State2[State2["STYLE"] = 8] = "STYLE";
1520
1520
  State2[State2["TEXTAREA"] = 9] = "TEXTAREA";
1521
+ State2[State2["TITLE"] = 10] = "TITLE";
1521
1522
  return State2;
1522
1523
  })(State || {});
1523
1524
 
@@ -1526,6 +1527,7 @@ var ContentModel = /* @__PURE__ */ ((ContentModel2) => {
1526
1527
  ContentModel2[ContentModel2["SCRIPT"] = 2] = "SCRIPT";
1527
1528
  ContentModel2[ContentModel2["STYLE"] = 3] = "STYLE";
1528
1529
  ContentModel2[ContentModel2["TEXTAREA"] = 4] = "TEXTAREA";
1530
+ ContentModel2[ContentModel2["TITLE"] = 5] = "TITLE";
1529
1531
  return ContentModel2;
1530
1532
  })(ContentModel || {});
1531
1533
  class Context {
@@ -3279,7 +3281,7 @@ class Validator {
3279
3281
  static validatePermittedCategory(node, category, defaultMatch) {
3280
3282
  const [, rawCategory] = /^(@?.*?)([?*]?)$/.exec(category);
3281
3283
  if (!rawCategory.startsWith("@")) {
3282
- return node.tagName === rawCategory;
3284
+ return node.matches(rawCategory);
3283
3285
  }
3284
3286
  if (!node.meta) {
3285
3287
  return defaultMatch;
@@ -3480,7 +3482,7 @@ function inAccessibilityTree(node) {
3480
3482
  function isAriaHiddenImpl(node) {
3481
3483
  const getAriaHiddenAttr = (node2) => {
3482
3484
  const ariaHidden = node2.getAttribute("aria-hidden");
3483
- return Boolean(ariaHidden && ariaHidden.value === "true");
3485
+ return ariaHidden?.value === "true";
3484
3486
  };
3485
3487
  return {
3486
3488
  byParent: node.parent ? isAriaHidden(node.parent) : false,
@@ -3715,7 +3717,7 @@ function getSchemaValidator(ruleId, properties) {
3715
3717
  return ajv$1.compile(schema);
3716
3718
  }
3717
3719
  function isErrorDescriptor(value) {
3718
- return Boolean(value[0] && value[0].message);
3720
+ return Boolean(value[0] && value[0]["message"]);
3719
3721
  }
3720
3722
  function unpackErrorDescriptor(value) {
3721
3723
  if (isErrorDescriptor(value)) {
@@ -3986,8 +3988,7 @@ class Rule {
3986
3988
  * @returns Rule documentation and url with additional details or `null` if no
3987
3989
  * additional documentation is available.
3988
3990
  */
3989
- /* eslint-disable-next-line @typescript-eslint/no-unused-vars -- technical debt, prototype should be moved to interface */
3990
- documentation(context) {
3991
+ documentation(_context) {
3991
3992
  return null;
3992
3993
  }
3993
3994
  }
@@ -4316,7 +4317,7 @@ class AriaHiddenBody extends Rule {
4316
4317
  const defaults$w = {
4317
4318
  allowAnyNamable: false
4318
4319
  };
4319
- const whitelisted = [
4320
+ const allowlist = /* @__PURE__ */ new Set([
4320
4321
  "main",
4321
4322
  "nav",
4322
4323
  "table",
@@ -4329,18 +4330,19 @@ const whitelisted = [
4329
4330
  "article",
4330
4331
  "dialog",
4331
4332
  "form",
4333
+ "iframe",
4332
4334
  "img",
4333
4335
  "area",
4334
4336
  "fieldset",
4335
4337
  "summary",
4336
4338
  "figure"
4337
- ];
4339
+ ]);
4338
4340
  function isValidUsage(target, meta) {
4339
4341
  const explicit = meta.attributes["aria-label"];
4340
4342
  if (explicit) {
4341
4343
  return true;
4342
4344
  }
4343
- if (whitelisted.includes(target.tagName)) {
4345
+ if (allowlist.has(target.tagName)) {
4344
4346
  return true;
4345
4347
  }
4346
4348
  if (target.hasAttribute("role")) {
@@ -4358,7 +4360,7 @@ class AriaLabelMisuse extends Rule {
4358
4360
  constructor(options) {
4359
4361
  super({ ...defaults$w, ...options });
4360
4362
  }
4361
- documentation() {
4363
+ documentation(context) {
4362
4364
  const valid = [
4363
4365
  "Interactive elements",
4364
4366
  "Labelable elements",
@@ -4372,25 +4374,41 @@ class AriaLabelMisuse extends Rule {
4372
4374
  "`<summary>`",
4373
4375
  "`<table>`, `<td>` and `<th>`"
4374
4376
  ];
4375
- const lines = valid.map((it) => `- ${it}
4376
- `).join("");
4377
- return {
4378
- description: `\`aria-label\` can only be used on:
4379
-
4380
- ${lines}`,
4381
- url: "https://html-validate.org/rules/aria-label-misuse.html"
4382
- };
4377
+ const lines = valid.map((it) => `- ${it}`);
4378
+ const url = "https://html-validate.org/rules/aria-label-misuse.html";
4379
+ if (context.allowsNaming) {
4380
+ return {
4381
+ description: [
4382
+ `\`${context.attr}\` is strictly allowed but is not recommended to be used on this element.`,
4383
+ `\`${context.attr}\` can only be used on:`,
4384
+ "",
4385
+ ...lines
4386
+ ].join("\n"),
4387
+ url
4388
+ };
4389
+ } else {
4390
+ return {
4391
+ description: [`\`${context.attr}\` can only be used on:`, "", ...lines].join("\n"),
4392
+ url
4393
+ };
4394
+ }
4383
4395
  }
4384
4396
  setup() {
4385
4397
  this.on("dom:ready", (event) => {
4386
4398
  const { document } = event;
4387
- for (const target of document.querySelectorAll("[aria-label]")) {
4388
- this.validateElement(target);
4399
+ for (const target of document.querySelectorAll("[aria-label], [aria-labelledby]")) {
4400
+ const ariaLabel = target.getAttribute("aria-label");
4401
+ if (ariaLabel) {
4402
+ this.validateElement(target, ariaLabel, "aria-label");
4403
+ }
4404
+ const ariaLabelledby = target.getAttribute("aria-labelledby");
4405
+ if (ariaLabelledby) {
4406
+ this.validateElement(target, ariaLabelledby, "aria-labelledby");
4407
+ }
4389
4408
  }
4390
4409
  });
4391
4410
  }
4392
- validateElement(target) {
4393
- const attr = target.getAttribute("aria-label");
4411
+ validateElement(target, attr, key) {
4394
4412
  if (!attr.value || attr.valueMatches("", false)) {
4395
4413
  return;
4396
4414
  }
@@ -4401,10 +4419,26 @@ ${lines}`,
4401
4419
  if (isValidUsage(target, meta)) {
4402
4420
  return;
4403
4421
  }
4404
- if (this.options.allowAnyNamable && ariaNaming(target) === "allowed") {
4422
+ const allowsNaming = ariaNaming(target) === "allowed";
4423
+ if (allowsNaming && this.options.allowAnyNamable) {
4405
4424
  return;
4406
4425
  }
4407
- this.report(target, `"aria-label" cannot be used on this element`, attr.keyLocation);
4426
+ const context = { attr: key, allowsNaming };
4427
+ if (allowsNaming) {
4428
+ this.report({
4429
+ node: target,
4430
+ location: attr.keyLocation,
4431
+ context,
4432
+ message: `"{{ attr }}" is strictly allowed but is not recommended to be used on this element`
4433
+ });
4434
+ } else {
4435
+ this.report({
4436
+ node: target,
4437
+ location: attr.keyLocation,
4438
+ context,
4439
+ message: `"{{ attr }}" cannot be used on this element`
4440
+ });
4441
+ }
4408
4442
  }
4409
4443
  }
4410
4444
 
@@ -4580,6 +4614,8 @@ const MATCH_STYLE_DATA = /^[^]*?(?=<\/style)/;
4580
4614
  const MATCH_STYLE_END = /^<(\/)(style)/;
4581
4615
  const MATCH_TEXTAREA_DATA = /^[^]*?(?=<\/textarea)/;
4582
4616
  const MATCH_TEXTAREA_END = /^<(\/)(textarea)/;
4617
+ const MATCH_TITLE_DATA = /^[^]*?(?=<\/title)/;
4618
+ const MATCH_TITLE_END = /^<(\/)(title)/;
4583
4619
  const MATCH_DIRECTIVE = /^(<!--\s*\[html-validate-)([a-z0-9-]+)(\s*)(.*?)(]?\s*-->)/;
4584
4620
  const MATCH_COMMENT = /^<!--([^]*?)-->/;
4585
4621
  const MATCH_CONDITIONAL = /^<!\[([^\]]*?)\]>/;
@@ -4625,6 +4661,9 @@ class Lexer {
4625
4661
  case State.TEXTAREA:
4626
4662
  yield* this.tokenizeTextarea(context);
4627
4663
  break;
4664
+ case State.TITLE:
4665
+ yield* this.tokenizeTitle(context);
4666
+ break;
4628
4667
  /* istanbul ignore next: sanity check: should not happen unless adding new states */
4629
4668
  default:
4630
4669
  this.unhandled(context);
@@ -4699,6 +4738,8 @@ class Lexer {
4699
4738
  context.contentModel = ContentModel.STYLE;
4700
4739
  } else if (data[0] === "<textarea") {
4701
4740
  context.contentModel = ContentModel.TEXTAREA;
4741
+ } else if (data[0] === "<title") {
4742
+ context.contentModel = ContentModel.TITLE;
4702
4743
  } else {
4703
4744
  context.contentModel = ContentModel.TEXT;
4704
4745
  }
@@ -4756,6 +4797,12 @@ class Lexer {
4756
4797
  } else {
4757
4798
  return State.TEXT;
4758
4799
  }
4800
+ case ContentModel.TITLE:
4801
+ if (selfClosed) {
4802
+ return State.TITLE;
4803
+ } else {
4804
+ return State.TEXT;
4805
+ }
4759
4806
  }
4760
4807
  }
4761
4808
  yield* this.match(
@@ -4830,6 +4877,16 @@ class Lexer {
4830
4877
  "expected </textarea>"
4831
4878
  );
4832
4879
  }
4880
+ *tokenizeTitle(context) {
4881
+ yield* this.match(
4882
+ context,
4883
+ [
4884
+ [MATCH_TITLE_END, State.TAG, TokenType.TAG_OPEN],
4885
+ [MATCH_TITLE_DATA, State.TITLE, TokenType.TEXT]
4886
+ ],
4887
+ "expected </title>"
4888
+ );
4889
+ }
4833
4890
  }
4834
4891
 
4835
4892
  const whitespace = /(\s+)/;
@@ -5099,9 +5156,6 @@ class AttributeAllowedValues extends Rule {
5099
5156
  description: "Attribute has invalid value.",
5100
5157
  url: "https://html-validate.org/rules/attribute-allowed-values.html"
5101
5158
  };
5102
- if (!context) {
5103
- return docs;
5104
- }
5105
5159
  const { allowed, attribute, element, value } = context;
5106
5160
  if (allowed.enum) {
5107
5161
  const allowedList = allowed.enum.map((value2) => {
@@ -5936,7 +5990,10 @@ class ElementName extends Rule {
5936
5990
 
5937
5991
  function isNativeTemplate(node) {
5938
5992
  const { tagName, meta } = node;
5939
- return Boolean(tagName === "template" && meta?.templateRoot && meta?.scriptSupporting);
5993
+ if (!meta) {
5994
+ return false;
5995
+ }
5996
+ return Boolean(tagName === "template" && meta.templateRoot && meta.scriptSupporting);
5940
5997
  }
5941
5998
  function getTransparentChildren(node, transparent) {
5942
5999
  if (typeof transparent === "boolean") {
@@ -6165,7 +6222,7 @@ class ElementPermittedParent extends Rule {
6165
6222
  }
6166
6223
  const rules = node.meta?.permittedParent;
6167
6224
  if (!rules) {
6168
- return false;
6225
+ return;
6169
6226
  }
6170
6227
  if (Validator.validatePermitted(parent, rules)) {
6171
6228
  return;
@@ -6232,14 +6289,10 @@ class ElementRequiredAncestor extends Rule {
6232
6289
 
6233
6290
  class ElementRequiredAttributes extends Rule {
6234
6291
  documentation(context) {
6235
- const docs = {
6236
- description: "Element is missing a required attribute",
6292
+ return {
6293
+ description: `The \`<${context.element}>\` element is required to have a \`${context.attribute}\` attribute.`,
6237
6294
  url: "https://html-validate.org/rules/element-required-attributes.html"
6238
6295
  };
6239
- if (context) {
6240
- docs.description = `The <${context.element}> element is required to have a "${context.attribute}" attribute.`;
6241
- }
6242
- return docs;
6243
6296
  }
6244
6297
  setup() {
6245
6298
  this.on("tag:end", (event) => {
@@ -7746,6 +7799,9 @@ class NoImplicitButtonType extends Rule {
7746
7799
  setup() {
7747
7800
  this.on("element:ready", isRelevant$2, (event) => {
7748
7801
  const { target } = event;
7802
+ if (target.parent?.is("select")) {
7803
+ return;
7804
+ }
7749
7805
  const attr = target.getAttribute("type");
7750
7806
  if (!attr) {
7751
7807
  this.report({
@@ -8242,10 +8298,9 @@ class NoSelfClosing extends Rule {
8242
8298
  }
8243
8299
  };
8244
8300
  }
8245
- documentation(tagName) {
8246
- tagName = tagName || "element";
8301
+ documentation(context) {
8247
8302
  return {
8248
- description: `Self-closing elements are disallowed. Use regular end tag <${tagName}></${tagName}> instead of self-closing <${tagName}/>.`,
8303
+ description: `Self-closing elements are disallowed. Use regular end tag <${context}></${context}> instead of self-closing <${context}/>.`,
8249
8304
  url: "https://html-validate.org/rules/no-self-closing.html"
8250
8305
  };
8251
8306
  }
@@ -8823,7 +8878,7 @@ class ScriptElement extends Rule {
8823
8878
  setup() {
8824
8879
  this.on("tag:end", (event) => {
8825
8880
  const node = event.target;
8826
- if (!node || node.tagName !== "script") {
8881
+ if (node?.tagName !== "script") {
8827
8882
  return;
8828
8883
  }
8829
8884
  if (node.closed !== NodeClosed.EndTag) {
@@ -9086,6 +9141,9 @@ function haveAccessibleText(node) {
9086
9141
  if (node.is("img") && hasNonEmptyAttribute(node, "alt")) {
9087
9142
  return true;
9088
9143
  }
9144
+ if (node.is("selectedcontent")) {
9145
+ return true;
9146
+ }
9089
9147
  if (hasDefaultText(node)) {
9090
9148
  return true;
9091
9149
  }
@@ -10036,13 +10094,13 @@ class ValidID extends Rule {
10036
10094
  }
10037
10095
 
10038
10096
  class VoidContent extends Rule {
10039
- documentation(tagName) {
10097
+ documentation(context) {
10040
10098
  const doc = {
10041
10099
  description: "HTML void elements cannot have any content and must not have content or end tag.",
10042
10100
  url: "https://html-validate.org/rules/void-content.html"
10043
10101
  };
10044
- if (tagName) {
10045
- doc.description = `<${tagName}> is a void element and must not have content or end tag.`;
10102
+ if (context) {
10103
+ doc.description = `<${context}> is a void element and must not have content or end tag.`;
10046
10104
  }
10047
10105
  return doc;
10048
10106
  }
@@ -10345,7 +10403,7 @@ class H37 extends Rule {
10345
10403
  const defaults$1 = {
10346
10404
  strict: false
10347
10405
  };
10348
- const { enum: validScopes } = elements.html5.th.attributes?.scope;
10406
+ const { enum: validScopes } = elements.html5.th.attributes.scope;
10349
10407
  const joinedScopes = utils_naturalJoin.naturalJoin(validScopes);
10350
10408
  const td = 0;
10351
10409
  const th = 1;
@@ -10443,7 +10501,7 @@ class H67 extends Rule {
10443
10501
  setup() {
10444
10502
  this.on("tag:end", (event) => {
10445
10503
  const node = event.target;
10446
- if (!node || node.tagName !== "img") {
10504
+ if (node?.tagName !== "img") {
10447
10505
  return;
10448
10506
  }
10449
10507
  const title = node.getAttribute("title");
@@ -11887,7 +11945,7 @@ class EventHandler {
11887
11945
  }
11888
11946
 
11889
11947
  const name = "html-validate";
11890
- const version = "10.1.2";
11948
+ const version = "10.3.0";
11891
11949
  const bugs = "https://gitlab.com/html-validate/html-validate/issues/new";
11892
11950
 
11893
11951
  function freeze(src) {
@@ -12620,9 +12678,9 @@ class Parser {
12620
12678
  * Trigger close events for any still open elements.
12621
12679
  */
12622
12680
  closeTree(source, location) {
12623
- let active;
12624
12681
  const documentElement = this.dom.root;
12625
- while ((active = this.dom.getActive()) && !active.isRootElement()) {
12682
+ let active = this.dom.getActive();
12683
+ while (!active.isRootElement()) {
12626
12684
  if (active.meta?.implicitClosed) {
12627
12685
  active.closed = NodeClosed.ImplicitClosed;
12628
12686
  this.closeElement(source, documentElement, active, location);
@@ -12630,6 +12688,7 @@ class Parser {
12630
12688
  this.closeElement(source, null, active, location);
12631
12689
  }
12632
12690
  this.dom.popActive();
12691
+ active = this.dom.getActive();
12633
12692
  }
12634
12693
  }
12635
12694
  }
@@ -12996,8 +13055,8 @@ function getUnnamedTransformerFromPlugin(name, plugin) {
12996
13055
  throw new ConfigError(`Plugin does not expose any transformers`);
12997
13056
  }
12998
13057
  if (typeof plugin.transformer !== "function") {
12999
- if (plugin.transformer.default) {
13000
- return plugin.transformer.default;
13058
+ if (plugin.transformer["default"]) {
13059
+ return plugin.transformer["default"];
13001
13060
  }
13002
13061
  throw new ConfigError(
13003
13062
  `Transformer "${name}" refers to unnamed transformer but plugin exposes only named.`
@@ -13226,7 +13285,7 @@ function checkstyleFormatter(results) {
13226
13285
  output += "</checkstyle>\n";
13227
13286
  return output;
13228
13287
  }
13229
- const formatter$3 = checkstyleFormatter;
13288
+ const formatter$2 = checkstyleFormatter;
13230
13289
 
13231
13290
  const defaults = {
13232
13291
  showLink: true,
@@ -13401,7 +13460,7 @@ function codeframe(results, options) {
13401
13460
  function jsonFormatter(results) {
13402
13461
  return JSON.stringify(results);
13403
13462
  }
13404
- const formatter$2 = jsonFormatter;
13463
+ const formatter$1 = jsonFormatter;
13405
13464
 
13406
13465
  function linkSummary(results) {
13407
13466
  const urls = results.reduce((result, it) => {
@@ -13430,7 +13489,6 @@ function stylish(results) {
13430
13489
  const links = linkSummary(results);
13431
13490
  return `${errors}${links}`;
13432
13491
  }
13433
- const formatter$1 = stylish;
13434
13492
 
13435
13493
  function textFormatter(results) {
13436
13494
  let output = "";
@@ -13460,10 +13518,10 @@ function textFormatter(results) {
13460
13518
  const formatter = textFormatter;
13461
13519
 
13462
13520
  const availableFormatters = {
13463
- checkstyle: formatter$3,
13521
+ checkstyle: formatter$2,
13464
13522
  codeframe,
13465
- json: formatter$2,
13466
- stylish: formatter$1,
13523
+ json: formatter$1,
13524
+ stylish,
13467
13525
  text: formatter
13468
13526
  };
13469
13527
  function getFormatter(name) {