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/cjs/core.js CHANGED
@@ -601,6 +601,18 @@ const patternProperties = {
601
601
  description: "Script-supporting elements are elements which can be inserted where othersise not permitted to assist in templating",
602
602
  type: "boolean"
603
603
  },
604
+ focusable: {
605
+ title: "Mark this element as focusable",
606
+ description: "This element may contain an associated label element.",
607
+ anyOf: [
608
+ {
609
+ type: "boolean"
610
+ },
611
+ {
612
+ "function": true
613
+ }
614
+ ]
615
+ },
604
616
  form: {
605
617
  title: "Mark element as a submittable form element",
606
618
  type: "boolean"
@@ -975,6 +987,7 @@ const MetaCopyableProperty = [
975
987
  "embedded",
976
988
  "interactive",
977
989
  "transparent",
990
+ "focusable",
978
991
  "form",
979
992
  "formAssociated",
980
993
  "labelable",
@@ -1047,6 +1060,7 @@ function migrateElement(src) {
1047
1060
  },
1048
1061
  attributes: migrateAttributes(src),
1049
1062
  textContent: src.textContent,
1063
+ focusable: src.focusable ?? false,
1050
1064
  implicitRole: src.implicitRole ?? (() => null)
1051
1065
  };
1052
1066
  delete result.deprecatedAttributes;
@@ -1302,6 +1316,9 @@ function expandProperties(node, entry) {
1302
1316
  setMetaProperty(entry, key, evaluateProperty(node, property));
1303
1317
  }
1304
1318
  }
1319
+ if (typeof entry.focusable === "function") {
1320
+ setMetaProperty(entry, "focusable", entry.focusable(node._adapter));
1321
+ }
1305
1322
  }
