html-validate 8.8.0 → 8.9.1

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
@@ -591,6 +591,18 @@ const patternProperties = {
591
591
  description: "Script-supporting elements are elements which can be inserted where othersise not permitted to assist in templating",
592
592
  type: "boolean"
593
593
  },
594
+ focusable: {
595
+ title: "Mark this element as focusable",
596
+ description: "This element may contain an associated label element.",
597
+ anyOf: [
598
+ {
599
+ type: "boolean"
600
+ },
601
+ {
602
+ "function": true
603
+ }
604
+ ]
605
+ },
594
606
  form: {
595
607
  title: "Mark element as a submittable form element",
596
608
  type: "boolean"
@@ -965,6 +977,7 @@ const MetaCopyableProperty = [
965
977
  "embedded",
966
978
  "interactive",
967
979
  "transparent",
980
+ "focusable",
968
981
  "form",
969
982
  "formAssociated",
970
983
  "labelable",
@@ -1037,6 +1050,7 @@ function migrateElement(src) {
1037
1050
  },
1038
1051
  attributes: migrateAttributes(src),
1039
1052
  textContent: src.textContent,
1053
+ focusable: src.focusable ?? false,
1040
1054
  implicitRole: src.implicitRole ?? (() => null)
1041
1055
  };
1042
1056
  delete result.deprecatedAttributes;
@@ -1292,6 +1306,9 @@ function expandProperties(node, entry) {
1292
1306
  setMetaProperty(entry, key, evaluateProperty(node, property));
1293
1307
  }
1294
1308
  }
1309
+ if (typeof entry.focusable === "function") {
1310
+ setMetaProperty(entry, "focusable", entry.focusable(node._adapter));
1311
+ }
1295
1312
  }
