@markuplint/ml-spec 5.2.0 → 5.4.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/CHANGELOG.md CHANGED
@@ -3,6 +3,26 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [5.4.0](https://github.com/markuplint/markuplint/compare/v5.3.0...v5.4.0) (2026-10-29)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **ml-spec:** keep roles that are not permitted and align the accessible name with AccName ([6c38c60](https://github.com/markuplint/markuplint/commit/6c38c60971690edec8b6453e50c76146eb10deb2)), closes [#4408](https://github.com/markuplint/markuplint/issues/4408)
11
+
12
+
13
+ # [5.3.0](https://github.com/markuplint/markuplint/compare/v5.2.0...v5.3.0) (2026-10-21)
14
+
15
+ ### Bug Fixes
16
+
17
+ * make published packages loadable from require() and put types first ([723106f](https://github.com/markuplint/markuplint/commit/723106f51aac4e60dc02db2c1a894617368b0253)), closes [#4198](https://github.com/markuplint/markuplint/issues/4198)
18
+ * **ml-spec:** ask the element whether a name is the attribute, and keep the spelling of a directive ([e414513](https://github.com/markuplint/markuplint/commit/e414513598bf068c323dc335b4177c63941d5775))
19
+ * **ml-spec:** keep the results of getSpecByTagName for each list of specs ([3322329](https://github.com/markuplint/markuplint/commit/33223295d7990561a8686a80029706c096e65666))
20
+
21
+ ### Reverts
22
+
23
+ * **ml-spec:** leave is-exposed as it was on dev ([ce4c5c8](https://github.com/markuplint/markuplint/commit/ce4c5c833ca542bcfef8460d1a8228c2a3144f87))
24
+
25
+
6
26
  # [5.2.0](https://github.com/markuplint/markuplint/compare/v5.1.1...v5.2.0) (2026-10-13)
7
27
 
8
28
  ### Bug Fixes
@@ -1,4 +1,4 @@
1
- import type { AccnameElement, AccnameResolver, AccnameResult } from './types.js';
1
+ import type { AccnameComputeFn, AccnameElement, AccnameResolver, AccnameResult } from './types.js';
2
2
  /**
3
3
  * Implements AccName 1.2 §4.3.2 Step 2B:
4
4
  * "If the current node has an `aria-labelledby` attribute that contains
@@ -16,7 +16,19 @@ import type { AccnameElement, AccnameResolver, AccnameResult } from './types.js'
16
16
  *
17
17
  * @see https://www.w3.org/TR/accname-1.2/#comp_labelledby_traversal — AccName 1.2 §4.3.2 Step 2B
18
18
  */
19
- export declare function resolveAriaLabelledby(el: AccnameElement, resolver: AccnameResolver, visited: ReadonlySet<string>, computeFn: (el: AccnameElement, resolver: AccnameResolver, inLabelledbyTraversal: boolean, visited: ReadonlySet<string>) => AccnameResult): AccnameResult | null;
19
+ export declare function resolveAriaLabelledby(el: AccnameElement, resolver: AccnameResolver, visited: ReadonlySet<string>, computeFn: AccnameComputeFn): AccnameResult | null;
20
+ /**
21
+ * Computes the text alternative of a node directly referenced by
22
+ * `aria-labelledby` (Step 2B, LabelledBy Traversal).
23
+ *
24
+ * Step 2A is skipped for the whole subtree only when the referenced node is
25
+ * hidden; otherwise its hidden descendants contribute nothing.
26
+ *
27
+ * @see https://w3c.github.io/accname/#comp_hidden_not_referenced
28
+ *
29
+ * > If the current node is hidden and is: Not part of an aria-labelledby or aria-describedby traversal, where the node directly referenced by that relation was hidden.
30
+ */
31
+ export declare function computeReferenced(referenced: AccnameElement, resolver: AccnameResolver, visited: ReadonlySet<string>, computeFn: AccnameComputeFn): AccnameResult;
20
32
  /**
21
33
  * Implements AccName 1.2 §4.3.2 Step 2D:
22
34
  * "If the current node has an `aria-label` attribute whose value is
@@ -1,5 +1,5 @@
1
1
  /* eslint-disable @typescript-eslint/prefer-readonly-parameter-types -- AccnameElement wraps mutable DOM types */
2
- import { flattenText, makeResult } from './helpers.js';
2
+ import { flattenText, makeResult } from './result.js';
3
3
  /**
4
4
  * Implements AccName 1.2 §4.3.2 Step 2B:
5
5
  * "If the current node has an `aria-labelledby` attribute that contains
@@ -52,7 +52,7 @@ export function resolveAriaLabelledby(el, resolver, visited, computeFn) {
52
52
  // Each branch gets its own visited set so that one IDREF's
53
53
  // traversal doesn't block resolution of a later IDREF.
54
54
  const innerVisited = new Set([...newVisited, id]);
55
- const result = computeFn(referenced, resolver, true, innerVisited);
55
+ const result = computeReferenced(referenced, resolver, innerVisited, computeFn);
56
56
  if (result.name) {
57
57
  parts.push(result.name);
58
58
  }
@@ -63,6 +63,26 @@ export function resolveAriaLabelledby(el, resolver, visited, computeFn) {
63
63
  }
64
64
  return null;
65
65
  }
66
+ /**
67
+ * Computes the text alternative of a node directly referenced by
68
+ * `aria-labelledby` (Step 2B, LabelledBy Traversal).
69
+ *
70
+ * Step 2A is skipped for the whole subtree only when the referenced node is
71
+ * hidden; otherwise its hidden descendants contribute nothing.
72
+ *
73
+ * @see https://w3c.github.io/accname/#comp_hidden_not_referenced
74
+ *
75
+ * > If the current node is hidden and is: Not part of an aria-labelledby or aria-describedby traversal, where the node directly referenced by that relation was hidden.
76
+ */
77
+ export function computeReferenced(referenced, resolver, visited, computeFn) {
78
+ const traversalResolver = resolver.isHidden(referenced)
79
+ ? {
80
+ ...resolver,
81
+ isHidden: () => false,
82
+ }
83
+ : resolver;
84
+ return computeFn(referenced, traversalResolver, true, visited);
85
+ }
66
86
  /**
67
87
  * Implements AccName 1.2 §4.3.2 Step 2D:
68
88
  * "If the current node has an `aria-label` attribute whose value is
@@ -8,8 +8,9 @@ import type { AccnameElement, AccnameResolver, AccnameResult } from './types.js'
8
8
  *
9
9
  * Control flow (Steps map to AccName 1.2 §4.3.2):
10
10
  *
11
- * 1. **Step 2A — Hidden check** — If the element is hidden (per `isHidden`) and NOT
12
- * referenced by `aria-labelledby`, return empty immediately.
11
+ * 1. **Step 2A — Hidden check** — If the element is hidden (per `isHidden`), return
12
+ * empty immediately, unless it is in an `aria-labelledby` traversal whose
13
+ * directly referenced node is hidden.
13
14
  * 2. **Pre-computed name** — (Implementation-specific extension) If the
14
15
  * resolver provides `getPrecomputedName`, check it before standard steps.
15
16
  * Used by ml-core's Pretender integration for framework components.
@@ -18,7 +19,9 @@ import type { AccnameElement, AccnameResolver, AccnameResult } from './types.js'
18
19
  * labelledby traversal (`inLabelledbyTraversal`) to prevent re-entry.
19
20
  * 4. **Step 2D — `aria-label`** — Use the `aria-label` attribute value.
20
21
  * 5. **Step 2E — Element-specific name** — Dispatch to HTML-AAM §4.1
21
- * element-specific rules (label association, alt, value, legend, caption, SVG title).
22
+ * element-specific rules (label association, alt, value, legend, caption, SVG title),
23
+ * skipped for an element the resolver reports as exposed as presentational
24
+ * (`isExposedAsPresentational`).
22
25
  * 6. **Step 2F — Name from content** — If the element's role allows
23
26
  * `nameFrom: ["content"]`, or the element is referenced by `aria-labelledby`
24
27
  * (`inLabelledbyTraversal`), recursively collect child text (including
@@ -32,6 +35,35 @@ import type { AccnameElement, AccnameResolver, AccnameResult } from './types.js'
32
35
  * @see https://www.w3.org/TR/html-aam-1.0/#accessible-name-and-description-computation — HTML-AAM §4.1
33
36
  */
34
37
  export declare function computeAccessibleName(el: AccnameElement, resolver: AccnameResolver): AccnameResult;
38
+ /**
39
+ * Computes the text alternative of an element as a node referenced by
40
+ * `aria-labelledby` or `aria-describedby` (the LabelledBy Traversal of Step 2B,
41
+ * which the description computation also uses).
42
+ *
43
+ * @param el - The referenced element
44
+ * @param resolver - Environment-dependent resolver for DOM traversal and role queries
45
+ * @returns The computed text alternative and its source
46
+ * @see https://w3c.github.io/accname/#comp_labelledby_traversal
47
+ */
48
+ export declare function computeReferencedTextAlternative(el: AccnameElement, resolver: AccnameResolver): AccnameResult;
49
+ /**
50
+ * Computes the host language text alternative of an element (AccName §4.3.2
51
+ * Step 2E, HTML-AAM §4.1), such as the `alt` of an image or the `title` of an
52
+ * SVG element, whether or not the element is exposed as presentational.
53
+ *
54
+ * Tells what an explicit `none` or `presentation` role drops from the name:
55
+ * every source the HTML-AAM rules of Step 2E give (`label`, `alt`, `value`,
56
+ * `placeholder`, `legend`, `caption`, `svg-title`, and the `default` label of a
57
+ * submit, reset or image button). The sources that Step 2E shares with other
58
+ * steps (`content`, the `title` attribute) are not dropped by the role, and give
59
+ * `null`.
60
+ *
61
+ * @param el - The element
62
+ * @param resolver - Environment-dependent resolver for DOM traversal and role queries
63
+ * @returns The text alternative and its source, or `null` if the element has none
64
+ * @see https://w3c.github.io/accname/#comp_host_language_label
65
+ */
66
+ export declare function computeHostLanguageLabel(el: AccnameElement, resolver: AccnameResolver): AccnameResult | null;
35
67
  /**
36
68
  * @param inLabelledbyTraversal - When true, Step 2B is skipped to prevent re-entry
37
69
  * into `resolveAriaLabelledby`, and Step 2F name-from-content is enabled regardless
@@ -1,7 +1,8 @@
1
1
  /* eslint-disable @typescript-eslint/prefer-readonly-parameter-types -- AccnameElement wraps mutable DOM types */
2
- import { resolveAriaLabel, resolveAriaLabelledby } from './aria-steps.js';
2
+ import { computeReferenced, resolveAriaLabel, resolveAriaLabelledby } from './aria-steps.js';
3
3
  import { getElementSpecificName } from './element-names.js';
4
- import { makeResult, resolveNameFromContent } from './helpers.js';
4
+ import { resolveNameFromContent } from './helpers.js';
5
+ import { makeResult } from './result.js';
5
6
  const EMPTY_RESULT = { name: '', source: null };
6
7
  /**
7
8
  * Computes the accessible name for an element.
@@ -12,8 +13,9 @@ const EMPTY_RESULT = { name: '', source: null };
12
13
  *
13
14
  * Control flow (Steps map to AccName 1.2 §4.3.2):
14
15
  *
15
- * 1. **Step 2A — Hidden check** — If the element is hidden (per `isHidden`) and NOT
16
- * referenced by `aria-labelledby`, return empty immediately.
16
+ * 1. **Step 2A — Hidden check** — If the element is hidden (per `isHidden`), return
17
+ * empty immediately, unless it is in an `aria-labelledby` traversal whose
18
+ * directly referenced node is hidden.
17
19
  * 2. **Pre-computed name** — (Implementation-specific extension) If the
18
20
  * resolver provides `getPrecomputedName`, check it before standard steps.
19
21
  * Used by ml-core's Pretender integration for framework components.
@@ -22,7 +24,9 @@ const EMPTY_RESULT = { name: '', source: null };
22
24
  * labelledby traversal (`inLabelledbyTraversal`) to prevent re-entry.
23
25
  * 4. **Step 2D — `aria-label`** — Use the `aria-label` attribute value.
24
26
  * 5. **Step 2E — Element-specific name** — Dispatch to HTML-AAM §4.1
25
- * element-specific rules (label association, alt, value, legend, caption, SVG title).
27
+ * element-specific rules (label association, alt, value, legend, caption, SVG title),
28
+ * skipped for an element the resolver reports as exposed as presentational
29
+ * (`isExposedAsPresentational`).
26
30
  * 6. **Step 2F — Name from content** — If the element's role allows
27
31
  * `nameFrom: ["content"]`, or the element is referenced by `aria-labelledby`
28
32
  * (`inLabelledbyTraversal`), recursively collect child text (including
@@ -38,6 +42,43 @@ const EMPTY_RESULT = { name: '', source: null };
38
42
  export function computeAccessibleName(el, resolver) {
39
43
  return computeAccessibleNameInternal(el, resolver, false, new Set());
40
44
  }
45
+ /**
46
+ * Computes the text alternative of an element as a node referenced by
47
+ * `aria-labelledby` or `aria-describedby` (the LabelledBy Traversal of Step 2B,
48
+ * which the description computation also uses).
49
+ *
50
+ * @param el - The referenced element
51
+ * @param resolver - Environment-dependent resolver for DOM traversal and role queries
52
+ * @returns The computed text alternative and its source
53
+ * @see https://w3c.github.io/accname/#comp_labelledby_traversal
54
+ */
55
+ export function computeReferencedTextAlternative(el, resolver) {
56
+ return computeReferenced(el, resolver, new Set(el.id ? [el.id] : []), computeAccessibleNameInternal);
57
+ }
58
+ /**
59
+ * Computes the host language text alternative of an element (AccName §4.3.2
60
+ * Step 2E, HTML-AAM §4.1), such as the `alt` of an image or the `title` of an
61
+ * SVG element, whether or not the element is exposed as presentational.
62
+ *
63
+ * Tells what an explicit `none` or `presentation` role drops from the name:
64
+ * every source the HTML-AAM rules of Step 2E give (`label`, `alt`, `value`,
65
+ * `placeholder`, `legend`, `caption`, `svg-title`, and the `default` label of a
66
+ * submit, reset or image button). The sources that Step 2E shares with other
67
+ * steps (`content`, the `title` attribute) are not dropped by the role, and give
68
+ * `null`.
69
+ *
70
+ * @param el - The element
71
+ * @param resolver - Environment-dependent resolver for DOM traversal and role queries
72
+ * @returns The text alternative and its source, or `null` if the element has none
73
+ * @see https://w3c.github.io/accname/#comp_host_language_label
74
+ */
75
+ export function computeHostLanguageLabel(el, resolver) {
76
+ const result = getElementSpecificName(el, resolver, new Set(), computeAccessibleNameInternal, false);
77
+ if (!result?.source || result.source === 'content' || result.source === 'title') {
78
+ return null;
79
+ }
80
+ return result;
81
+ }
41
82
  /**
42
83
  * @param inLabelledbyTraversal - When true, Step 2B is skipped to prevent re-entry
43
84
  * into `resolveAriaLabelledby`, and Step 2F name-from-content is enabled regardless
@@ -45,8 +86,12 @@ export function computeAccessibleName(el, resolver) {
45
86
  * @see https://www.w3.org/TR/accname-1.2/#computation-steps — AccName 1.2 §4.3.2
46
87
  */
47
88
  export function computeAccessibleNameInternal(el, resolver, inLabelledbyTraversal, visited) {
48
- // AccName 1.2 §4.3.2 Step 2A: Hidden elements return empty unless referenced by aria-labelledby
49
- if (!inLabelledbyTraversal && resolver.isHidden(el)) {
89
+ // AccName 1.2 §4.3.2 Step 2A: Hidden elements return empty. In an aria-labelledby
90
+ // traversal whose referenced node is hidden, `computeReferenced` passes a
91
+ // resolver that finds nothing hidden.
92
+ const hidden = resolver.isHidden(el);
93
+ resolver.onVisit?.(el, { hidden, embeddedControl: false, inLabelledbyTraversal });
94
+ if (hidden) {
50
95
  return EMPTY_RESULT;
51
96
  }
52
97
  // [Implementation-specific] Pre-computed name (e.g., Pretender integration) — checked after hidden check
@@ -68,8 +113,11 @@ export function computeAccessibleNameInternal(el, resolver, inLabelledbyTraversa
68
113
  if (ariaLabelResult) {
69
114
  return ariaLabelResult;
70
115
  }
71
- // AccName 1.2 §4.3.2 Step 2E: Element-specific name (HTML-AAM §4.1)
72
- const elementResult = getElementSpecificName(el, resolver, visited, computeAccessibleNameInternal, inLabelledbyTraversal);
116
+ // AccName 1.2 §4.3.2 Step 2E: Element-specific name (HTML-AAM §4.1),
117
+ // "unless the current node is exposed as presentational"
118
+ const elementResult = resolver.isExposedAsPresentational?.(el)
119
+ ? null
120
+ : getElementSpecificName(el, resolver, visited, computeAccessibleNameInternal, inLabelledbyTraversal);
73
121
  if (elementResult) {
74
122
  return elementResult;
75
123
  }
@@ -1,4 +1,4 @@
1
- import type { AccnameElement, AccnameResolver, AccnameResult } from './types.js';
1
+ import type { AccnameComputeFn, AccnameElement, AccnameResolver, AccnameResult } from './types.js';
2
2
  /**
3
3
  * Implements AccName 1.2 §4.3.2 Step 2E: for elements that have a native
4
4
  * host language text alternative, use that alternative. The specific rules
@@ -10,4 +10,4 @@ import type { AccnameElement, AccnameResolver, AccnameResult } from './types.js'
10
10
  * @see https://www.w3.org/TR/accname-1.2/#computation-steps — AccName 1.2 §4.3.2 Step 2E
11
11
  * @see https://www.w3.org/TR/html-aam-1.0/#accessible-name-and-description-computation — HTML-AAM §4.1
12
12
  */
13
- export declare function getElementSpecificName(el: AccnameElement, resolver: AccnameResolver, visited: ReadonlySet<string>, computeFn: (el: AccnameElement, resolver: AccnameResolver, inLabelledbyTraversal: boolean, visited: ReadonlySet<string>) => AccnameResult, inLabelledbyTraversal: boolean): AccnameResult | null;
13
+ export declare function getElementSpecificName(el: AccnameElement, resolver: AccnameResolver, visited: ReadonlySet<string>, computeFn: AccnameComputeFn, inLabelledbyTraversal: boolean): AccnameResult | null;
@@ -1,7 +1,8 @@
1
1
  /* eslint-disable @typescript-eslint/prefer-readonly-parameter-types -- AccnameElement wraps mutable DOM types */
2
2
  import { DEFAULT_IMAGE_LABEL, DEFAULT_RESET_LABEL, DEFAULT_SUBMIT_LABEL, TEXT_INPUT_TYPES, } from '../../../const/index.js';
3
- import { findChildByLocalName, getInputType, getTextContent, isSvgElement, makeResult, resolveNameFromContent, } from './helpers.js';
3
+ import { findChildByLocalName, getInputType, getTextContent, isSvgElement, resolveNameFromContent } from './helpers.js';
4
4
  import { resolveLabelText } from './label-steps.js';
5
+ import { makeResult } from './result.js';
5
6
  /**
6
7
  * Implements AccName 1.2 §4.3.2 Step 2E: for elements that have a native
7
8
  * host language text alternative, use that alternative. The specific rules
@@ -1,7 +1,5 @@
1
- import type { AccnameElement, AccnameNode, AccnameResolver, AccnameResult, AccnameSource } from './types.js';
1
+ import type { AccnameComputeFn, AccnameElement, AccnameNode, AccnameResolver } from './types.js';
2
2
  export { EMBEDDED_CONTROL_ROLES } from '../../../const/index.js';
3
- export declare function makeResult(name: string, source: AccnameSource | null): AccnameResult;
4
- export declare function flattenText(text: string): string;
5
3
  /**
6
4
  * Implements AccName 1.2 §4.3.2 Steps 2F and 2C:
7
5
  *
@@ -21,7 +19,19 @@ export declare function flattenText(text: string): string;
21
19
  * @see https://www.w3.org/TR/accname-1.2/#computation-steps — AccName 1.2 §4.3.2 Step 2F
22
20
  * @see https://www.w3.org/TR/accname-1.2/#comp_embedded_control — AccName 1.2 §4.3.2 Step 2C
23
21
  */
24
- export declare function resolveNameFromContent(el: AccnameElement, resolver: AccnameResolver, visited: ReadonlySet<string>, computeFn: (el: AccnameElement, resolver: AccnameResolver, inLabelledbyTraversal: boolean, visited: ReadonlySet<string>) => AccnameResult, inLabelledbyTraversal: boolean): string;
22
+ export declare function resolveNameFromContent(el: AccnameElement, resolver: AccnameResolver, visited: ReadonlySet<string>, computeFn: AccnameComputeFn, inLabelledbyTraversal: boolean): string;
23
+ /**
24
+ * The text an embedded control reached by name-from-content recursion
25
+ * contributes, in the order of AccName §4.3.2: nothing when it is hidden
26
+ * (Step 2A), the text of its `aria-labelledby` outside an ongoing
27
+ * `aria-labelledby` traversal (Step 2B), otherwise its value (Step 2C), as its
28
+ * `aria-label` is ignored (Step 2D).
29
+ *
30
+ * @see https://w3c.github.io/accname/#comp_label
31
+ *
32
+ * > If traversal of the current node is due to Name From Content descendant node recursion and the current node is an embedded control, ignore aria-label and skip to rule Embedded Control.
33
+ */
34
+ export declare function embeddedControlText(el: AccnameElement, resolver: AccnameResolver, visited: ReadonlySet<string>, computeFn: AccnameComputeFn, inLabelledbyTraversal: boolean): string;
25
35
  /**
26
36
  * The child nodes to traverse for `el`: what the resolver says it renders,
27
37
  * or `el.childNodes` when it says nothing.
@@ -38,6 +48,15 @@ export declare function findAncestorLabel(el: AccnameElement): AccnameElement |
38
48
  export declare function resolveLabel(el: AccnameElement, resolver: AccnameResolver): readonly AccnameElement[];
39
49
  export declare function getInputType(el: AccnameElement): string;
40
50
  export declare function isSvgElement(el: AccnameElement): boolean;
51
+ /**
52
+ * Whether the element is an SVG `title` or `desc`. They are never rendered,
53
+ * so their text is not content of the parent: a `title` gives the name only
54
+ * through Step 2E of its parent (`handleSvgElement`), which an element exposed
55
+ * as presentational skips.
56
+ *
57
+ * @see https://w3c.github.io/svgwg/svg2-draft/struct.html#TermNeverRenderedElement
58
+ */
59
+ export declare function isSvgDescriptiveElement(el: AccnameElement): boolean;
41
60
  /**
42
61
  * Escapes a string for safe use inside a CSS selector.
43
62
  *
@@ -1,16 +1,7 @@
1
1
  /* eslint-disable @typescript-eslint/prefer-readonly-parameter-types -- AccnameElement wraps mutable DOM types */
2
2
  import { ELEMENT_NODE, SVG_NAMESPACE, TEXT_NODE, TEXT_LIKE_INPUT_TYPES } from '../../../const/index.js';
3
+ import { resolveAriaLabelledby } from './aria-steps.js';
3
4
  export { EMBEDDED_CONTROL_ROLES } from '../../../const/index.js';
4
- export function makeResult(name, source) {
5
- const trimmed = flattenText(name);
6
- return {
7
- name: trimmed,
8
- source: trimmed ? source : null,
9
- };
10
- }
11
- export function flattenText(text) {
12
- return text.replaceAll(/\s+/g, ' ').trim();
13
- }
14
5
  /**
15
6
  * Implements AccName 1.2 §4.3.2 Steps 2F and 2C:
16
7
  *
@@ -38,9 +29,12 @@ export function resolveNameFromContent(el, resolver, visited, computeFn, inLabel
38
29
  }
39
30
  else if (child.nodeType === ELEMENT_NODE) {
40
31
  const childEl = child;
32
+ if (isSvgDescriptiveElement(childEl)) {
33
+ continue;
34
+ }
41
35
  if (resolver.isEmbeddedControl(childEl)) {
42
36
  // Step 2C: Embedded controls — use value directly, aria-label is ignored
43
- parts.push(getEmbeddedControlValue(childEl, resolver));
37
+ parts.push(embeddedControlText(childEl, resolver, visited, computeFn, inLabelledbyTraversal));
44
38
  }
45
39
  else {
46
40
  const result = computeFn(childEl, resolver, inLabelledbyTraversal, visited);
@@ -84,8 +78,11 @@ function collectTextContent(el, resolver, visited, computeFn, inLabelledbyTraver
84
78
  }
85
79
  else if (child.nodeType === ELEMENT_NODE) {
86
80
  const childEl = child;
81
+ if (isSvgDescriptiveElement(childEl)) {
82
+ continue;
83
+ }
87
84
  if (resolver.isEmbeddedControl(childEl)) {
88
- parts.push(getEmbeddedControlValue(childEl, resolver));
85
+ parts.push(embeddedControlText(childEl, resolver, visited, computeFn, inLabelledbyTraversal));
89
86
  }
90
87
  else {
91
88
  const result = computeFn(childEl, resolver, inLabelledbyTraversal, visited);
@@ -100,6 +97,31 @@ function collectTextContent(el, resolver, visited, computeFn, inLabelledbyTraver
100
97
  }
101
98
  return parts.join(' ');
102
99
  }
100
+ /**
101
+ * The text an embedded control reached by name-from-content recursion
102
+ * contributes, in the order of AccName §4.3.2: nothing when it is hidden
103
+ * (Step 2A), the text of its `aria-labelledby` outside an ongoing
104
+ * `aria-labelledby` traversal (Step 2B), otherwise its value (Step 2C), as its
105
+ * `aria-label` is ignored (Step 2D).
106
+ *
107
+ * @see https://w3c.github.io/accname/#comp_label
108
+ *
109
+ * > If traversal of the current node is due to Name From Content descendant node recursion and the current node is an embedded control, ignore aria-label and skip to rule Embedded Control.
110
+ */
111
+ export function embeddedControlText(el, resolver, visited, computeFn, inLabelledbyTraversal) {
112
+ const hidden = resolver.isHidden(el);
113
+ resolver.onVisit?.(el, { hidden, embeddedControl: true, inLabelledbyTraversal });
114
+ if (hidden) {
115
+ return '';
116
+ }
117
+ if (!inLabelledbyTraversal) {
118
+ const labelledby = resolveAriaLabelledby(el, resolver, visited, computeFn);
119
+ if (labelledby) {
120
+ return labelledby.name;
121
+ }
122
+ }
123
+ return getEmbeddedControlValue(el, resolver);
124
+ }
103
125
  /**
104
126
  * Implements AccName 1.2 §4.3.2 Step 2C embedded control value resolution:
105
127
  * - **Range controls** (slider/spinbutton/input[range]):
@@ -287,6 +309,17 @@ export function getInputType(el) {
287
309
  export function isSvgElement(el) {
288
310
  return el.namespaceURI === SVG_NAMESPACE;
289
311
  }
312
+ /**
313
+ * Whether the element is an SVG `title` or `desc`. They are never rendered,
314
+ * so their text is not content of the parent: a `title` gives the name only
315
+ * through Step 2E of its parent (`handleSvgElement`), which an element exposed
316
+ * as presentational skips.
317
+ *
318
+ * @see https://w3c.github.io/svgwg/svg2-draft/struct.html#TermNeverRenderedElement
319
+ */
320
+ export function isSvgDescriptiveElement(el) {
321
+ return isSvgElement(el) && (el.localName === 'title' || el.localName === 'desc');
322
+ }
290
323
  /**
291
324
  * Escapes a string for safe use inside a CSS selector.
292
325
  *
@@ -1,4 +1,4 @@
1
- export { computeAccessibleName } from './compute.js';
1
+ export { computeAccessibleName, computeHostLanguageLabel, computeReferencedTextAlternative } from './compute.js';
2
2
  export { escapeCSS } from './helpers.js';
3
3
  export { hasSvgAccessibleNameSource } from './svg-helpers.js';
4
- export type { AccnameElement, AccnameNode, AccnameResolver, AccnameResult, AccnameSource } from './types.js';
4
+ export type { AccnameElement, AccnameNode, AccnameResolver, AccnameResult, AccnameSource, AccnameVisit, } from './types.js';
@@ -1,3 +1,3 @@
1
- export { computeAccessibleName } from './compute.js';
1
+ export { computeAccessibleName, computeHostLanguageLabel, computeReferencedTextAlternative } from './compute.js';
2
2
  export { escapeCSS } from './helpers.js';
3
3
  export { hasSvgAccessibleNameSource } from './svg-helpers.js';
@@ -1,4 +1,4 @@
1
- import type { AccnameElement, AccnameResolver, AccnameResult } from './types.js';
1
+ import type { AccnameComputeFn, AccnameElement, AccnameResolver, AccnameResult } from './types.js';
2
2
  /**
3
3
  * Part of AccName 1.2 §4.3.2 Step 2E — for labelable elements, the HTML label
4
4
  * association is checked before other element-specific rules.
@@ -6,4 +6,4 @@ import type { AccnameElement, AccnameResolver, AccnameResult } from './types.js'
6
6
  * @see https://www.w3.org/TR/accname-1.2/#computation-steps — AccName 1.2 §4.3.2 Step 2E
7
7
  * @see https://www.w3.org/TR/html-aam-1.0/#el-input-text — HTML-AAM label association
8
8
  */
9
- export declare function resolveLabelText(el: AccnameElement, resolver: AccnameResolver, visited: ReadonlySet<string>, computeFn: (el: AccnameElement, resolver: AccnameResolver, inLabelledbyTraversal: boolean, visited: ReadonlySet<string>) => AccnameResult, inLabelledbyTraversal: boolean): AccnameResult | null;
9
+ export declare function resolveLabelText(el: AccnameElement, resolver: AccnameResolver, visited: ReadonlySet<string>, computeFn: AccnameComputeFn, inLabelledbyTraversal: boolean): AccnameResult | null;
@@ -1,6 +1,7 @@
1
1
  /* eslint-disable @typescript-eslint/prefer-readonly-parameter-types -- AccnameElement wraps mutable DOM types */
2
2
  import { ELEMENT_NODE, TEXT_NODE } from '../../../const/index.js';
3
- import { flattenText, getChildNodes, makeResult, resolveLabel } from './helpers.js';
3
+ import { embeddedControlText, getChildNodes, isSvgDescriptiveElement, resolveLabel } from './helpers.js';
4
+ import { flattenText, makeResult } from './result.js';
4
5
  /**
5
6
  * Part of AccName 1.2 §4.3.2 Step 2E — for labelable elements, the HTML label
6
7
  * association is checked before other element-specific rules.
@@ -30,6 +31,10 @@ export function resolveLabelText(el, resolver, visited, computeFn, inLabelledbyT
30
31
  * Excludes the labeled element itself to prevent circular inclusion. Parts are
31
32
  * joined with a space separator, matching the AccName 1.2 concatenation
32
33
  * behavior for label text.
34
+ *
35
+ * The children are treated as name-from-content recursion treats them
36
+ * (`resolveNameFromContent`): a hidden one and an SVG `title` or `desc`
37
+ * contribute nothing, and an embedded control contributes its value.
33
38
  */
34
39
  function collectLabelText(label, labeledElement, resolver, visited, computeFn, inLabelledbyTraversal) {
35
40
  const parts = [];
@@ -39,14 +44,18 @@ function collectLabelText(label, labeledElement, resolver, visited, computeFn, i
39
44
  }
40
45
  else if (child.nodeType === ELEMENT_NODE) {
41
46
  const childEl = child;
42
- if (childEl === labeledElement) {
47
+ if (childEl === labeledElement || isSvgDescriptiveElement(childEl)) {
48
+ continue;
49
+ }
50
+ if (resolver.isEmbeddedControl(childEl)) {
51
+ parts.push(embeddedControlText(childEl, resolver, visited, computeFn, inLabelledbyTraversal));
43
52
  continue;
44
53
  }
45
54
  const result = computeFn(childEl, resolver, inLabelledbyTraversal, visited);
46
55
  if (result.name) {
47
56
  parts.push(result.name);
48
57
  }
49
- else {
58
+ else if (!resolver.isHidden(childEl)) {
50
59
  // Transparent traversal: when the child element has no accessible name
51
60
  // (e.g., <span> with role="generic"), recursively collect its descendant text.
52
61
  // This matches the behavior of resolveNameFromContent's fallback.
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The result builders of the accessible name computation.
3
+ *
4
+ * A module of their own, not part of `helpers.ts`: `aria-steps.ts` builds
5
+ * results, and `helpers.ts` runs Step 2B for an embedded control through
6
+ * `aria-steps.ts`, so keeping them in `helpers.ts` would make the two import
7
+ * each other.
8
+ *
9
+ * @module
10
+ */
11
+ import type { AccnameResult, AccnameSource } from './types.js';
12
+ /**
13
+ * Builds a result from a name, flattened; an empty name has no source.
14
+ */
15
+ export declare function makeResult(name: string, source: AccnameSource | null): AccnameResult;
16
+ /**
17
+ * Makes a flat string: whitespace runs become a single space, and the ends are trimmed.
18
+ *
19
+ * @see https://w3c.github.io/accname/#terminology
20
+ */
21
+ export declare function flattenText(text: string): string;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The result builders of the accessible name computation.
3
+ *
4
+ * A module of their own, not part of `helpers.ts`: `aria-steps.ts` builds
5
+ * results, and `helpers.ts` runs Step 2B for an embedded control through
6
+ * `aria-steps.ts`, so keeping them in `helpers.ts` would make the two import
7
+ * each other.
8
+ *
9
+ * @module
10
+ */
11
+ /**
12
+ * Builds a result from a name, flattened; an empty name has no source.
13
+ */
14
+ export function makeResult(name, source) {
15
+ const trimmed = flattenText(name);
16
+ return {
17
+ name: trimmed,
18
+ source: trimmed ? source : null,
19
+ };
20
+ }
21
+ /**
22
+ * Makes a flat string: whitespace runs become a single space, and the ends are trimmed.
23
+ *
24
+ * @see https://w3c.github.io/accname/#terminology
25
+ */
26
+ export function flattenText(text) {
27
+ return text.replaceAll(/\s+/g, ' ').trim();
28
+ }
@@ -42,6 +42,19 @@ export interface AccnameResolver {
42
42
  * in name-from-content computation (textbox, combobox, listbox, spinbutton, slider, searchbox).
43
43
  */
44
44
  isEmbeddedControl(el: AccnameElement): boolean;
45
+ /**
46
+ * Checks if an element is exposed with an explicit `none` or `presentation`
47
+ * role, so that Step 2E does not use its host language text alternative.
48
+ * When absent, no element is treated so.
49
+ */
50
+ isExposedAsPresentational?(el: AccnameElement): boolean;
51
+ /**
52
+ * Called for every element the computation reaches, before Step 2A, so that
53
+ * a caller can tell which attributes of a descendant took part in a name.
54
+ * An element that the computation does not reach (the subtree of a hidden
55
+ * element, or of one named before content) is never reported.
56
+ */
57
+ onVisit?(el: AccnameElement, visit: AccnameVisit): void;
45
58
  /**
46
59
  * Returns a pre-computed name for an element, bypassing the standard algorithm.
47
60
  * Used by ml-core for Pretender integration where framework components provide
@@ -60,6 +73,32 @@ export interface AccnameResolver {
60
73
  */
61
74
  getChildNodes?(el: AccnameElement): Iterable<AccnameNode>;
62
75
  }
76
+ /**
77
+ * The recursive entry of the computation (`computeAccessibleNameInternal`),
78
+ * handed to the steps that compute the text alternative of another node.
79
+ */
80
+ export type AccnameComputeFn = (el: AccnameElement, resolver: AccnameResolver, inLabelledbyTraversal: boolean, visited: ReadonlySet<string>) => AccnameResult;
81
+ /**
82
+ * How the computation reached an element (see `AccnameResolver.onVisit`).
83
+ */
84
+ export interface AccnameVisit {
85
+ /**
86
+ * Whether Step 2A found the element hidden, which ends the computation of
87
+ * it. Step 2A does not apply inside an `aria-labelledby` traversal whose
88
+ * directly referenced node is hidden, so no element is found hidden there.
89
+ */
90
+ readonly hidden: boolean;
91
+ /**
92
+ * Whether the element is an embedded control reached by name-from-content
93
+ * recursion (Step 2C), whose value is used and whose `aria-label` is ignored.
94
+ */
95
+ readonly embeddedControl: boolean;
96
+ /**
97
+ * Whether the element is part of an ongoing `aria-labelledby` traversal, in
98
+ * which Step 2B is not applied.
99
+ */
100
+ readonly inLabelledbyTraversal: boolean;
101
+ }
63
102
  /**
64
103
  * Identifies the source of an element's accessible name per HTML-AAM §4.1.
65
104
  * Used for diagnostic purposes (e.g., reporting which source provided the name).
@@ -2,6 +2,7 @@ import { computeAccessibleName } from './accname/compute.js';
2
2
  import { EMBEDDED_CONTROL_ROLES, escapeCSS } from './accname/helpers.js';
3
3
  import { isNativeEmbeddedControl } from '../../const/index.js';
4
4
  import { getComputedRole } from './get-computed-role.js';
5
+ import { isExposedAsPresentational } from './is-presentational.js';
5
6
  /**
6
7
  * [Implementation-specific] Reentrant guard to prevent infinite recursion when
7
8
  * getComputedRole evaluates `:aria(has name)` selectors, which call back into getAccname.
@@ -121,6 +122,11 @@ el, specs, version) {
121
122
  // Native HTML embedded controls without explicit role
122
123
  return isNativeEmbeddedControl(target);
123
124
  },
125
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
126
+ isExposedAsPresentational(target) {
127
+ // TODO: Remove cast when getComputedRole accepts AccnameElement (#3178)
128
+ return isExposedAsPresentational(getComputedRole(specs, target, version));
129
+ },
124
130
  };
125
131
  }
126
132
  /**
@@ -174,7 +174,7 @@ el, version, assumeSingleNode) {
174
174
  */
175
175
  if (
176
176
  // It doesn't been specified a valid explicit role.
177
- (explicitRole.role === null || explicitRole.errorType != null) &&
177
+ explicitRole.role === null &&
178
178
  // It is an SVG element.
179
179
  el.namespaceURI === SVG_NAMESPACE &&
180
180
  !hasSvgAccessibleNameSource(el)) {
@@ -204,10 +204,9 @@ el, version, assumeSingleNode) {
204
204
  *
205
205
  * 1. It may be focusable
206
206
  *
207
- * THIS CONDITION IS ALMOST MEANINGLESS.
208
- * Because it has already been determined that
209
- * the interactive elements can not specify the presentational role
210
- * in the previous processing that computes the permitted role (`getExplicitRole`).
207
+ * ARIA in HTML does not permit the presentational roles on most
208
+ * interactive elements, but `getExplicitRole` keeps such a role with
209
+ * `NO_PERMITTED`, as user agents expose it, so this resolves it.
211
210
  */
212
211
  mayBeFocusable(el, specs) &&
213
212
  /**
@@ -264,13 +263,13 @@ el, version, assumeSingleNode) {
264
263
  * > and an explicit non-presentational role is applied.
265
264
  */
266
265
  const { props } = ariaSpecs(specs, version);
267
- for (const attr of el.attributes) {
268
- if (props.find(p => p.name === attr.name)?.isGlobal) {
269
- return {
270
- ...implicitRole,
271
- errorType: 'GLOBAL_PROP_MUST_NOT_BE_PRESENTATIONAL',
272
- };
273
- }
266
+ // Asks the element, not its attribute list: whether `ARIA-LABEL` is `aria-label` is up to the element
267
+ // (a document of the HTML syntax folds the name, one that sets it under the name written does not).
268
+ if (props.some(p => p.isGlobal && el.hasAttribute(p.name))) {
269
+ return {
270
+ ...implicitRole,
271
+ errorType: 'GLOBAL_PROP_MUST_NOT_BE_PRESENTATIONAL',
272
+ };
274
273
  }
275
274
  return computedRole;
276
275
  }
@@ -2,6 +2,12 @@ import type { ARIAVersion, ComputedRole, MLMLSpec } from '../../types/index.js';
2
2
  /**
3
3
  * Implements the WAI-ARIA "Handling Author Errors" algorithm.
4
4
  *
5
+ * The roles that ARIA in HTML does not permit on the element are authoring
6
+ * errors that the user agent does not correct: such a role is returned with
7
+ * `errorType: 'NO_PERMITTED'` (Core-AAM §3.2), except on an element that is
8
+ * never rendered.
9
+ *
5
10
  * @see https://w3c.github.io/aria/#document-handling_author-errors
11
+ * @see https://w3c.github.io/core-aam/#mapping_conflicts
6
12
  */
7
13
  export declare function getExplicitRole(specs: MLMLSpec, el: Element, version: ARIAVersion): ComputedRole;
@@ -1,10 +1,17 @@
1
+ import { XHTML_NAMESPACE } from '../../const/index.js';
1
2
  import { getRoleSpec } from './get-role-spec.js';
2
3
  import { resolveNamespace } from '../../utils/resolve-namespace.js';
3
4
  import { getPermittedRoles } from './get-permitted-roles.js';
4
5
  /**
5
6
  * Implements the WAI-ARIA "Handling Author Errors" algorithm.
6
7
  *
8
+ * The roles that ARIA in HTML does not permit on the element are authoring
9
+ * errors that the user agent does not correct: such a role is returned with
10
+ * `errorType: 'NO_PERMITTED'` (Core-AAM §3.2), except on an element that is
11
+ * never rendered.
12
+ *
7
13
  * @see https://w3c.github.io/aria/#document-handling_author-errors
14
+ * @see https://w3c.github.io/core-aam/#mapping_conflicts
8
15
  */
9
16
  export function getExplicitRole(specs,
10
17
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
@@ -46,13 +53,6 @@ el, version) {
46
53
  error = 'ABSTRACT';
47
54
  continue;
48
55
  }
49
- /**
50
- * Whether the role is the permitted role according to ARIA in HTML.
51
- */
52
- if (!permittedRoles.some(r => r.name === roleName)) {
53
- error = 'NO_PERMITTED';
54
- continue;
55
- }
56
56
  /**
57
57
  * > Certain landmark roles require names from authors.
58
58
  * > In situations where an author has not specified names for these landmarks,
@@ -66,6 +66,35 @@ el, version) {
66
66
  error = 'INVALID_LANDMARK';
67
67
  continue;
68
68
  }
69
+ /**
70
+ * A role that ARIA in HTML does not permit on the element is an
71
+ * authoring error, but user agents still expose it, so the role is
72
+ * kept and the error is reported alongside. An element that is never
73
+ * rendered is not in the accessibility tree at all (WAI-ARIA §7.1),
74
+ * so the role is dropped there.
75
+ *
76
+ * @see https://w3c.github.io/aria/#tree_exclusion
77
+ *
78
+ * > Elements, including their descendent elements, that have host language semantics specifying that the element is not displayed, such as CSS display:none, visibility:hidden, or the HTML hidden attribute.
79
+ *
80
+ * @see https://w3c.github.io/core-aam/#mapping_conflicts
81
+ *
82
+ * > While conformance checkers might signal an error or warning when a WAI-ARIA role is used on elements with strong native semantics, user agents MUST still use the value of the semantic of the WAI-ARIA role when exposing the element to accessibility APIs.
83
+ */
84
+ if (!permittedRoles.some(r => r.name === roleName)) {
85
+ if (isNeverRendered(el, namespaceURI)) {
86
+ error = 'NO_PERMITTED';
87
+ continue;
88
+ }
89
+ return {
90
+ el,
91
+ role: {
92
+ ...spec,
93
+ isImplicit: false,
94
+ },
95
+ errorType: 'NO_PERMITTED',
96
+ };
97
+ }
69
98
  /**
70
99
  * Otherwise
71
100
  */
@@ -83,6 +112,43 @@ el, version) {
83
112
  errorType: error,
84
113
  };
85
114
  }
115
+ /**
116
+ * HTML elements that the user agent style sheet never renders (`display: none`),
117
+ * together with `input[type=hidden]` from the same rules (`isNeverRendered`).
118
+ * `area` is left out: it is rendered through its image map. The rules that
119
+ * depend on state (`[hidden]`, `noscript` with scripting) are left out too: the
120
+ * role stays on such an element, as its `hidden` attribute is a matter of the
121
+ * accessible name computation (Step 2A), not of the role.
122
+ *
123
+ * @see https://html.spec.whatwg.org/multipage/rendering.html#hidden-elements
124
+ */
125
+ const NEVER_RENDERED_HTML_ELEMENTS = new Set([
126
+ 'base',
127
+ 'basefont',
128
+ 'datalist',
129
+ 'head',
130
+ 'link',
131
+ 'meta',
132
+ 'noembed',
133
+ 'noframes',
134
+ 'param',
135
+ 'rp',
136
+ 'script',
137
+ 'style',
138
+ 'template',
139
+ 'title',
140
+ ]);
141
+ function isNeverRendered(
142
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
143
+ el, namespaceURI) {
144
+ if (namespaceURI !== XHTML_NAMESPACE) {
145
+ return false;
146
+ }
147
+ if (NEVER_RENDERED_HTML_ELEMENTS.has(el.localName)) {
148
+ return true;
149
+ }
150
+ return el.localName === 'input' && el.getAttribute('type')?.trim().toLowerCase() === 'hidden';
151
+ }
86
152
  function isLandmarkRole(role) {
87
153
  return role?.superClassRoles.some(su => su.name === 'landmark');
88
154
  }
@@ -1,4 +1,4 @@
1
- import type { ARIAVersion } from '../../types/index.js';
1
+ import type { ARIAVersion, ComputedRole } from '../../types/index.js';
2
2
  /**
3
3
  * Determines whether a given role name corresponds to a presentational role
4
4
  * (`"presentation"` or `"none"`), which indicates the element should be
@@ -8,6 +8,18 @@ import type { ARIAVersion } from '../../types/index.js';
8
8
  * @returns `true` if the role name is `"presentation"` or `"none"`, `false` otherwise
9
9
  */
10
10
  export declare function isPresentational(roleName?: string): boolean;
11
+ /**
12
+ * Determines whether an element is exposed with the `none` or `presentation`
13
+ * role that its author specified, after the Presentational Roles Conflict
14
+ * Resolution. The implicit `presentation` role of `<img alt="">` does not count:
15
+ * HTML-AAM gives that image no name by its own rule.
16
+ *
17
+ * @see https://w3c.github.io/accname/#comp_host_language_label
18
+ *
19
+ * @param computed - The computed role of the element
20
+ * @returns `true` if the computed role is an explicit presentational role
21
+ */
22
+ export declare function isExposedAsPresentational(computed: ComputedRole): boolean;
11
23
  /**
12
24
  * Determines whether a given role is transparent for ownership traversal.
13
25
  *
@@ -12,6 +12,20 @@ export function isPresentational(roleName) {
12
12
  }
13
13
  return ['presentation', 'none'].includes(roleName);
14
14
  }
15
+ /**
16
+ * Determines whether an element is exposed with the `none` or `presentation`
17
+ * role that its author specified, after the Presentational Roles Conflict
18
+ * Resolution. The implicit `presentation` role of `<img alt="">` does not count:
19
+ * HTML-AAM gives that image no name by its own rule.
20
+ *
21
+ * @see https://w3c.github.io/accname/#comp_host_language_label
22
+ *
23
+ * @param computed - The computed role of the element
24
+ * @returns `true` if the computed role is an explicit presentational role
25
+ */
26
+ export function isExposedAsPresentational(computed) {
27
+ return !!computed.role && !computed.role.isImplicit && isPresentational(computed.role.name);
28
+ }
15
29
  /**
16
30
  * Determines whether a given role is transparent for ownership traversal.
17
31
  *
@@ -329,6 +329,10 @@ export type Matches = (selector: string) => boolean;
329
329
  * The result of computing an element's ARIA role, containing the element reference,
330
330
  * the resolved role specification (or null if no role applies), and an optional
331
331
  * error type indicating issues during role computation.
332
+ *
333
+ * An error type does not mean the role was dropped: with `NO_PERMITTED` (a role
334
+ * ARIA in HTML does not permit on the element) the `role` is the one the author
335
+ * specified, as user agents still expose it.
332
336
  */
333
337
  export type ComputedRole = {
334
338
  readonly el: Element;
@@ -29,7 +29,7 @@ compiledPatterns) {
29
29
  }
30
30
  let potentialName;
31
31
  if (pattern.potentialName != null) {
32
- potentialName = pattern.potentialName.replaceAll(/\$(\d+)/g, (_, idx) => (match[Number(idx)] ?? '').toLowerCase());
32
+ potentialName = pattern.potentialName.replaceAll(/\$(\d+)/g, (_, idx) => match[Number(idx)] ?? '');
33
33
  }
34
34
  let isDuplicatable;
35
35
  if (pattern.isDuplicatable === true) {
@@ -2,7 +2,8 @@ import type { ElementSpec } from '../types/index.js';
2
2
  /**
3
3
  * Looks up an element specification by its local tag name and namespace. The tag name
4
4
  * is resolved to a namespace-qualified form (e.g., `"svg:circle"`) before searching.
5
- * Results are cached by the namespace-qualified name for subsequent lookups.
5
+ * Results are cached by the namespace-qualified name for subsequent lookups in the same
6
+ * list of specs.
6
7
  *
7
8
  * @template K - The keys of `ElementSpec` to include in the returned spec object (defaults to all keys)
8
9
  * @param specs - The array of element specifications to search
@@ -1,9 +1,16 @@
1
1
  import { resolveNamespace } from './resolve-namespace.js';
2
- const cache = new Map();
2
+ /**
3
+ * The results, for each list of specs it was searched in. A list is not told what another has:
4
+ * `permitted-contents` searches the specs of the document plus the `tag` rules of its option, which
5
+ * a lookup made in the specs of the document alone must not hide (nor the other way round). A list
6
+ * that nothing refers to any more takes its results with it.
7
+ */
8
+ const caches = new WeakMap();
3
9
  /**
4
10
  * Looks up an element specification by its local tag name and namespace. The tag name
5
11
  * is resolved to a namespace-qualified form (e.g., `"svg:circle"`) before searching.
6
- * Results are cached by the namespace-qualified name for subsequent lookups.
12
+ * Results are cached by the namespace-qualified name for subsequent lookups in the same
13
+ * list of specs.
7
14
  *
8
15
  * @template K - The keys of `ElementSpec` to include in the returned spec object (defaults to all keys)
9
16
  * @param specs - The array of element specifications to search
@@ -13,6 +20,11 @@ const cache = new Map();
13
20
  */
14
21
  export function getSpecByTagName(specs, localName, namespace) {
15
22
  const { localNameWithNS } = resolveNamespace(localName, namespace ?? undefined);
23
+ let cache = caches.get(specs);
24
+ if (!cache) {
25
+ cache = new Map();
26
+ caches.set(specs, cache);
27
+ }
16
28
  let spec = cache.get(localNameWithNS);
17
29
  if (spec !== undefined) {
18
30
  return spec;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markuplint/ml-spec",
3
- "version": "5.2.0",
3
+ "version": "5.4.0",
4
4
  "description": "Types and schema that specs of the Markup languages for markuplint",
5
5
  "repository": {
6
6
  "type": "git",
@@ -15,8 +15,8 @@
15
15
  "type": "module",
16
16
  "exports": {
17
17
  ".": {
18
- "import": "./lib/index.js",
19
- "types": "./lib/index.d.ts"
18
+ "types": "./lib/index.d.ts",
19
+ "default": "./lib/index.js"
20
20
  }
21
21
  },
22
22
  "publishConfig": {
@@ -36,14 +36,14 @@
36
36
  "schema:aria": "npx json2ts ./schemas/aria.schema.json --cwd ./schemas > ./src/types/aria.ts"
37
37
  },
38
38
  "dependencies": {
39
- "@markuplint/ml-ast": "5.1.1",
40
- "@markuplint/types": "5.2.0",
39
+ "@markuplint/ml-ast": "5.3.0",
40
+ "@markuplint/types": "5.4.0",
41
41
  "is-plain-object": "^5.1.0",
42
42
  "type-fest": "^5.10.0"
43
43
  },
44
44
  "devDependencies": {
45
- "@markuplint/test-tools": "5.2.0",
46
- "json-schema-to-typescript": "15.0.4"
45
+ "@markuplint/test-tools": "5.4.0",
46
+ "json-schema-to-typescript": "16.0.0"
47
47
  },
48
- "gitHead": "d8b2feab77c9c862513615bea6b80d2817431878"
48
+ "gitHead": "ab56aa8e12b67fbf793ddf1267aaba9fda90269d"
49
49
  }