1306
1323
  function expandRegexValue(value) {
1307
1324
  if (value instanceof RegExp) {
@@ -1516,10 +1533,10 @@ class Context {
1516
1533
  constructor(source) {
1517
1534
  this.state = State.INITIAL;
1518
1535
  this.string = source.data;
1519
- this.filename = source.filename ?? "";
1520
- this.offset = source.offset ?? 0;
1521
- this.line = source.line ?? 1;
1522
- this.column = source.column ?? 1;
1536
+ this.filename = source.filename;
1537
+ this.offset = source.offset;
1538
+ this.line = source.line;
1539
+ this.column = source.column;
1523
1540
  this.contentModel = 1 /* TEXT */;
1524
1541
  }
1525
1542
  getTruncatedLine(n = 13) {
@@ -1552,6 +1569,16 @@ class Context {
1552
1569
  }
1553
1570
  }
1554
1571
 
1572
+ function normalizeSource(source) {
1573
+ return {
1574
+ filename: "",
1575
+ offset: 0,
1576
+ line: 1,
1577
+ column: 1,
1578
+ ...source
1579
+ };
1580
+ }
1581
+
1555
1582
  var NodeType = /* @__PURE__ */ ((NodeType2) => {
1556
1583
  NodeType2[NodeType2["ELEMENT_NODE"] = 1] = "ELEMENT_NODE";
1557
1584
  NodeType2[NodeType2["TEXT_NODE"] = 3] = "TEXT_NODE";
@@ -2132,6 +2159,7 @@ class TextNode extends DOMNode {
2132
2159
  }
2133
2160
  }
2134
2161
 
2162
+ const ROLE = Symbol("role");
2135
2163
  var NodeClosed = /* @__PURE__ */ ((NodeClosed2) => {
2136
2164
  NodeClosed2[NodeClosed2["Open"] = 0] = "Open";
2137
2165
  NodeClosed2[NodeClosed2["EndTag"] = 1] = "EndTag";
@@ -2367,6 +2395,27 @@ class HtmlElement extends DOMNode {
2367
2395
  get meta() {
2368
2396
  return this.metaElement;
2369
2397
  }
2398
+ /**
2399
+ * Get current role for this element (explicit with `role` attribute or mapped
2400
+ * with implicit role).
2401
+ *
2402
+ * @since 8.9.1
2403
+ */
2404
+ get role() {
2405
+ const cached = this.cacheGet(ROLE);
2406
+ if (cached !== void 0) {
2407
+ return cached;
2408
+ }
2409
+ const role = this.getAttribute("role");
2410
+ if (role) {
2411
+ return this.cacheSet(ROLE, role.value);
2412
+ }
2413
+ if (this.metaElement) {
2414
+ const implicitRole = this.metaElement.implicitRole(this._adapter);
2415
+ return this.cacheSet(ROLE, implicitRole);
2416
+ }
2417
+ return this.cacheSet(ROLE, null);
2418
+ }
2370
2419
  /**
2371
2420
  * Set annotation for this element.
2372
2421
  */
@@ -3145,8 +3194,21 @@ function isKeywordIgnored(options, keyword, matcher = (list, it) => list.include
3145
3194
  const ARIA_HIDDEN_CACHE = Symbol(isAriaHidden.name);
3146
3195
  const HTML_HIDDEN_CACHE = Symbol(isHTMLHidden.name);
3147
3196
  const ROLE_PRESENTATION_CACHE = Symbol(isPresentation.name);
3197
+ const STYLE_HIDDEN_CACHE = Symbol(isStyleHidden.name);
3148
3198
  function inAccessibilityTree(node) {
3149
- return !isAriaHidden(node) && !isPresentation(node);
3199
+ if (isAriaHidden(node)) {
3200
+ return false;
3201
+ }
3202
+ if (isPresentation(node)) {
3203
+ return false;
3204
+ }
3205
+ if (isHTMLHidden(node)) {
3206
+ return false;
3207
+ }
3208
+ if (isStyleHidden(node)) {
3209
+ return false;
3210
+ }
3211
+ return true;
3150
3212
  }
3151
3213
  function isAriaHiddenImpl(node) {
3152
3214
  const isHidden = (node2) => {
@@ -3184,22 +3246,41 @@ function isHTMLHidden(node, details) {
3184
3246
  const result = node.cacheSet(HTML_HIDDEN_CACHE, isHTMLHiddenImpl(node));
3185
3247
  return details ? result : result.byParent || result.bySelf;
3186
3248
  }
3249
+ function isStyleHiddenImpl(node) {
3250
+ const isHidden = (node2) => {
3251
+ const style = node2.getAttribute("style");
3252
+ const { display, visibility } = parseCssDeclaration(style == null ? void 0 : style.value);
3253
+ return display === "none" || visibility === "hidden";
3254
+ };
3255
+ const byParent = node.parent ? isStyleHidden(node.parent) : false;
3256
+ const bySelf = isHidden(node);
3257
+ return byParent || bySelf;
3258
+ }
3259
+ function isStyleHidden(node) {
3260
+ const cached = node.cacheGet(STYLE_HIDDEN_CACHE);
3261
+ if (cached) {
3262
+ return cached;
3263
+ }
3264
+ return node.cacheSet(STYLE_HIDDEN_CACHE, isStyleHiddenImpl(node));
3265
+ }
3187
3266
  function isPresentation(node) {
3188
3267
  if (node.cacheExists(ROLE_PRESENTATION_CACHE)) {
3189
3268
  return Boolean(node.cacheGet(ROLE_PRESENTATION_CACHE));
3190
3269
  }
3191
- let cur = node;
3192
- do {
3193
- const role = cur.getAttribute("role");
3194
- if (role && role.value === "presentation") {
3195
- return cur.cacheSet(ROLE_PRESENTATION_CACHE, true);
3196
- }
3197
- if (!cur.parent) {
3198
- break;
3199
- }
3200
- cur = cur.parent;
3201
- } while (!cur.isRootElement());
3202
- return node.cacheSet(ROLE_PRESENTATION_CACHE, false);
3270
+ const meta = node.meta;
3271
+ if (meta && meta.interactive) {
3272
+ return node.cacheSet(ROLE_PRESENTATION_CACHE, false);
3273
+ }
3274
+ const tabindex = node.getAttribute("tabindex");
3275
+ if (tabindex) {
3276
+ return node.cacheSet(ROLE_PRESENTATION_CACHE, false);
3277
+ }
3278
+ const role = node.getAttribute("role");
3279
+ if (role && (role.value === "presentation" || role.value === "none")) {
3280
+ return node.cacheSet(ROLE_PRESENTATION_CACHE, true);
3281
+ } else {
3282
+ return node.cacheSet(ROLE_PRESENTATION_CACHE, false);
3283
+ }
3203
3284
  }
3204
3285
 
3205
3286
  const cachePrefix = classifyNodeText.name;
@@ -6187,6 +6268,58 @@ class HeadingLevel extends Rule {
6187
6268
  }
6188
6269
  }
6189
6270
 
6271
+ class HiddenFocusable extends Rule {
6272
+ documentation(context) {
6273
+ const byParent = context === "parent" ? " In this case it is being hidden by an ancestor with `aria-hidden.`" : "";
6274
+ return {
6275
+ description: [
6276
+ `\`aria-hidden\` cannot be used on focusable elements.${byParent}`,
6277
+ "",
6278
+ "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).",
6279
+ "This is often confusing for users of AT such as screenreaders.",
6280
+ "",
6281
+ "To fix this either:",
6282
+ " - Remove `aria-hidden`.",
6283
+ " - Remove the element from the DOM instead.",
6284
+ " - Use the `hidden` attribute or similar means to hide the element."
6285
+ ].join("\n"),
6286
+ url: "https://html-validate.org/rules/hidden-focusable.html"
6287
+ };
6288
+ }
6289
+ setup() {
6290
+ const focusable = this.getTagsWithProperty("focusable");
6291
+ const selector = ["[tabindex]", ...focusable].join(",");
6292
+ this.on("dom:ready", (event) => {
6293
+ const { document } = event;
6294
+ for (const element of document.querySelectorAll(selector)) {
6295
+ if (isHTMLHidden(element) || isStyleHidden(element)) {
6296
+ continue;
6297
+ }
6298
+ if (isAriaHidden(element)) {
6299
+ this.validateElement(element);
6300
+ }
6301
+ }
6302
+ });
6303
+ }
6304
+ validateElement(element) {
6305
+ const { meta } = element;
6306
+ const tabindex = element.getAttribute("tabindex");
6307
+ if (meta && !meta.focusable && !tabindex) {
6308
+ return;
6309
+ }
6310
+ const attribute = element.getAttribute("aria-hidden");
6311
+ const message = attribute ? `aria-hidden cannot be used on focusable elements` : `aria-hidden cannot be used on focusable elements (hidden by ancestor element)`;
6312
+ const location = attribute ? attribute.keyLocation : element.location;
6313
+ const context = attribute ? "self" : "parent";
6314
+ this.report({
6315
+ node: element,
6316
+ message,
6317
+ location,
6318
+ context
6319
+ });
6320
+ }
6321
+ }
6322
+
6190
6323
  const defaults$g = {
6191
6324
  pattern: "kebabcase"
6192
6325
  };
@@ -6372,7 +6505,7 @@ function isHidden(node, context) {
6372
6505
  if (reference && reference.isSameNode(node)) {
6373
6506
  return false;
6374
6507
  } else {
6375
- return isHTMLHidden(node) || !inAccessibilityTree(node);
6508
+ return !inAccessibilityTree(node);
6376
6509
  }
6377
6510
  }
6378
6511
  function hasImgAltText(node, context) {
@@ -7322,7 +7455,7 @@ class NoRawCharacters extends Rule {
7322
7455
  }
7323
7456
  }
7324
7457
 
7325
- const selectors = ["input[aria-label]", "textarea[aria-label]", "select[aria-label]"];
7458
+ const selectors$1 = ["input[aria-label]", "textarea[aria-label]", "select[aria-label]"];
7326
7459
  class NoRedundantAriaLabel extends Rule {
7327
7460
  documentation() {
7328
7461
  return {
@@ -7333,7 +7466,7 @@ class NoRedundantAriaLabel extends Rule {
7333
7466
  setup() {
7334
7467
  this.on("dom:ready", (event) => {
7335
7468
  const { document } = event;
7336
- const elements = document.querySelectorAll(selectors.join(","));
7469
+ const elements = document.querySelectorAll(selectors$1.join(","));
7337
7470
  for (const element of elements) {
7338
7471
  const ariaLabel = element.getAttribute("aria-label");
7339
7472
  const id = element.id;
@@ -7949,7 +8082,7 @@ class RequireSri extends Rule {
7949
8082
  }
7950
8083
  documentation() {
7951
8084
  return {
7952
- description: `Subresource Integrity (SRI) \`integrity\` attribute is required to prevent manipulation from Content Delivery Networks or other third-party hosting.`,
8085
+ description: `Subresource Integrity (SRI) \`integrity\` attribute is required to prevent tampering or manipulation from Content Delivery Networks (CDN), rouge proxies, malicious entities, etc.`,
7953
8086
  url: "https://html-validate.org/rules/require-sri.html"
7954
8087
  };
7955
8088
  }
@@ -8362,6 +8495,130 @@ class TextContent extends Rule {
8362
8495
  }
8363
8496
  }
8364
8497
 
8498
+ const roles = ["complementary", "contentinfo", "form", "banner", "main", "navigation", "region"];
8499
+ const selectors = [
8500
+ "aside",
8501
+ "footer",
8502
+ "form",
8503
+ "header",
8504
+ "main",
8505
+ "nav",
8506
+ "section",
8507
+ ...roles.map((it) => `[role="${it}"]`)
8508
+ /* <search> does not (yet?) require a unique name */
8509
+ ];
8510
+ function getTextFromReference(document, id) {
8511
+ if (!id || id instanceof DynamicValue) {
8512
+ return id;
8513
+ }
8514
+ const selector = `#${id}`;
8515
+ const ref = document.querySelector(selector);
8516
+ if (ref) {
8517
+ return ref.textContent;
8518
+ } else {
8519
+ return selector;
8520
+ }
8521
+ }
8522
+ function groupBy(values, callback) {
8523
+ const result = {};
8524
+ for (const value of values) {
8525
+ const key = callback(value);
8526
+ if (key in result) {
8527
+ result[key].push(value);
8528
+ } else {
8529
+ result[key] = [value];
8530
+ }
8531
+ }
8532
+ return result;
8533
+ }
8534
+ function getTextEntryFromElement(document, node) {
8535
+ const ariaLabel = node.getAttribute("aria-label");
8536
+ if (ariaLabel) {
8537
+ return {
8538
+ node,
8539
+ text: ariaLabel.value,
8540
+ location: ariaLabel.keyLocation
8541
+ };
8542
+ }
8543
+ const ariaLabelledby = node.getAttribute("aria-labelledby");
8544
+ if (ariaLabelledby) {
8545
+ const text = getTextFromReference(document, ariaLabelledby.value);
8546
+ return {
8547
+ node,
8548
+ text,
8549
+ location: ariaLabelledby.keyLocation
8550
+ };
8551
+ }
8552
+ return {
8553
+ node,
8554
+ text: null,
8555
+ location: node.location
8556
+ };
8557
+ }
8558
+ function isExcluded(entry) {
8559
+ const { node, text } = entry;
8560
+ if (text === null) {
8561
+ return !(node.is("form") || node.is("section"));
8562
+ }
8563
+ return true;
8564
+ }
8565
+ class UniqueLandmark extends Rule {
8566
+ documentation() {
8567
+ return {
8568
+ description: [
8569
+ "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.",
8570
+ "For instance, if the document has two `<nav>` elements each of them need an accessible name to be distinguished from each other.",
8571
+ "",
8572
+ "The following elements / roles are considered landmarks:",
8573
+ "",
8574
+ ' - `aside` or `[role="complementary"]`',
8575
+ ' - `footer` or `[role="contentinfo"]`',
8576
+ ' - `form` or `[role="form"]`',
8577
+ ' - `header` or `[role="banner"]`',
8578
+ ' - `main` or `[role="main"]`',
8579
+ ' - `nav` or `[role="navigation"]`',
8580
+ ' - `section` or `[role="region"]`',
8581
+ "",
8582
+ "To fix this either:",
8583
+ "",
8584
+ " - Add `aria-label`.",
8585
+ " - Add `aria-labelledby`.",
8586
+ " - Remove one of the landmarks."
8587
+ ].join("\n"),
8588
+ url: "https://html-validate.org/rules/unique-landmark.html"
8589
+ };
8590
+ }
8591
+ setup() {
8592
+ this.on("dom:ready", (event) => {
8593
+ const { document } = event;
8594
+ const elements = document.querySelectorAll(selectors.join(",")).filter((it) => typeof it.role === "string" && roles.includes(it.role));
8595
+ const grouped = groupBy(elements, (it) => it.role);
8596
+ for (const nodes of Object.values(grouped)) {
8597
+ if (nodes.length <= 1) {
8598
+ continue;
8599
+ }
8600
+ const entries = nodes.map((it) => getTextEntryFromElement(document, it));
8601
+ const filteredEntries = entries.filter(isExcluded);
8602
+ for (const entry of filteredEntries) {
8603
+ if (entry.text instanceof DynamicValue) {
8604
+ continue;
8605
+ }
8606
+ const dup = entries.filter((it) => it.text === entry.text).length > 1;
8607
+ if (!entry.text || dup) {
8608
+ const message = `Landmarks must have a non-empty and unique accessible name (aria-label or aria-labelledby)`;
8609
+ const location = entry.location;
8610
+ this.report({
8611
+ node: entry.node,
8612
+ message,
8613
+ location
8614
+ });
8615
+ }
8616
+ }
8617
+ }
8618
+ });
8619
+ }
8620
+ }
8621
+
8365
8622
  const defaults$4 = {
8366
8623
  ignoreCase: false,
8367
8624
  requireSemicolon: true
@@ -8751,7 +9008,7 @@ class H32 extends Rule {
8751
9008
  }
8752
9009
  function isSubmit(node) {
8753
9010
  const type = node.getAttribute("type");
8754
- return Boolean(type && type.valueMatches(/submit|image/));
9011
+ return Boolean(!type || type.valueMatches(/submit|image/));
8755
9012
  }
8756
9013
  function isAssociated(id, node) {
8757
9014
  const form = node.getAttribute("form");
@@ -8843,29 +9100,35 @@ class H37 extends Rule {
8843
9100
  };
8844
9101
  }
8845
9102
  setup() {
8846
- this.on("tag:end", (event) => {
8847
- const node = event.previous;
8848
- if (!needsAlt(node)) {
8849
- return;
8850
- }
8851
- if (!inAccessibilityTree(node)) {
8852
- return;
9103
+ this.on("dom:ready", (event) => {
9104
+ const { document } = event;
9105
+ const nodes = document.querySelectorAll("img, input");
9106
+ for (const node of nodes) {
9107
+ this.validateNode(node);
8853
9108
  }
8854
- if (Boolean(node.getAttributeValue("alt")) || Boolean(node.hasAttribute("alt") && this.options.allowEmpty)) {
9109
+ });
9110
+ }
9111
+ validateNode(node) {
9112
+ if (!needsAlt(node)) {
9113
+ return;
9114
+ }
9115
+ if (!inAccessibilityTree(node)) {
9116
+ return;
9117
+ }
9118
+ if (Boolean(node.getAttributeValue("alt")) || Boolean(node.hasAttribute("alt") && this.options.allowEmpty)) {
9119
+ return;
9120
+ }
9121
+ for (const attr of this.options.alias) {
9122
+ if (node.getAttribute(attr)) {
8855
9123
  return;
8856
9124
  }
8857
- for (const attr of this.options.alias) {
8858
- if (node.getAttribute(attr)) {
8859
- return;
8860
- }
8861
- }
8862
- if (node.hasAttribute("alt")) {
8863
- const attr = node.getAttribute("alt");
8864
- this.report(node, `${getTag(node)} cannot have empty "alt" attribute`, attr == null ? void 0 : attr.keyLocation);
8865
- } else {
8866
- this.report(node, `${getTag(node)} is missing required "alt" attribute`, node.location);
8867
- }
8868
- });
9125
+ }
9126
+ if (node.hasAttribute("alt")) {
9127
+ const attr = node.getAttribute("alt");
9128
+ this.report(node, `${getTag(node)} cannot have empty "alt" attribute`, attr == null ? void 0 : attr.keyLocation);
9129
+ } else {
9130
+ this.report(node, `${getTag(node)} is missing required "alt" attribute`, node.location);
9131
+ }
8869
9132
  }
8870
9133
  }
8871
9134
 
@@ -8956,7 +9219,7 @@ class H71 extends Rule {
8956
9219
  }
8957
9220
  }
8958
9221
 
8959
- const bundledRules$2 = {
9222
+ const bundledRules$1 = {
8960
9223
  "wcag/h30": H30,
8961
9224
  "wcag/h32": H32,
8962
9225
  "wcag/h36": H36,
@@ -8965,7 +9228,7 @@ const bundledRules$2 = {
8965
9228
  "wcag/h67": H67,
8966
9229
  "wcag/h71": H71
8967
9230
  };
8968
- var WCAG = bundledRules$2;
9231
+ var WCAG = bundledRules$1;
8969
9232
 
8970
9233
  const bundledRules = {
8971
9234
  "allowed-links": AllowedLinks,
@@ -9001,6 +9264,7 @@ const bundledRules = {
9001
9264
  "empty-title": EmptyTitle,
9002
9265
  "form-dup-name": FormDupName,
9003
9266
  "heading-level": HeadingLevel,
9267
+ "hidden-focusable": HiddenFocusable,
9004
9268
  "id-pattern": IdPattern,
9005
9269
  "input-attributes": InputAttributes,
9006
9270
  "input-missing-label": InputMissingLabel,
@@ -9041,13 +9305,13 @@ const bundledRules = {
9041
9305
  "svg-focusable": SvgFocusable,
9042
9306
  "tel-non-breaking": TelNonBreaking,
9043
9307
  "text-content": TextContent,
9308
+ "unique-landmark": UniqueLandmark,
9044
9309
  "unrecognized-char-ref": UnknownCharReference,
9045
9310
  "valid-id": ValidID,
9046
9311
  "void-content": VoidContent,
9047
9312
  "void-style": VoidStyle,
9048
9313
  ...WCAG
9049
9314
  };
9050
- var bundledRules$1 = bundledRules;
9051
9315
 
9052
9316
  var defaultConfig = {};
9053
9317
 
@@ -9059,6 +9323,7 @@ const config$4 = {
9059
9323
  "deprecated-rule": "warn",
9060
9324
  "empty-heading": "error",
9061
9325
  "empty-title": "error",
9326
+ "hidden-focusable": "error",
9062
9327
  "meta-refresh": "error",
9063
9328
  "multiple-labeled-controls": "error",
9064
9329
  "no-autoplay": ["error", { include: ["audio", "video"] }],
@@ -9070,6 +9335,7 @@ const config$4 = {
9070
9335
  "prefer-native-element": "error",
9071
9336
  "svg-focusable": "off",
9072
9337
  "text-content": "error",
9338
+ "unique-landmark": "error",
9073
9339
  "wcag/h30": "error",
9074
9340
  "wcag/h32": "error",
9075
9341
  "wcag/h36": "error",
@@ -9132,6 +9398,7 @@ const config$1 = {
9132
9398
  "empty-heading": "error",
9133
9399
  "empty-title": "error",
9134
9400
  "form-dup-name": "error",
9401
+ "hidden-focusable": "error",
9135
9402
  "input-attributes": "error",
9136
9403
  "long-title": "error",
9137
9404
  "map-dup-name": "error",
@@ -9164,6 +9431,7 @@ const config$1 = {
9164
9431
  "svg-focusable": "off",
9165
9432
  "tel-non-breaking": "error",
9166
9433
  "text-content": "error",
9434
+ "unique-landmark": "error",
9167
9435
  "unrecognized-char-ref": "error",
9168
9436
  "valid-id": ["error", { relaxed: false }],
9169
9437
  void: "off",
@@ -9508,7 +9776,7 @@ class Config {
9508
9776
  if (configData.rules) {
9509
9777
  const normalizedRules = Config.getRulesObject(configData.rules);
9510
9778
  for (const [ruleId, [, ruleOptions]] of normalizedRules.entries()) {
9511
- const cls = bundledRules$1[ruleId];
9779
+ const cls = bundledRules[ruleId];
9512
9780
  const path = `/rules/${ruleId}/1`;
9513
9781
  Rule.validateOptions(cls, ruleId, path, ruleOptions, filename, configData);
9514
9782
  }
@@ -9881,16 +10149,17 @@ class EventHandler {
9881
10149
  * @returns Unregistration function.
9882
10150
  */
9883
10151
  on(event, callback) {
9884
- const names = event.split(",").map((x) => x.trim());
10152
+ const { listeners } = this;
10153
+ const names = event.split(",").map((it) => it.trim());
9885
10154
  for (const name of names) {
9886
- this.listeners[name] = this.listeners[name] || [];
9887
- this.listeners[name].push(callback);
10155
+ const list = listeners[name] ?? [];
10156
+ listeners[name] = list;
10157
+ list.push(callback);
9888
10158
  }
9889
10159
  return () => {
9890
10160
  for (const name of names) {
9891
- this.listeners[name] = this.listeners[name].filter((fn) => {
9892
- return fn !== callback;
9893
- });
10161
+ const list = listeners[name];
10162
+ this.listeners[name] = list.filter((fn) => fn !== callback);
9894
10163
  }
9895
10164
  };
9896
10165
  }
@@ -9916,10 +10185,15 @@ class EventHandler {
9916
10185
  * @param data - Event data.
9917
10186
  */
9918
10187
  trigger(event, data) {
9919
- const callbacks = [...this.listeners[event] ?? [], ...this.listeners["*"] ?? []];
9920
- callbacks.forEach((listener) => {
10188
+ for (const listener of this.getCallbacks(event)) {
9921
10189
  listener.call(null, event, data);
9922
- });
10190
+ }
10191
+ }
10192
+ getCallbacks(event) {
10193
+ const { listeners } = this;
10194
+ const callbacks = listeners[event] ?? [];
10195
+ const global = listeners["*"] ?? [];
10196
+ return [...callbacks, ...global];
9923
10197
  }
9924
10198
  }
9925
10199
 
@@ -9989,10 +10263,10 @@ class Parser {
9989
10263
  location: null
9990
10264
  });
9991
10265
  this.dom = new DOMTree({
9992
- filename: source.filename ?? "",
9993
- offset: source.offset ?? 0,
9994
- line: source.line ?? 1,
9995
- column: source.column ?? 1,
10266
+ filename: source.filename,
10267
+ offset: source.offset,
10268
+ line: source.line,
10269
+ column: source.column,
9996
10270
  size: 0
9997
10271
  });
9998
10272
  this.trigger("dom:load", {
@@ -10516,7 +10790,7 @@ function isThenable(value) {
10516
10790
  return value && typeof value === "object" && "then" in value && typeof value.then === "function";
10517
10791
  }
10518
10792
 
10519
- const ruleIds = new Set(Object.keys(bundledRules$1));
10793
+ const ruleIds = new Set(Object.keys(bundledRules));
10520
10794
  function ruleExists(ruleId) {
10521
10795
  return ruleIds.has(ruleId);
10522
10796
  }
@@ -10605,9 +10879,7 @@ class Reporter {
10605
10879
  valid: this.isValid(),
10606
10880
  results: Object.keys(this.result).map((filePath) => {
10607
10881
  const messages = Array.from(this.result[filePath], freeze).sort(messageSort);
10608
- const source = (sources ?? []).find(
10609
- (source2) => filePath === (source2.filename ?? "")
10610
- );
10882
+ const source = (sources ?? []).find((source2) => filePath === source2.filename);
10611
10883
  return {
10612
10884
  filePath,
10613
10885
  messages,
@@ -10675,7 +10947,7 @@ class Engine {
10675
10947
  this.ParserClass = ParserClass;
10676
10948
  const result = this.initPlugins(this.config);
10677
10949
  this.availableRules = {
10678
- ...bundledRules$1,
10950
+ ...bundledRules,
10679
10951
  ...result.availableRules
10680
10952
  };
10681
10953
  }
@@ -11104,10 +11376,11 @@ class HtmlValidate {
11104
11376
  * @returns Report output.
11105
11377
  */
11106
11378
  async validateSource(input, configOverride) {
11107
- const config = await this.getConfigFor(input.filename, configOverride);
11108
- const source = config.transformSource(input);
11379
+ const source = normalizeSource(input);
11380
+ const config = await this.getConfigFor(source.filename, configOverride);
11381
+ const transformedSource = config.transformSource(source);
11109
11382
  const engine = new Engine(config, Parser);
11110
- return engine.lint(source);
11383
+ return engine.lint(transformedSource);
11111
11384
  }
11112
11385
  /**
11113
11386
  * Parse and validate HTML from [[Source]].
@@ -11117,10 +11390,11 @@ class HtmlValidate {
11117
11390
  * @returns Report output.
11118
11391
  */
11119
11392
  validateSourceSync(input, configOverride) {
11120
- const config = this.getConfigForSync(input.filename, configOverride);
11121
- const source = config.transformSource(input);
11393
+ const source = normalizeSource(input);
11394
+ const config = this.getConfigForSync(source.filename, configOverride);
11395
+ const transformedSource = config.transformSource(source);
11122
11396
  const engine = new Engine(config, Parser);
11123
- return engine.lint(source);
11397
+ return engine.lint(transformedSource);
11124
11398
  }
11125
11399
  /**
11126
11400
  * Parse and validate HTML from file.
@@ -11429,7 +11703,7 @@ class HtmlValidate {
11429
11703
  }
11430
11704
 
11431
11705
  const name = "html-validate";
11432
- const version = "8.8.0";
11706
+ const version = "8.9.1";
11433
11707
  const bugs = "https://gitlab.com/html-validate/html-validate/issues/new";
11434
11708
 
11435
11709
  function definePlugin(plugin) {