1296
1313
  function expandRegexValue(value) {
1297
1314
  if (value instanceof RegExp) {
@@ -1506,10 +1523,10 @@ class Context {
1506
1523
  constructor(source) {
1507
1524
  this.state = State.INITIAL;
1508
1525
  this.string = source.data;
1509
- this.filename = source.filename ?? "";
1510
- this.offset = source.offset ?? 0;
1511
- this.line = source.line ?? 1;
1512
- this.column = source.column ?? 1;
1526
+ this.filename = source.filename;
1527
+ this.offset = source.offset;
1528
+ this.line = source.line;
1529
+ this.column = source.column;
1513
1530
  this.contentModel = 1 /* TEXT */;
1514
1531
  }
1515
1532
  getTruncatedLine(n = 13) {
@@ -1542,6 +1559,16 @@ class Context {
1542
1559
  }
1543
1560
  }
1544
1561
 
1562
+ function normalizeSource(source) {
1563
+ return {
1564
+ filename: "",
1565
+ offset: 0,
1566
+ line: 1,
1567
+ column: 1,
1568
+ ...source
1569
+ };
1570
+ }
1571
+
1545
1572
  var NodeType = /* @__PURE__ */ ((NodeType2) => {
1546
1573
  NodeType2[NodeType2["ELEMENT_NODE"] = 1] = "ELEMENT_NODE";
1547
1574
  NodeType2[NodeType2["TEXT_NODE"] = 3] = "TEXT_NODE";
@@ -2122,6 +2149,7 @@ class TextNode extends DOMNode {
2122
2149
  }
2123
2150
  }
2124
2151
 
2152
+ const ROLE = Symbol("role");
2125
2153
  var NodeClosed = /* @__PURE__ */ ((NodeClosed2) => {
2126
2154
  NodeClosed2[NodeClosed2["Open"] = 0] = "Open";
2127
2155
  NodeClosed2[NodeClosed2["EndTag"] = 1] = "EndTag";
@@ -2357,6 +2385,27 @@ class HtmlElement extends DOMNode {
2357
2385
  get meta() {
2358
2386
  return this.metaElement;
2359
2387
  }
2388
+ /**
2389
+ * Get current role for this element (explicit with `role` attribute or mapped
2390
+ * with implicit role).
2391
+ *
2392
+ * @since 8.9.1
2393
+ */
2394
+ get role() {
2395
+ const cached = this.cacheGet(ROLE);
2396
+ if (cached !== void 0) {
2397
+ return cached;
2398
+ }
2399
+ const role = this.getAttribute("role");
2400
+ if (role) {
2401
+ return this.cacheSet(ROLE, role.value);
2402
+ }
2403
+ if (this.metaElement) {
2404
+ const implicitRole = this.metaElement.implicitRole(this._adapter);
2405
+ return this.cacheSet(ROLE, implicitRole);
2406
+ }
2407
+ return this.cacheSet(ROLE, null);
2408
+ }
2360
2409
  /**
2361
2410
  * Set annotation for this element.
2362
2411
  */
@@ -3135,8 +3184,21 @@ function isKeywordIgnored(options, keyword, matcher = (list, it) => list.include
3135
3184
  const ARIA_HIDDEN_CACHE = Symbol(isAriaHidden.name);
3136
3185
  const HTML_HIDDEN_CACHE = Symbol(isHTMLHidden.name);
3137
3186
  const ROLE_PRESENTATION_CACHE = Symbol(isPresentation.name);
3187
+ const STYLE_HIDDEN_CACHE = Symbol(isStyleHidden.name);
3138
3188
  function inAccessibilityTree(node) {
3139
- return !isAriaHidden(node) && !isPresentation(node);
3189
+ if (isAriaHidden(node)) {
3190
+ return false;
3191
+ }
3192
+ if (isPresentation(node)) {
3193
+ return false;
3194
+ }
3195
+ if (isHTMLHidden(node)) {
3196
+ return false;
3197
+ }
3198
+ if (isStyleHidden(node)) {
3199
+ return false;
3200
+ }
3201
+ return true;
3140
3202
  }
3141
3203
  function isAriaHiddenImpl(node) {
3142
3204
  const isHidden = (node2) => {
@@ -3174,22 +3236,41 @@ function isHTMLHidden(node, details) {
3174
3236
  const result = node.cacheSet(HTML_HIDDEN_CACHE, isHTMLHiddenImpl(node));
3175
3237
  return details ? result : result.byParent || result.bySelf;
3176
3238
  }
3239
+ function isStyleHiddenImpl(node) {
3240
+ const isHidden = (node2) => {
3241
+ const style = node2.getAttribute("style");
3242
+ const { display, visibility } = parseCssDeclaration(style == null ? void 0 : style.value);
3243
+ return display === "none" || visibility === "hidden";
3244
+ };
3245
+ const byParent = node.parent ? isStyleHidden(node.parent) : false;
3246
+ const bySelf = isHidden(node);
3247
+ return byParent || bySelf;
3248
+ }
3249
+ function isStyleHidden(node) {
3250
+ const cached = node.cacheGet(STYLE_HIDDEN_CACHE);
3251
+ if (cached) {
3252
+ return cached;
3253
+ }
3254
+ return node.cacheSet(STYLE_HIDDEN_CACHE, isStyleHiddenImpl(node));
3255
+ }
3177
3256
  function isPresentation(node) {
3178
3257
  if (node.cacheExists(ROLE_PRESENTATION_CACHE)) {
3179
3258
  return Boolean(node.cacheGet(ROLE_PRESENTATION_CACHE));
3180
3259
  }
3181
- let cur = node;
3182
- do {
3183
- const role = cur.getAttribute("role");
3184
- if (role && role.value === "presentation") {
3185
- return cur.cacheSet(ROLE_PRESENTATION_CACHE, true);
3186
- }
3187
- if (!cur.parent) {
3188
- break;
3189
- }
3190
- cur = cur.parent;
3191
- } while (!cur.isRootElement());
3192
- return node.cacheSet(ROLE_PRESENTATION_CACHE, false);
3260
+ const meta = node.meta;
3261
+ if (meta && meta.interactive) {
3262
+ return node.cacheSet(ROLE_PRESENTATION_CACHE, false);
3263
+ }
3264
+ const tabindex = node.getAttribute("tabindex");
3265
+ if (tabindex) {
3266
+ return node.cacheSet(ROLE_PRESENTATION_CACHE, false);
3267
+ }
3268
+ const role = node.getAttribute("role");
3269
+ if (role && (role.value === "presentation" || role.value === "none")) {
3270
+ return node.cacheSet(ROLE_PRESENTATION_CACHE, true);
3271
+ } else {
3272
+ return node.cacheSet(ROLE_PRESENTATION_CACHE, false);
3273
+ }
3193
3274
  }
3194
3275
 
3195
3276
  const cachePrefix = classifyNodeText.name;
@@ -6177,6 +6258,58 @@ class HeadingLevel extends Rule {
6177
6258
  }
6178
6259
  }
6179
6260
 
6261
+ class HiddenFocusable extends Rule {
6262
+ documentation(context) {
6263
+ const byParent = context === "parent" ? " In this case it is being hidden by an ancestor with `aria-hidden.`" : "";
6264
+ return {
6265
+ description: [
6266
+ `\`aria-hidden\` cannot be used on focusable elements.${byParent}`,
6267
+ "",
6268
+ "When focusable elements are hidden with `aria-hidden` they are still reachable using conventional means such as a mouse or keyboard but won't be exposed to assistive technology (AT).",
6269
+ "This is often confusing for users of AT such as screenreaders.",
6270
+ "",
6271
+ "To fix this either:",
6272
+ " - Remove `aria-hidden`.",
6273
+ " - Remove the element from the DOM instead.",
6274
+ " - Use the `hidden` attribute or similar means to hide the element."
6275
+ ].join("\n"),
6276
+ url: "https://html-validate.org/rules/hidden-focusable.html"
6277
+ };
6278
+ }
6279
+ setup() {
6280
+ const focusable = this.getTagsWithProperty("focusable");
6281
+ const selector = ["[tabindex]", ...focusable].join(",");
6282
+ this.on("dom:ready", (event) => {
6283
+ const { document } = event;
6284
+ for (const element of document.querySelectorAll(selector)) {
6285
+ if (isHTMLHidden(element) || isStyleHidden(element)) {
6286
+ continue;
6287
+ }
6288
+ if (isAriaHidden(element)) {
6289
+ this.validateElement(element);
6290
+ }
6291
+ }
6292
+ });
6293
+ }
6294
+ validateElement(element) {
6295
+ const { meta } = element;
6296
+ const tabindex = element.getAttribute("tabindex");
6297
+ if (meta && !meta.focusable && !tabindex) {
6298
+ return;
6299
+ }
6300
+ const attribute = element.getAttribute("aria-hidden");
6301
+ const message = attribute ? `aria-hidden cannot be used on focusable elements` : `aria-hidden cannot be used on focusable elements (hidden by ancestor element)`;
6302
+ const location = attribute ? attribute.keyLocation : element.location;
6303
+ const context = attribute ? "self" : "parent";
6304
+ this.report({
6305
+ node: element,
6306
+ message,
6307
+ location,
6308
+ context
6309
+ });
6310
+ }
6311
+ }
6312
+
6180
6313
  const defaults$g = {
6181
6314
  pattern: "kebabcase"
6182
6315
  };
@@ -6362,7 +6495,7 @@ function isHidden(node, context) {
6362
6495
  if (reference && reference.isSameNode(node)) {
6363
6496
  return false;
6364
6497
  } else {
6365
- return isHTMLHidden(node) || !inAccessibilityTree(node);
6498
+ return !inAccessibilityTree(node);
6366
6499
  }
6367
6500
  }
6368
6501
  function hasImgAltText(node, context) {
@@ -7312,7 +7445,7 @@ class NoRawCharacters extends Rule {
7312
7445
  }
7313
7446
  }
7314
7447
 
7315
- const selectors = ["input[aria-label]", "textarea[aria-label]", "select[aria-label]"];
7448
+ const selectors$1 = ["input[aria-label]", "textarea[aria-label]", "select[aria-label]"];
7316
7449
  class NoRedundantAriaLabel extends Rule {
7317
7450
  documentation() {
7318
7451
  return {
@@ -7323,7 +7456,7 @@ class NoRedundantAriaLabel extends Rule {
7323
7456
  setup() {
7324
7457
  this.on("dom:ready", (event) => {
7325
7458
  const { document } = event;
7326
- const elements = document.querySelectorAll(selectors.join(","));
7459
+ const elements = document.querySelectorAll(selectors$1.join(","));
7327
7460
  for (const element of elements) {
7328
7461
  const ariaLabel = element.getAttribute("aria-label");
7329
7462
  const id = element.id;
@@ -7939,7 +8072,7 @@ class RequireSri extends Rule {
7939
8072
  }
7940
8073
  documentation() {
7941
8074
  return {
7942
- description: `Subresource Integrity (SRI) \`integrity\` attribute is required to prevent manipulation from Content Delivery Networks or other third-party hosting.`,
8075
+ description: `Subresource Integrity (SRI) \`integrity\` attribute is required to prevent tampering or manipulation from Content Delivery Networks (CDN), rouge proxies, malicious entities, etc.`,
7943
8076
  url: "https://html-validate.org/rules/require-sri.html"
7944
8077
  };
7945
8078
  }
@@ -8352,6 +8485,130 @@ class TextContent extends Rule {
8352
8485
  }
8353
8486
  }
8354
8487
 
8488
+ const roles = ["complementary", "contentinfo", "form", "banner", "main", "navigation", "region"];
8489
+ const selectors = [
8490
+ "aside",
8491
+ "footer",
8492
+ "form",
8493
+ "header",
8494
+ "main",
8495
+ "nav",
8496
+ "section",
8497
+ ...roles.map((it) => `[role="${it}"]`)
8498
+ /* <search> does not (yet?) require a unique name */
8499
+ ];
8500
+ function getTextFromReference(document, id) {
8501
+ if (!id || id instanceof DynamicValue) {
8502
+ return id;
8503
+ }
8504
+ const selector = `#${id}`;
8505
+ const ref = document.querySelector(selector);
8506
+ if (ref) {
8507
+ return ref.textContent;
8508
+ } else {
8509
+ return selector;
8510
+ }
8511
+ }
8512
+ function groupBy(values, callback) {
8513
+ const result = {};
8514
+ for (const value of values) {
8515
+ const key = callback(value);
8516
+ if (key in result) {
8517
+ result[key].push(value);
8518
+ } else {
8519
+ result[key] = [value];
8520
+ }
8521
+ }
8522
+ return result;
8523
+ }
8524
+ function getTextEntryFromElement(document, node) {
8525
+ const ariaLabel = node.getAttribute("aria-label");
8526
+ if (ariaLabel) {
8527
+ return {
8528
+ node,
8529
+ text: ariaLabel.value,
8530
+ location: ariaLabel.keyLocation
8531
+ };
8532
+ }
8533
+ const ariaLabelledby = node.getAttribute("aria-labelledby");
8534
+ if (ariaLabelledby) {
8535
+ const text = getTextFromReference(document, ariaLabelledby.value);
8536
+ return {
8537
+ node,
8538
+ text,
8539
+ location: ariaLabelledby.keyLocation
8540
+ };
8541
+ }
8542
+ return {
8543
+ node,
8544
+ text: null,
8545
+ location: node.location
8546
+ };
8547
+ }
8548
+ function isExcluded(entry) {
8549
+ const { node, text } = entry;
8550
+ if (text === null) {
8551
+ return !(node.is("form") || node.is("section"));
8552
+ }
8553
+ return true;
8554
+ }
8555
+ class UniqueLandmark extends Rule {
8556
+ documentation() {
8557
+ return {
8558
+ description: [
8559
+ "When the same type of landmark is present more than once in the same document each must be uniquely identifiable with a non-empty and unique name.",
8560
+ "For instance, if the document has two `<nav>` elements each of them need an accessible name to be distinguished from each other.",
8561
+ "",
8562
+ "The following elements / roles are considered landmarks:",
8563
+ "",
8564
+ ' - `aside` or `[role="complementary"]`',
8565
+ ' - `footer` or `[role="contentinfo"]`',
8566
+ ' - `form` or `[role="form"]`',
8567
+ ' - `header` or `[role="banner"]`',
8568
+ ' - `main` or `[role="main"]`',
8569
+ ' - `nav` or `[role="navigation"]`',
8570
+ ' - `section` or `[role="region"]`',
8571
+ "",
8572
+ "To fix this either:",
8573
+ "",
8574
+ " - Add `aria-label`.",
8575
+ " - Add `aria-labelledby`.",
8576
+ " - Remove one of the landmarks."
8577
+ ].join("\n"),
8578
+ url: "https://html-validate.org/rules/unique-landmark.html"
8579
+ };
8580
+ }
8581
+ setup() {
8582
+ this.on("dom:ready", (event) => {
8583
+ const { document } = event;
8584
+ const elements = document.querySelectorAll(selectors.join(",")).filter((it) => typeof it.role === "string" && roles.includes(it.role));
8585
+ const grouped = groupBy(elements, (it) => it.role);
8586
+ for (const nodes of Object.values(grouped)) {
8587
+ if (nodes.length <= 1) {
8588
+ continue;
8589
+ }
8590
+ const entries = nodes.map((it) => getTextEntryFromElement(document, it));
8591
+ const filteredEntries = entries.filter(isExcluded);
8592
+ for (const entry of filteredEntries) {
8593
+ if (entry.text instanceof DynamicValue) {
8594
+ continue;
8595
+ }
8596
+ const dup = entries.filter((it) => it.text === entry.text).length > 1;
8597
+ if (!entry.text || dup) {
8598
+ const message = `Landmarks must have a non-empty and unique accessible name (aria-label or aria-labelledby)`;
8599
+ const location = entry.location;
8600
+ this.report({
8601
+ node: entry.node,
8602
+ message,
8603
+ location
8604
+ });
8605
+ }
8606
+ }
8607
+ }
8608
+ });
8609
+ }
8610
+ }
8611
+
8355
8612
  const defaults$4 = {
8356
8613
  ignoreCase: false,
8357
8614
  requireSemicolon: true
@@ -8741,7 +8998,7 @@ class H32 extends Rule {
8741
8998
  }
8742
8999
  function isSubmit(node) {
8743
9000
  const type = node.getAttribute("type");
8744
- return Boolean(type && type.valueMatches(/submit|image/));
9001
+ return Boolean(!type || type.valueMatches(/submit|image/));
8745
9002
  }
8746
9003
  function isAssociated(id, node) {
8747
9004
  const form = node.getAttribute("form");
@@ -8833,29 +9090,35 @@ class H37 extends Rule {
8833
9090
  };
8834
9091
  }
8835
9092
  setup() {
8836
- this.on("tag:end", (event) => {
8837
- const node = event.previous;
8838
- if (!needsAlt(node)) {
8839
- return;
8840
- }
8841
- if (!inAccessibilityTree(node)) {
8842
- return;
9093
+ this.on("dom:ready", (event) => {
9094
+ const { document } = event;
9095
+ const nodes = document.querySelectorAll("img, input");
9096
+ for (const node of nodes) {
9097
+ this.validateNode(node);
8843
9098
  }
8844
- if (Boolean(node.getAttributeValue("alt")) || Boolean(node.hasAttribute("alt") && this.options.allowEmpty)) {
9099
+ });
9100
+ }
9101
+ validateNode(node) {
9102
+ if (!needsAlt(node)) {
9103
+ return;
9104
+ }
9105
+ if (!inAccessibilityTree(node)) {
9106
+ return;
9107
+ }
9108
+ if (Boolean(node.getAttributeValue("alt")) || Boolean(node.hasAttribute("alt") && this.options.allowEmpty)) {
9109
+ return;
9110
+ }
9111
+ for (const attr of this.options.alias) {
9112
+ if (node.getAttribute(attr)) {
8845
9113
  return;
8846
9114
  }
8847
- for (const attr of this.options.alias) {
8848
- if (node.getAttribute(attr)) {
8849
- return;
8850
- }
8851
- }
8852
- if (node.hasAttribute("alt")) {
8853
- const attr = node.getAttribute("alt");
8854
- this.report(node, `${getTag(node)} cannot have empty "alt" attribute`, attr == null ? void 0 : attr.keyLocation);
8855
- } else {
8856
- this.report(node, `${getTag(node)} is missing required "alt" attribute`, node.location);
8857
- }
8858
- });
9115
+ }
9116
+ if (node.hasAttribute("alt")) {
9117
+ const attr = node.getAttribute("alt");
9118
+ this.report(node, `${getTag(node)} cannot have empty "alt" attribute`, attr == null ? void 0 : attr.keyLocation);
9119
+ } else {
9120
+ this.report(node, `${getTag(node)} is missing required "alt" attribute`, node.location);
9121
+ }
8859
9122
  }
8860
9123
  }
8861
9124
 
@@ -8946,7 +9209,7 @@ class H71 extends Rule {
8946
9209
  }
8947
9210
  }
8948
9211
 
8949
- const bundledRules$2 = {
9212
+ const bundledRules$1 = {
8950
9213
  "wcag/h30": H30,
8951
9214
  "wcag/h32": H32,
8952
9215
  "wcag/h36": H36,
@@ -8955,7 +9218,7 @@ const bundledRules$2 = {
8955
9218
  "wcag/h67": H67,
8956
9219
  "wcag/h71": H71
8957
9220
  };
8958
- var WCAG = bundledRules$2;
9221
+ var WCAG = bundledRules$1;
8959
9222
 
8960
9223
  const bundledRules = {
8961
9224
  "allowed-links": AllowedLinks,
@@ -8991,6 +9254,7 @@ const bundledRules = {
8991
9254
  "empty-title": EmptyTitle,
8992
9255
  "form-dup-name": FormDupName,
8993
9256
  "heading-level": HeadingLevel,
9257
+ "hidden-focusable": HiddenFocusable,
8994
9258
  "id-pattern": IdPattern,
8995
9259
  "input-attributes": InputAttributes,
8996
9260
  "input-missing-label": InputMissingLabel,
@@ -9031,13 +9295,13 @@ const bundledRules = {
9031
9295
  "svg-focusable": SvgFocusable,
9032
9296
  "tel-non-breaking": TelNonBreaking,
9033
9297
  "text-content": TextContent,
9298
+ "unique-landmark": UniqueLandmark,
9034
9299
  "unrecognized-char-ref": UnknownCharReference,
9035
9300
  "valid-id": ValidID,
9036
9301
  "void-content": VoidContent,
9037
9302
  "void-style": VoidStyle,
9038
9303
  ...WCAG
9039
9304
  };
9040
- var bundledRules$1 = bundledRules;
9041
9305
 
9042
9306
  var defaultConfig = {};
9043
9307
 
@@ -9049,6 +9313,7 @@ const config$4 = {
9049
9313
  "deprecated-rule": "warn",
9050
9314
  "empty-heading": "error",
9051
9315
  "empty-title": "error",
9316
+ "hidden-focusable": "error",
9052
9317
  "meta-refresh": "error",
9053
9318
  "multiple-labeled-controls": "error",
9054
9319
  "no-autoplay": ["error", { include: ["audio", "video"] }],
@@ -9060,6 +9325,7 @@ const config$4 = {
9060
9325
  "prefer-native-element": "error",
9061
9326
  "svg-focusable": "off",
9062
9327
  "text-content": "error",
9328
+ "unique-landmark": "error",
9063
9329
  "wcag/h30": "error",
9064
9330
  "wcag/h32": "error",
9065
9331
  "wcag/h36": "error",
@@ -9122,6 +9388,7 @@ const config$1 = {
9122
9388
  "empty-heading": "error",
9123
9389
  "empty-title": "error",
9124
9390
  "form-dup-name": "error",
9391
+ "hidden-focusable": "error",
9125
9392
  "input-attributes": "error",
9126
9393
  "long-title": "error",
9127
9394
  "map-dup-name": "error",
@@ -9154,6 +9421,7 @@ const config$1 = {
9154
9421
  "svg-focusable": "off",
9155
9422
  "tel-non-breaking": "error",
9156
9423
  "text-content": "error",
9424
+ "unique-landmark": "error",
9157
9425
  "unrecognized-char-ref": "error",
9158
9426
  "valid-id": ["error", { relaxed: false }],
9159
9427
  void: "off",
@@ -9498,7 +9766,7 @@ class Config {
9498
9766
  if (configData.rules) {
9499
9767
  const normalizedRules = Config.getRulesObject(configData.rules);
9500
9768
  for (const [ruleId, [, ruleOptions]] of normalizedRules.entries()) {
9501
- const cls = bundledRules$1[ruleId];
9769
+ const cls = bundledRules[ruleId];
9502
9770
  const path = `/rules/${ruleId}/1`;
9503
9771
  Rule.validateOptions(cls, ruleId, path, ruleOptions, filename, configData);
9504
9772
  }
@@ -9871,16 +10139,17 @@ class EventHandler {
9871
10139
  * @returns Unregistration function.
9872
10140
  */
9873
10141
  on(event, callback) {
9874
- const names = event.split(",").map((x) => x.trim());
10142
+ const { listeners } = this;
10143
+ const names = event.split(",").map((it) => it.trim());
9875
10144
  for (const name of names) {
9876
- this.listeners[name] = this.listeners[name] || [];
9877
- this.listeners[name].push(callback);
10145
+ const list = listeners[name] ?? [];
10146
+ listeners[name] = list;
10147
+ list.push(callback);
9878
10148
  }
9879
10149
  return () => {
9880
10150
  for (const name of names) {
9881
- this.listeners[name] = this.listeners[name].filter((fn) => {
9882
- return fn !== callback;
9883
- });
10151
+ const list = listeners[name];
10152
+ this.listeners[name] = list.filter((fn) => fn !== callback);
9884
10153
  }
9885
10154
  };
9886
10155
  }
@@ -9906,10 +10175,15 @@ class EventHandler {
9906
10175
  * @param data - Event data.
9907
10176
  */
9908
10177
  trigger(event, data) {
9909
- const callbacks = [...this.listeners[event] ?? [], ...this.listeners["*"] ?? []];
9910
- callbacks.forEach((listener) => {
10178
+ for (const listener of this.getCallbacks(event)) {
9911
10179
  listener.call(null, event, data);
9912
- });
10180
+ }
10181
+ }
10182
+ getCallbacks(event) {
10183
+ const { listeners } = this;
10184
+ const callbacks = listeners[event] ?? [];
10185
+ const global = listeners["*"] ?? [];
10186
+ return [...callbacks, ...global];
9913
10187
  }
9914
10188
  }
9915
10189
 
@@ -9979,10 +10253,10 @@ class Parser {
9979
10253
  location: null
9980
10254
  });
9981
10255
  this.dom = new DOMTree({
9982
- filename: source.filename ?? "",
9983
- offset: source.offset ?? 0,
9984
- line: source.line ?? 1,
9985
- column: source.column ?? 1,
10256
+ filename: source.filename,
10257
+ offset: source.offset,
10258
+ line: source.line,
10259
+ column: source.column,
9986
10260
  size: 0
9987
10261
  });
9988
10262
  this.trigger("dom:load", {
@@ -10506,7 +10780,7 @@ function isThenable(value) {
10506
10780
  return value && typeof value === "object" && "then" in value && typeof value.then === "function";
10507
10781
  }
10508
10782
 
10509
- const ruleIds = new Set(Object.keys(bundledRules$1));
10783
+ const ruleIds = new Set(Object.keys(bundledRules));
10510
10784
  function ruleExists(ruleId) {
10511
10785
  return ruleIds.has(ruleId);
10512
10786
  }
@@ -10595,9 +10869,7 @@ class Reporter {
10595
10869
  valid: this.isValid(),
10596
10870
  results: Object.keys(this.result).map((filePath) => {
10597
10871
  const messages = Array.from(this.result[filePath], freeze).sort(messageSort);
10598
- const source = (sources ?? []).find(
10599
- (source2) => filePath === (source2.filename ?? "")
10600
- );
10872
+ const source = (sources ?? []).find((source2) => filePath === source2.filename);
10601
10873
  return {
10602
10874
  filePath,
10603
10875
  messages,
@@ -10665,7 +10937,7 @@ class Engine {
10665
10937
  this.ParserClass = ParserClass;
10666
10938
  const result = this.initPlugins(this.config);
10667
10939
  this.availableRules = {
10668
- ...bundledRules$1,
10940
+ ...bundledRules,
10669
10941
  ...result.availableRules
10670
10942
  };
10671
10943
  }
@@ -11094,10 +11366,11 @@ class HtmlValidate {
11094
11366
  * @returns Report output.
11095
11367
  */
11096
11368
  async validateSource(input, configOverride) {
11097
- const config = await this.getConfigFor(input.filename, configOverride);
11098
- const source = config.transformSource(input);
11369
+ const source = normalizeSource(input);
11370
+ const config = await this.getConfigFor(source.filename, configOverride);
11371
+ const transformedSource = config.transformSource(source);
11099
11372
  const engine = new Engine(config, Parser);
11100
- return engine.lint(source);
11373
+ return engine.lint(transformedSource);
11101
11374
  }
11102
11375
  /**
11103
11376
  * Parse and validate HTML from [[Source]].
@@ -11107,10 +11380,11 @@ class HtmlValidate {
11107
11380
  * @returns Report output.
11108
11381
  */
11109
11382
  validateSourceSync(input, configOverride) {
11110
- const config = this.getConfigForSync(input.filename, configOverride);
11111
- const source = config.transformSource(input);
11383
+ const source = normalizeSource(input);
11384
+ const config = this.getConfigForSync(source.filename, configOverride);
11385
+ const transformedSource = config.transformSource(source);
11112
11386
  const engine = new Engine(config, Parser);
11113
- return engine.lint(source);
11387
+ return engine.lint(transformedSource);
11114
11388
  }
11115
11389
  /**
11116
11390
  * Parse and validate HTML from file.
@@ -11419,7 +11693,7 @@ class HtmlValidate {
11419
11693
  }
11420
11694
 
11421
11695
  const name = "html-validate";
11422
- const version = "8.8.0";
11696
+ const version = "8.9.1";
11423
11697
  const bugs = "https://gitlab.com/html-validate/html-validate/issues/new";
11424
11698
 
11425
11699
  function definePlugin(plugin) {