html-validate 7.11.1 → 7.12.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
@@ -303,6 +303,10 @@ class Attribute {
303
303
  if (this.value instanceof DynamicValue) {
304
304
  return dynamicMatches;
305
305
  }
306
+ /* test against an array of keywords */
307
+ if (Array.isArray(pattern)) {
308
+ return pattern.includes(this.value);
309
+ }
306
310
  /* test value against pattern */
307
311
  if (pattern instanceof RegExp) {
308
312
  return this.value.match(pattern) !== null;
@@ -466,6 +470,7 @@ const MetaCopyableProperty = [
466
470
  "interactive",
467
471
  "transparent",
468
472
  "form",
473
+ "formAssociated",
469
474
  "labelable",
470
475
  "attributes",
471
476
  "permittedContent",
@@ -1656,6 +1661,10 @@ class UserError extends NestedError {
1656
1661
  Error.captureStackTrace(this, UserError);
1657
1662
  this.name = UserError.name;
1658
1663
  }
1664
+ /**
1665
+ * @public
1666
+ */
1667
+ /* istanbul ignore next: default implementation */
1659
1668
  prettyFormat() {
1660
1669
  return undefined;
1661
1670
  }
@@ -1878,6 +1887,10 @@ const patternProperties = {
1878
1887
  title: "Mark element as a submittable form element",
1879
1888
  type: "boolean"
1880
1889
  },
1890
+ formAssociated: {
1891
+ title: "Mark element as a form-associated element",
1892
+ $ref: "#/definitions/FormAssociated"
1893
+ },
1881
1894
  labelable: {
1882
1895
  title: "Mark this element as labelable",
1883
1896
  description: "This element may contain an associated label element.",
@@ -2023,6 +2036,16 @@ const definitions = {
2023
2036
  }
2024
2037
  }
2025
2038
  },
2039
+ FormAssociated: {
2040
+ type: "object",
2041
+ additionalProperties: false,
2042
+ properties: {
2043
+ listed: {
2044
+ type: "boolean",
2045
+ title: "Listed elements have a name attribute and is listed in the form and fieldset elements property."
2046
+ }
2047
+ }
2048
+ },
2026
2049
  Permitted: {
2027
2050
  type: "array",
2028
2051
  items: {
@@ -2281,6 +2304,9 @@ function migrateAttributes(src) {
2281
2304
  function migrateElement(src) {
2282
2305
  const result = {
2283
2306
  ...src,
2307
+ ...{
2308
+ formAssociated: undefined,
2309
+ },
2284
2310
  attributes: migrateAttributes(src),
2285
2311
  textContent: src.textContent,
2286
2312
  };
@@ -2291,6 +2317,14 @@ function migrateElement(src) {
2291
2317
  if (!result.textContent) {
2292
2318
  delete result.textContent;
2293
2319
  }
2320
+ if (src.formAssociated) {
2321
+ result.formAssociated = {
2322
+ listed: Boolean(src.formAssociated.listed),
2323
+ };
2324
+ }
2325
+ else {
2326
+ delete result.formAssociated;
2327
+ }
2294
2328
  return result;
2295
2329
  }
2296
2330
 
@@ -3453,6 +3487,16 @@ class Rule {
3453
3487
  isKeywordIgnored(keyword, matcher = (list, it) => list.includes(it)) {
3454
3488
  return isKeywordIgnored(this.options, keyword, matcher);
3455
3489
  }
3490
+ /**
3491
+ * Get [[MetaElement]] for the given tag. If no specific metadata is present
3492
+ * the global metadata is returned or null if no global is present.
3493
+ *
3494
+ * @public
3495
+ * @returns A shallow copy of metadata.
3496
+ */
3497
+ getMetaFor(tagName) {
3498
+ return this.meta.getMetaFor(tagName);
3499
+ }
3456
3500
  /**
3457
3501
  * Find all tags which has enabled given property.
3458
3502
  */
@@ -4863,7 +4907,7 @@ class AttributeMisuse extends Rule {
4863
4907
  if (!meta || !meta.allowed) {
4864
4908
  return;
4865
4909
  }
4866
- const details = meta.allowed(node);
4910
+ const details = meta.allowed(node, attr);
4867
4911
  if (details) {
4868
4912
  this.report({
4869
4913
  node,
@@ -5887,12 +5931,92 @@ class EmptyTitle extends Rule {
5887
5931
  }
5888
5932
  }
5889
5933
 
5934
+ const CACHE_KEY = Symbol("form-elements");
5935
+ function haveName(name) {
5936
+ return typeof name === "string" && name !== "";
5937
+ }
5938
+ function isRelevant$4(node) {
5939
+ if (node.is("input")) {
5940
+ /* ignore radiobuttons and checkboxes */
5941
+ const type = node.getAttribute("type");
5942
+ return !type || !type.valueMatches(["radio", "checkbox"], true);
5943
+ }
5944
+ return true;
5945
+ }
5946
+ class FormDupName extends Rule {
5947
+ documentation() {
5948
+ return {
5949
+ description: "Each form control must have a unique name.",
5950
+ url: "https://html-validate.org/rules/form-dup-name.html",
5951
+ };
5952
+ }
5953
+ setup() {
5954
+ const selector = this.getSelector();
5955
+ this.on("dom:ready", (event) => {
5956
+ var _a;
5957
+ const { document } = event;
5958
+ const controls = document.querySelectorAll(selector).filter(isRelevant$4);
5959
+ for (const control of controls) {
5960
+ const attr = control.getAttribute("name");
5961
+ const name = attr === null || attr === void 0 ? void 0 : attr.value;
5962
+ if (attr && haveName(name)) {
5963
+ const form = (_a = control.closest("form")) !== null && _a !== void 0 ? _a : document.root;
5964
+ this.validateName(control, form, attr, name);
5965
+ }
5966
+ }
5967
+ });
5968
+ }
5969
+ validateName(control, form, attr, name) {
5970
+ const elements = this.getElements(form);
5971
+ if (elements.has(name)) {
5972
+ const context = {
5973
+ name,
5974
+ };
5975
+ this.report({
5976
+ node: control,
5977
+ location: attr.valueLocation,
5978
+ message: 'Duplicate form control name "{{ name }}"',
5979
+ context,
5980
+ });
5981
+ }
5982
+ else {
5983
+ elements.add(name);
5984
+ }
5985
+ }
5986
+ getSelector() {
5987
+ const tags = this.getTagsWithProperty("formAssociated").filter((it) => {
5988
+ return this.isListedElement(it);
5989
+ });
5990
+ return tags.join(", ");
5991
+ }
5992
+ isListedElement(tagName) {
5993
+ const meta = this.getMetaFor(tagName);
5994
+ /* istanbul ignore if: the earlier check for getTagsWithProperty ensures
5995
+ * these will actually be set so this is just an untestable fallback */
5996
+ if (!meta || !meta.formAssociated) {
5997
+ return false;
5998
+ }
5999
+ return meta.formAssociated.listed;
6000
+ }
6001
+ getElements(form) {
6002
+ const existing = form.cacheGet(CACHE_KEY);
6003
+ if (existing) {
6004
+ return existing;
6005
+ }
6006
+ else {
6007
+ const elements = new Set();
6008
+ form.cacheSet(CACHE_KEY, elements);
6009
+ return elements;
6010
+ }
6011
+ }
6012
+ }
6013
+
5890
6014
  const defaults$j = {
5891
6015
  allowMultipleH1: false,
5892
6016
  minInitialRank: "h1",
5893
6017
  sectioningRoots: ["dialog", '[role="dialog"]'],
5894
6018
  };
5895
- function isRelevant$2(event) {
6019
+ function isRelevant$3(event) {
5896
6020
  const node = event.target;
5897
6021
  return Boolean(node.meta && node.meta.heading);
5898
6022
  }
@@ -5960,7 +6084,7 @@ class HeadingLevel extends Rule {
5960
6084
  };
5961
6085
  }
5962
6086
  setup() {
5963
- this.on("tag:start", isRelevant$2, (event) => this.onTagStart(event));
6087
+ this.on("tag:start", isRelevant$3, (event) => this.onTagStart(event));
5964
6088
  this.on("tag:ready", (event) => this.onTagReady(event));
5965
6089
  this.on("tag:close", (event) => this.onTagClose(event));
5966
6090
  }
@@ -6494,6 +6618,42 @@ class MapDupName extends Rule {
6494
6618
  }
6495
6619
  }
6496
6620
 
6621
+ function isRelevant$2(event) {
6622
+ return event.target.is("map");
6623
+ }
6624
+ function hasStaticValue(attr) {
6625
+ return Boolean(attr && !(attr.value instanceof DynamicValue));
6626
+ }
6627
+ class MapIdName extends Rule {
6628
+ documentation() {
6629
+ return {
6630
+ description: "When the `id` attribute is present on a `<map>` element it must be equal to the `name` attribute.",
6631
+ url: "https://html-validate.org/rules/map-id-name.html",
6632
+ };
6633
+ }
6634
+ setup() {
6635
+ this.on("tag:ready", isRelevant$2, (event) => {
6636
+ var _a;
6637
+ const { target } = event;
6638
+ const id = target.getAttribute("id");
6639
+ const name = target.getAttribute("name");
6640
+ // /* ignore missing attributes or dynamic value */
6641
+ if (!hasStaticValue(id) || !hasStaticValue(name)) {
6642
+ return;
6643
+ }
6644
+ /* ignore when id and name is the same */
6645
+ if (id.value === name.value) {
6646
+ return;
6647
+ }
6648
+ this.report({
6649
+ node: event.target,
6650
+ message: `"id" and "name" attribute must be the same on <map> elements`,
6651
+ location: (_a = id.valueLocation) !== null && _a !== void 0 ? _a : name.valueLocation,
6652
+ });
6653
+ });
6654
+ }
6655
+ }
6656
+
6497
6657
  class MissingDoctype extends Rule {
6498
6658
  documentation() {
6499
6659
  return {
@@ -8958,12 +9118,14 @@ const bundledRules = {
8958
9118
  "element-required-content": ElementRequiredContent,
8959
9119
  "empty-heading": EmptyHeading,
8960
9120
  "empty-title": EmptyTitle,
9121
+ "form-dup-name": FormDupName,
8961
9122
  "heading-level": HeadingLevel,
8962
9123
  "id-pattern": IdPattern,
8963
9124
  "input-attributes": InputAttributes,
8964
9125
  "input-missing-label": InputMissingLabel,
8965
9126
  "long-title": LongTitle,
8966
9127
  "map-dup-name": MapDupName,
9128
+ "map-id-name": MapIdName,
8967
9129
  "meta-refresh": MetaRefresh,
8968
9130
  "missing-doctype": MissingDoctype,
8969
9131
  "multiple-labeled-controls": MultipleLabeledControls,
@@ -9072,9 +9234,11 @@ const config$1 = {
9072
9234
  "element-required-content": "error",
9073
9235
  "empty-heading": "error",
9074
9236
  "empty-title": "error",
9237
+ "form-dup-name": "error",
9075
9238
  "input-attributes": "error",
9076
9239
  "long-title": "error",
9077
9240
  "map-dup-name": "error",
9241
+ "map-id-name": "error",
9078
9242
  "meta-refresh": "error",
9079
9243
  "multiple-labeled-controls": "error",
9080
9244
  "no-autoplay": ["error", { include: ["audio", "video"] }],
@@ -9135,6 +9299,7 @@ const config = {
9135
9299
  "element-required-attributes": "error",
9136
9300
  "element-required-content": "error",
9137
9301
  "map-dup-name": "error",
9302
+ "map-id-name": "error",
9138
9303
  "multiple-labeled-controls": "error",
9139
9304
  "no-deprecated-attr": "error",
9140
9305
  "no-dup-attr": "error",
@@ -9964,6 +10129,8 @@ class Parser {
9964
10129
  }
9965
10130
  /* resolve any dynamic meta element properties */
9966
10131
  this.dom.resolveMeta(this.metaTable);
10132
+ /* enable cache on root element, all children already have cached enabled */
10133
+ this.dom.root.cacheEnable();
9967
10134
  /* trigger any rules waiting for DOM ready */
9968
10135
  this.trigger("dom:ready", {
9969
10136
  document: this.dom,
@@ -10795,6 +10962,8 @@ class Engine {
10795
10962
  var _a, _b;
10796
10963
  /* wait for a tag to open and find the current block by using its parent */
10797
10964
  if (directiveBlock === null) {
10965
+ /* istanbul ignore next: there will always be a parent (root element if
10966
+ * nothing else) but typescript doesn't know that */
10798
10967
  directiveBlock = (_b = (_a = data.target.parent) === null || _a === void 0 ? void 0 : _a.unique) !== null && _b !== void 0 ? _b : null;
10799
10968
  }
10800
10969
  /* disable rules directly on the node so it will be recorded for later,
@@ -11223,7 +11392,7 @@ class HtmlValidate {
11223
11392
  /** @public */
11224
11393
  const name = "html-validate";
11225
11394
  /** @public */
11226
- const version = "7.11.1";
11395
+ const version = "7.12.0";
11227
11396
  /** @public */
11228
11397
  const bugs = "https://gitlab.com/html-validate/html-validate/issues/new";
11229
11398
 
@@ -11666,5 +11835,5 @@ function getFormatter(name) {
11666
11835
  return (_a = availableFormatters[name]) !== null && _a !== void 0 ? _a : null;
11667
11836
  }
11668
11837
 
11669
- export { Config as C, DynamicValue as D, EventHandler as E, FileSystemConfigLoader as F, HtmlValidate as H, MetaTable as M, NodeClosed as N, Parser as P, Rule as R, Severity as S, TextNode as T, UserError as U, WrappedError as W, ConfigError as a, ConfigLoader as b, StaticConfigLoader as c, HtmlElement as d, SchemaValidationError as e, NestedError as f, MetaCopyableProperty as g, Reporter as h, TemplateExtractor as i, definePlugin as j, getFormatter as k, legacyRequire as l, ensureError as m, configDataFromFile as n, compatibilityCheck as o, presets as p, codeframe as q, ruleExists as r, sliceLocation as s, isTextNode as t, isElementNode as u, version as v, generateIdSelector as w, name as x, bugs as y };
11838
+ export { Attribute as A, Config as C, DynamicValue as D, EventHandler as E, FileSystemConfigLoader as F, HtmlValidate as H, MetaTable as M, NodeClosed as N, Parser as P, Rule as R, Severity as S, TextNode as T, UserError as U, WrappedError as W, ConfigError as a, ConfigLoader as b, StaticConfigLoader as c, HtmlElement as d, SchemaValidationError as e, NestedError as f, MetaCopyableProperty as g, Reporter as h, TemplateExtractor as i, definePlugin as j, getFormatter as k, legacyRequire as l, ensureError as m, configDataFromFile as n, compatibilityCheck as o, presets as p, codeframe as q, ruleExists as r, sliceLocation as s, isTextNode as t, isElementNode as u, version as v, generateIdSelector as w, name as x, bugs as y };
11670
11839
  //# sourceMappingURL=core.js.map