@markuplint/ml-spec 4.10.1 → 4.18.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/ARCHITECTURE.ja.md +253 -0
- package/ARCHITECTURE.md +253 -0
- package/CHANGELOG.md +16 -2
- package/README.md +4 -186
- package/SKILL.md +116 -0
- package/docs/aria-algorithms.ja.md +651 -0
- package/docs/aria-algorithms.md +651 -0
- package/docs/html-algorithms.ja.md +469 -0
- package/docs/html-algorithms.md +469 -0
- package/docs/maintenance.ja.md +340 -0
- package/docs/maintenance.md +340 -0
- package/docs/spec-resolution.ja.md +540 -0
- package/docs/spec-resolution.md +551 -0
- package/docs/type-definitions.ja.md +561 -0
- package/docs/type-definitions.md +561 -0
- package/lib/algorithm/aria/accname-computation.d.ts +7 -0
- package/lib/algorithm/aria/accname-computation.js +7 -0
- package/lib/algorithm/aria/aria-specs.d.ts +7 -0
- package/lib/algorithm/aria/aria-specs.js +7 -0
- package/lib/algorithm/aria/get-aria.d.ts +12 -0
- package/lib/algorithm/aria/get-aria.js +12 -0
- package/lib/algorithm/aria/get-computed-aria-props.d.ts +22 -0
- package/lib/algorithm/aria/get-computed-aria-props.js +10 -0
- package/lib/algorithm/aria/get-computed-role.d.ts +12 -0
- package/lib/algorithm/aria/get-computed-role.js +12 -0
- package/lib/algorithm/aria/get-implicit-role.d.ts +18 -0
- package/lib/algorithm/aria/get-implicit-role.js +18 -0
- package/lib/algorithm/aria/get-permitted-roles.d.ts +9 -0
- package/lib/algorithm/aria/get-permitted-roles.js +9 -0
- package/lib/algorithm/aria/get-role-spec.d.ts +11 -0
- package/lib/algorithm/aria/get-role-spec.js +11 -0
- package/lib/algorithm/aria/has-required-owned-elements.d.ts +23 -0
- package/lib/algorithm/aria/has-required-owned-elements.js +23 -0
- package/lib/algorithm/aria/is-exposed.d.ts +7 -4
- package/lib/algorithm/aria/is-exposed.js +7 -4
- package/lib/algorithm/aria/is-presentational.d.ts +8 -0
- package/lib/algorithm/aria/is-presentational.js +8 -0
- package/lib/algorithm/aria/matches-context-role.d.ts +11 -0
- package/lib/algorithm/aria/matches-context-role.js +11 -0
- package/lib/algorithm/html/content-model-category-to-tag-names.d.ts +9 -0
- package/lib/algorithm/html/content-model-category-to-tag-names.js +9 -0
- package/lib/algorithm/html/get-content-model.d.ts +12 -1
- package/lib/algorithm/html/get-content-model.js +9 -0
- package/lib/algorithm/html/get-selectors-by-content-model-category.d.ts +8 -0
- package/lib/algorithm/html/get-selectors-by-content-model-category.js +8 -0
- package/lib/algorithm/html/is-nothing-content-model.d.ts +7 -0
- package/lib/algorithm/html/is-nothing-content-model.js +7 -0
- package/lib/algorithm/html/is-palpable-elements.d.ts +13 -0
- package/lib/algorithm/html/is-palpable-elements.js +13 -0
- package/lib/algorithm/html/is-void-element.d.ts +9 -0
- package/lib/algorithm/html/is-void-element.js +9 -0
- package/lib/algorithm/html/may-be-focusable.d.ts +10 -0
- package/lib/algorithm/html/may-be-focusable.js +10 -0
- package/lib/types/index.d.ts +54 -0
- package/lib/utils/aria-version.d.ts +6 -0
- package/lib/utils/aria-version.js +6 -0
- package/lib/utils/get-attr-specs-spec.d.ts +18 -0
- package/lib/utils/get-attr-specs-spec.js +18 -0
- package/lib/utils/get-attr-specs.d.ts +9 -0
- package/lib/utils/get-attr-specs.js +9 -0
- package/lib/utils/get-spec-by-tag-name.d.ts +11 -0
- package/lib/utils/get-spec-by-tag-name.js +11 -0
- package/lib/utils/get-spec.d.ts +10 -0
- package/lib/utils/get-spec.js +10 -0
- package/lib/utils/resolve-namespace.d.ts +13 -0
- package/lib/utils/resolve-namespace.js +10 -0
- package/lib/utils/schema-to-spec.d.ts +5 -2
- package/lib/utils/schema-to-spec.js +11 -6
- package/lib/utils/validate-aria-version.d.ts +7 -0
- package/lib/utils/validate-aria-version.js +7 -0
- package/package.json +7 -7
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
# Spec Resolution Pipeline
|
|
2
|
+
|
|
3
|
+
This document describes how `@markuplint/ml-spec` merges a base HTML specification with framework-specific extensions, resolves element and attribute lookups, handles ARIA versioning, and manages caching throughout the pipeline.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Overview](#overview)
|
|
8
|
+
- [Schema Merging (`schemaToSpec`)](#schema-merging-schematospec)
|
|
9
|
+
- [Element Spec Lookup](#element-spec-lookup)
|
|
10
|
+
- [Namespace Resolution](#namespace-resolution)
|
|
11
|
+
- [Attribute Spec Resolution](#attribute-spec-resolution)
|
|
12
|
+
- [ARIA Version Resolution](#aria-version-resolution)
|
|
13
|
+
- [Array Merging (`mergeArray`)](#array-merging-mergearray)
|
|
14
|
+
- [Caching Strategy](#caching-strategy)
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Overview
|
|
19
|
+
|
|
20
|
+
The spec resolution pipeline is the mechanism by which markuplint constructs a
|
|
21
|
+
single, unified specification from multiple sources. The overall flow is:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
@markuplint/html-spec (MLMLSpec)
|
|
25
|
+
+
|
|
26
|
+
framework specs (ExtendedSpec[]) e.g., vue-spec, react-spec, svelte-spec
|
|
27
|
+
|
|
|
28
|
+
v
|
|
29
|
+
schemaToSpec()
|
|
30
|
+
|
|
|
31
|
+
v
|
|
32
|
+
merged MLMLSpec (used by all downstream algorithms)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
A user's configuration file may reference one or more framework plugins. Each
|
|
36
|
+
plugin provides an `ExtendedSpec` that adds, overrides, or extends the base
|
|
37
|
+
HTML specification with framework-specific elements, attributes, ARIA
|
|
38
|
+
definitions, and content model categories.
|
|
39
|
+
|
|
40
|
+
All downstream APIs -- element lookup, attribute resolution, ARIA queries,
|
|
41
|
+
content model checks -- operate on the merged `MLMLSpec` returned by
|
|
42
|
+
`schemaToSpec`. This design means that resolution logic does not need to know
|
|
43
|
+
whether a particular definition came from the base spec or an extension.
|
|
44
|
+
|
|
45
|
+
### Key Types
|
|
46
|
+
|
|
47
|
+
| Type | Role |
|
|
48
|
+
| -------------- | ---------------------------------------------------------------------------------------------------- |
|
|
49
|
+
| `MLMLSpec` | The full specification object: cites, defs (globalAttrs, aria, contentModels), and per-element specs |
|
|
50
|
+
| `ExtendedSpec` | A partial overlay that may contribute to any section of `MLMLSpec` |
|
|
51
|
+
| `ElementSpec` | Per-element definition: name, categories, attributes, globalAttrs, aria, contentModel |
|
|
52
|
+
| `Attribute` | Single attribute definition: name, type, description, and other metadata |
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Schema Merging (`schemaToSpec`)
|
|
57
|
+
|
|
58
|
+
**File:** `src/utils/schema-to-spec.ts`
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
function schemaToSpec(schemas: readonly [MLMLSpec, ...ExtendedSpec[]]): MLMLSpec;
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The function takes a tuple whose first element is always the base `MLMLSpec`
|
|
65
|
+
(typically `@markuplint/html-spec`) and whose remaining elements are zero or
|
|
66
|
+
more `ExtendedSpec` objects. It returns a new `MLMLSpec` containing the merged
|
|
67
|
+
result.
|
|
68
|
+
|
|
69
|
+
### Step-by-Step Merge Process
|
|
70
|
+
|
|
71
|
+
The merge is performed iteratively. Each `ExtendedSpec` in order is folded into
|
|
72
|
+
the accumulating result:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
const [main, ...extendedSpecs] = schemas;
|
|
76
|
+
const result = { ...main };
|
|
77
|
+
|
|
78
|
+
for (const extendedSpec of extendedSpecs) {
|
|
79
|
+
// merge each section...
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
#### 1. Cites
|
|
84
|
+
|
|
85
|
+
If the extended spec provides `cites`, they are concatenated onto the existing
|
|
86
|
+
array:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
result.cites = [...result.cites, ...extendedSpec.cites];
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
This is simple array concatenation -- no deduplication is performed because
|
|
93
|
+
citation sources are expected to be distinct across specs.
|
|
94
|
+
|
|
95
|
+
#### 2. Global Attributes
|
|
96
|
+
|
|
97
|
+
The extended spec's `#extends` property is spread into the base spec's
|
|
98
|
+
`#HTMLGlobalAttrs` category:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
gAttrs['#HTMLGlobalAttrs'] = {
|
|
102
|
+
...def['#globalAttrs']?.['#HTMLGlobalAttrs'],
|
|
103
|
+
...extendedSpec.def['#globalAttrs']?.['#extends'],
|
|
104
|
+
};
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
This means a framework can introduce new global attributes (e.g., Vue's `v-if`,
|
|
108
|
+
`v-for`) and they become part of `#HTMLGlobalAttrs` for all elements that
|
|
109
|
+
reference that category. Existing attributes with the same key are overridden by
|
|
110
|
+
the extension.
|
|
111
|
+
|
|
112
|
+
#### 3. ARIA Definitions
|
|
113
|
+
|
|
114
|
+
For each ARIA version (`1.1`, `1.2`, `1.3`), the three arrays -- `roles`,
|
|
115
|
+
`props`, and `graphicsRoles` -- are merged using `mergeArray`:
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
def['#aria'] = {
|
|
119
|
+
'1.1': {
|
|
120
|
+
roles: mergeArray(def['#aria']['1.1'].roles, extendedSpec.def['#aria']['1.1'].roles),
|
|
121
|
+
props: mergeArray(def['#aria']['1.1'].props, extendedSpec.def['#aria']['1.1'].props),
|
|
122
|
+
graphicsRoles: mergeArray(def['#aria']['1.1'].graphicsRoles, extendedSpec.def['#aria']['1.1'].graphicsRoles),
|
|
123
|
+
},
|
|
124
|
+
// same for 1.2 and 1.3
|
|
125
|
+
};
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`mergeArray` uses name-based matching (see [Array Merging](#array-merging-mergearray)),
|
|
129
|
+
so an extension can both add new ARIA roles/properties and override existing
|
|
130
|
+
definitions.
|
|
131
|
+
|
|
132
|
+
#### 4. Content Models
|
|
133
|
+
|
|
134
|
+
All content model categories are unioned. For each category key across both the
|
|
135
|
+
base and extension, the selector arrays are concatenated:
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
const keys = new Set([...Object.keys(def['#contentModels']), ...Object.keys(extendedSpec.def['#contentModels'])]);
|
|
139
|
+
|
|
140
|
+
for (const modelName of keys) {
|
|
141
|
+
models[modelName] = [...(mainModel ?? []), ...(exModel ?? [])];
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
This means a framework can add its custom elements to existing categories
|
|
146
|
+
(e.g., adding `<router-link>` to `#phrasing`) or define entirely new
|
|
147
|
+
categories.
|
|
148
|
+
|
|
149
|
+
#### 5. Element Specs
|
|
150
|
+
|
|
151
|
+
Elements are matched by name (case-insensitive comparison). For each element in
|
|
152
|
+
the base spec:
|
|
153
|
+
|
|
154
|
+
- If no matching extension element exists, the base element is kept as-is.
|
|
155
|
+
- If a match is found, the specs are merged:
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
specs.push({
|
|
159
|
+
...elSpec, // base element spread
|
|
160
|
+
...exSpec, // extension overrides top-level properties
|
|
161
|
+
globalAttrs: {
|
|
162
|
+
...elSpec.globalAttrs,
|
|
163
|
+
...exSpec?.globalAttrs,
|
|
164
|
+
},
|
|
165
|
+
attributes: mergeAttrSpec(elSpec.attributes, exSpec?.attributes),
|
|
166
|
+
categories: mergeArray(elSpec.categories, exSpec?.categories),
|
|
167
|
+
});
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
The helper `mergeAttrSpec` unions all attribute keys and spreads the extension
|
|
171
|
+
attribute onto the base attribute for each key, allowing partial overrides of
|
|
172
|
+
individual attribute definitions.
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Element Spec Lookup
|
|
177
|
+
|
|
178
|
+
**Files:** `src/utils/get-spec.ts`, `src/utils/get-spec-by-tag-name.ts`
|
|
179
|
+
|
|
180
|
+
The element lookup API has two layers:
|
|
181
|
+
|
|
182
|
+
### DOM Wrapper: `getSpec`
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
function getSpec<K extends keyof ElementSpec>(
|
|
186
|
+
el: Element,
|
|
187
|
+
specs: readonly Pick<ElementSpec, 'name' | K>[],
|
|
188
|
+
): Pick<ElementSpec, 'name' | K> | null;
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
A convenience wrapper that extracts `el.localName` and `el.namespaceURI` from a
|
|
192
|
+
DOM `Element` and delegates to `getSpecByTagName`.
|
|
193
|
+
|
|
194
|
+
### Core Lookup: `getSpecByTagName`
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
function getSpecByTagName<K extends keyof ElementSpec>(
|
|
198
|
+
specs: readonly Pick<ElementSpec, 'name' | K>[],
|
|
199
|
+
localName: string,
|
|
200
|
+
namespace: string | null,
|
|
201
|
+
): Pick<ElementSpec, 'name' | K> | null;
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Steps:
|
|
205
|
+
|
|
206
|
+
1. Call `resolveNamespace(localName, namespace)` to get the namespace-qualified
|
|
207
|
+
name (e.g., `"svg:circle"` for an SVG circle element, or `"div"` for an HTML div).
|
|
208
|
+
2. Check the module-level `Map<string, ElementSpec | null>` cache using the
|
|
209
|
+
qualified name as key.
|
|
210
|
+
3. If not cached, perform a linear search through `specs` matching by `name`.
|
|
211
|
+
4. Store the result (including `null` for miss) in the cache and return.
|
|
212
|
+
|
|
213
|
+
The generic parameter `K` allows callers to request only specific keys from
|
|
214
|
+
`ElementSpec`, reducing the amount of data carried through the type system while
|
|
215
|
+
still preserving type safety.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Namespace Resolution
|
|
220
|
+
|
|
221
|
+
**Files:** `src/utils/resolve-namespace.ts`, `src/utils/get-ns.ts`
|
|
222
|
+
|
|
223
|
+
### `resolveNamespace`
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
function resolveNamespace(
|
|
227
|
+
name: string,
|
|
228
|
+
namespaceURI: string | null = 'http://www.w3.org/1999/xhtml',
|
|
229
|
+
): NamespacedElementName;
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Resolves an element name and optional namespace URI into a fully normalized
|
|
233
|
+
form:
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
type NamespacedElementName = {
|
|
237
|
+
localNameWithNS: string; // e.g., "svg:circle" or "div"
|
|
238
|
+
localName: string; // e.g., "circle" or "div"
|
|
239
|
+
namespace: Namespace; // "html" | "svg" | "mml" | "xlink"
|
|
240
|
+
namespaceURI: NamespaceURI; // full URI string
|
|
241
|
+
};
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Resolution logic:
|
|
245
|
+
|
|
246
|
+
1. **Split on colon** -- If `name` contains a colon (e.g., `"svg:circle"`), the
|
|
247
|
+
prefix is treated as an explicit namespace hint and the suffix as the local
|
|
248
|
+
name.
|
|
249
|
+
2. **Determine namespace** -- The namespace is resolved from the explicit prefix
|
|
250
|
+
or by calling `getNS(namespaceURI)`. If neither yields a recognized
|
|
251
|
+
namespace, it defaults to `'html'`.
|
|
252
|
+
3. **Build qualified name** -- For HTML namespace, the qualified name is just the
|
|
253
|
+
local name (no prefix). For all other namespaces, the shorthand is prepended:
|
|
254
|
+
`"svg:circle"`, `"mml:math"`, etc.
|
|
255
|
+
4. **Cache** -- Results are cached in a `Map<string, NamespacedElementName>`
|
|
256
|
+
keyed by the concatenation `name + namespaceURI`.
|
|
257
|
+
|
|
258
|
+
### Namespace URI Mapping
|
|
259
|
+
|
|
260
|
+
| Namespace URI | Shorthand |
|
|
261
|
+
| ------------------------------------ | --------- |
|
|
262
|
+
| `http://www.w3.org/1999/xhtml` | `html` |
|
|
263
|
+
| `http://www.w3.org/2000/svg` | `svg` |
|
|
264
|
+
| `http://www.w3.org/1998/Math/MathML` | `mml` |
|
|
265
|
+
| `http://www.w3.org/1999/xlink` | `xlink` |
|
|
266
|
+
|
|
267
|
+
### `getNS` Helper
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
function getNS(namespaceURI: string | null): Namespace;
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
A simple switch-case that maps a namespace URI string to its shorthand. Any
|
|
274
|
+
unrecognized URI (including `null`) returns `'html'`.
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Attribute Spec Resolution
|
|
279
|
+
|
|
280
|
+
**Files:** `src/utils/get-attr-specs.ts` (DOM wrapper), `src/utils/get-attr-specs-spec.ts` (core)
|
|
281
|
+
|
|
282
|
+
### DOM Wrapper: `getAttrSpecs` (from `get-attr-specs.ts`)
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
function getAttrSpecs(el: Element, schema: MLMLSpec): readonly Attribute[] | null;
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Extracts `el.localName` and `el.namespaceURI`, then delegates to the core
|
|
289
|
+
function.
|
|
290
|
+
|
|
291
|
+
### Core Function: `getAttrSpecs` (from `get-attr-specs-spec.ts`)
|
|
292
|
+
|
|
293
|
+
```ts
|
|
294
|
+
function getAttrSpecs(localName: string, namespace: NamespaceURI | null, schema: MLMLSpec): readonly Attribute[] | null;
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Resolution steps:
|
|
298
|
+
|
|
299
|
+
1. **Schema invalidation** -- If the `schema` reference has changed (checked via
|
|
300
|
+
a `WeakSet<MLMLSpec>`), the entire attribute cache is cleared. This ensures
|
|
301
|
+
correctness when specs are re-merged.
|
|
302
|
+
|
|
303
|
+
2. **Cache check** -- Look up the namespace-qualified name in
|
|
304
|
+
`Map<string, readonly Attribute[] | null>`.
|
|
305
|
+
|
|
306
|
+
3. **Find element spec** -- Search `schema.specs` for a matching element by
|
|
307
|
+
namespace-qualified name. Return `null` (and cache it) if not found.
|
|
308
|
+
|
|
309
|
+
4. **Collect global attributes** -- Iterate over the element's `globalAttrs`
|
|
310
|
+
selection map. For each category:
|
|
311
|
+
- `false` -- skip the category entirely
|
|
312
|
+
- `true` -- include all attributes from that global category
|
|
313
|
+
- `string[]` -- include only the named attributes from that category
|
|
314
|
+
|
|
315
|
+
```ts
|
|
316
|
+
for (const catName in elSpec.globalAttrs) {
|
|
317
|
+
const catAttrs = elSpec.globalAttrs[catName];
|
|
318
|
+
if (catAttrs === false) continue;
|
|
319
|
+
if (typeof catAttrs === 'boolean') {
|
|
320
|
+
attrs = { ...attrs, ...global };
|
|
321
|
+
}
|
|
322
|
+
if (Array.isArray(catAttrs)) {
|
|
323
|
+
for (const selectedName of catAttrs) {
|
|
324
|
+
attrs[selectedName] = { ...attrs[selectedName], ...global[selectedName] };
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
5. **Merge element-specific attributes** -- The element's own `attributes` are
|
|
331
|
+
spread on top of the collected globals, so element-specific definitions
|
|
332
|
+
override globals:
|
|
333
|
+
|
|
334
|
+
```ts
|
|
335
|
+
attrs[attrName] = {
|
|
336
|
+
description: '',
|
|
337
|
+
...current, // from globals
|
|
338
|
+
...attr, // from element spec
|
|
339
|
+
};
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
6. **Convert to sorted array** -- The attribute map is converted to an
|
|
343
|
+
`Attribute[]`, giving each entry a default `type: 'Any'` if none was
|
|
344
|
+
provided, then sorted alphabetically (case-insensitive) using `nameCompare`.
|
|
345
|
+
|
|
346
|
+
7. **Cache and return** -- The sorted array is stored in the cache and returned.
|
|
347
|
+
|
|
348
|
+
### `nameCompare`
|
|
349
|
+
|
|
350
|
+
```ts
|
|
351
|
+
function nameCompare(a: HasName | string, b: HasName | string): number;
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
Case-insensitive sort comparator. Extracts the `name` property (or uses the
|
|
355
|
+
string directly), converts to uppercase, and performs standard lexicographic
|
|
356
|
+
comparison.
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
## ARIA Version Resolution
|
|
361
|
+
|
|
362
|
+
**Files:** `src/utils/resolve-version.ts`, `src/utils/aria-version.ts`,
|
|
363
|
+
`src/utils/validate-aria-version.ts`, `src/algorithm/aria/get-aria.ts`
|
|
364
|
+
|
|
365
|
+
### `resolveVersion`
|
|
366
|
+
|
|
367
|
+
```ts
|
|
368
|
+
function resolveVersion(aria: ReadonlyDeep<ARIA>, version: ARIAVersion): Omit<ReadonlyDeep<ARIA>, ARIAVersion>;
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
Extracts a version-specific ARIA definition from a multi-version `ARIA` object.
|
|
372
|
+
For each property, the version-specific value is used if present; otherwise the
|
|
373
|
+
base (version-agnostic) value applies:
|
|
374
|
+
|
|
375
|
+
```ts
|
|
376
|
+
const implicitRole = aria[version]?.implicitRole ?? aria.implicitRole;
|
|
377
|
+
const permittedRoles = aria[version]?.permittedRoles ?? aria.permittedRoles;
|
|
378
|
+
// ...etc
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
Special case: `namingProhibited` for version `'1.1'` always uses the base
|
|
382
|
+
value, because the naming prohibition concept was not formalized until ARIA 1.2:
|
|
383
|
+
|
|
384
|
+
```ts
|
|
385
|
+
const namingProhibited =
|
|
386
|
+
version === '1.1' ? aria.namingProhibited : (aria[version]?.namingProhibited ?? aria.namingProhibited);
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
The returned object contains only the resolved properties -- the version keys
|
|
390
|
+
(`'1.1'`, `'1.2'`, `'1.3'`) are stripped from the type.
|
|
391
|
+
|
|
392
|
+
### `getARIA`
|
|
393
|
+
|
|
394
|
+
```ts
|
|
395
|
+
function getARIA(
|
|
396
|
+
specs: MLMLSpec,
|
|
397
|
+
localName: string,
|
|
398
|
+
namespace: string | null,
|
|
399
|
+
version: ARIAVersion,
|
|
400
|
+
matches: Matches,
|
|
401
|
+
): Omit<ReadonlyDeep<ARIA>, ARIAVersion | 'conditions'> | null;
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
The high-level ARIA resolver. It:
|
|
405
|
+
|
|
406
|
+
1. Calls `getVersionResolvedARIA` (internal) to get the version-resolved spec.
|
|
407
|
+
2. If the spec has `conditions` (CSS-selector-keyed overrides), iterates through
|
|
408
|
+
them and applies the first matching condition's properties. This handles
|
|
409
|
+
cases like `<input type="checkbox">` having different ARIA semantics than
|
|
410
|
+
`<input type="text">`.
|
|
411
|
+
3. Optimizes `permittedRoles` -- if both `"presentation"` and `"none"` are in
|
|
412
|
+
the list, ensures both synonyms are present (per WAI-ARIA 1.2 note).
|
|
413
|
+
|
|
414
|
+
The internal `getVersionResolvedARIA` function caches results in a
|
|
415
|
+
`Map<string, ARIA | null>` keyed by `localName + namespace + version`.
|
|
416
|
+
|
|
417
|
+
### Version Constants and Validation
|
|
418
|
+
|
|
419
|
+
```ts
|
|
420
|
+
// aria-version.ts
|
|
421
|
+
const ariaVersions = ['1.1', '1.2', '1.3'] as const;
|
|
422
|
+
const ARIA_RECOMMENDED_VERSION = '1.2';
|
|
423
|
+
|
|
424
|
+
// validate-aria-version.ts
|
|
425
|
+
function validateAriaVersion(version: string): version is ARIAVersion;
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
`validateAriaVersion` is a type guard that checks whether a string is a member
|
|
429
|
+
of the `ariaVersions` tuple. It is used at configuration boundaries to validate
|
|
430
|
+
user-supplied version strings before they enter the typed pipeline.
|
|
431
|
+
|
|
432
|
+
---
|
|
433
|
+
|
|
434
|
+
## Array Merging (`mergeArray`)
|
|
435
|
+
|
|
436
|
+
**File:** `src/utils/merge-array.ts`
|
|
437
|
+
|
|
438
|
+
```ts
|
|
439
|
+
function mergeArray<T extends NamedDefinition>(a: readonly T[], b: readonly T[] | null | undefined): readonly T[];
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
Where `NamedDefinition = string | { readonly name: string }`.
|
|
443
|
+
|
|
444
|
+
This is the core merge utility used throughout the spec merging pipeline. It
|
|
445
|
+
performs **name-based merging** rather than simple concatenation:
|
|
446
|
+
|
|
447
|
+
### Algorithm
|
|
448
|
+
|
|
449
|
+
1. If `b` is `null` or `undefined`, return `a` unchanged.
|
|
450
|
+
2. Start with a copy of `a`.
|
|
451
|
+
3. For each item in `b`:
|
|
452
|
+
- Extract the name using `getName()` (case-insensitive, trimmed).
|
|
453
|
+
- Search for an item with the same name in the result.
|
|
454
|
+
- **No match found:** append the extension item.
|
|
455
|
+
- **Match found (both are strings):** the string is a simple identifier with
|
|
456
|
+
no additional data, so the base item is kept (the splice removes it, then
|
|
457
|
+
the loop continues without pushing a replacement, effectively keeping the
|
|
458
|
+
base version).
|
|
459
|
+
- **Match found (base is string, extension is object):** replace with the
|
|
460
|
+
richer object form from the extension.
|
|
461
|
+
- **Match found (both are objects):** spread-merge the two objects, with the
|
|
462
|
+
extension's properties taking precedence:
|
|
463
|
+
```ts
|
|
464
|
+
const exItem = { ...aItem, ...bItem };
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
### `getName` Helper
|
|
468
|
+
|
|
469
|
+
```ts
|
|
470
|
+
function getName(def: NamedDefinition): string {
|
|
471
|
+
const result = typeof def === 'string' ? def : def.name;
|
|
472
|
+
return result.toLowerCase().trim();
|
|
473
|
+
}
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
Names are normalized to lowercase and trimmed before comparison, ensuring
|
|
477
|
+
case-insensitive matching.
|
|
478
|
+
|
|
479
|
+
---
|
|
480
|
+
|
|
481
|
+
## Caching Strategy
|
|
482
|
+
|
|
483
|
+
The package uses multiple cache layers to avoid redundant computation. Since
|
|
484
|
+
specs are typically loaded once and reused for the lifetime of a lint run, these
|
|
485
|
+
caches provide significant performance benefits.
|
|
486
|
+
|
|
487
|
+
### Cache Inventory
|
|
488
|
+
|
|
489
|
+
| # | Location | Cache Type | Key | Value | Invalidation |
|
|
490
|
+
| --- | ----------------------------------------------- | ----------------------------------------------------------------- | ---------------------------------------------------------- | ---------------------------------------------- | --------------------------------------------------------- |
|
|
491
|
+
| 1 | `getSpecByTagName` | `Map<string, any>` | Namespace-qualified name (e.g., `"svg:circle"`) | `ElementSpec \| null` | Module lifetime (never cleared) |
|
|
492
|
+
| 2 | `getVersionResolvedARIA` (inside `get-aria.ts`) | `Map<string, ARIA \| null>` | `localName + namespace + version` (string concatenation) | Version-resolved ARIA spec or null | Module lifetime (never cleared) |
|
|
493
|
+
| 3 | `getContentModel` | `Map<Specs, Map<Element, ...>>` | Outer: specs array reference; Inner: DOM Element reference | `PermittedContentPattern[] \| boolean \| null` | Outer map entry created per unique specs reference |
|
|
494
|
+
| 4 | `contentModelCategoryToTagNames` | `Map<Category, ReadonlyArray<string>>` | Category string (e.g., `"#flow"`) | Frozen sorted array of tag names | Module lifetime (never cleared) |
|
|
495
|
+
| 5 | `resolveNamespace` | `Map<string, NamespacedElementName>` | `name + namespaceURI` (string concatenation) | Resolved namespace object | Module lifetime (never cleared) |
|
|
496
|
+
| 6 | `getAttrSpecs` (in `get-attr-specs-spec.ts`) | `Map<string, readonly Attribute[] \| null>` + `WeakSet<MLMLSpec>` | Namespace-qualified name | Sorted attribute array or null | Cleared when schema reference changes (via WeakSet check) |
|
|
497
|
+
|
|
498
|
+
### Cache Characteristics
|
|
499
|
+
|
|
500
|
+
**No explicit eviction:** Most caches are module-level `Map` instances that
|
|
501
|
+
persist for the entire process lifetime. This is appropriate because:
|
|
502
|
+
|
|
503
|
+
- Spec data is immutable after merging.
|
|
504
|
+
- The number of unique elements/attributes is bounded (HTML has ~120 elements).
|
|
505
|
+
- A lint run typically processes one configuration.
|
|
506
|
+
|
|
507
|
+
**Schema-aware invalidation:** The `getAttrSpecs` cache (item 6) is the
|
|
508
|
+
exception. It uses a `WeakSet<MLMLSpec>` to detect when the schema reference
|
|
509
|
+
changes. If a new schema is passed (e.g., when running different file patterns
|
|
510
|
+
with different framework specs), the entire `cacheMap` is cleared before
|
|
511
|
+
proceeding:
|
|
512
|
+
|
|
513
|
+
```ts
|
|
514
|
+
if (!schemaCache.has(schema)) {
|
|
515
|
+
cacheMap.clear();
|
|
516
|
+
}
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
**Nested caching:** The `getContentModel` cache (item 3) uses a two-level
|
|
520
|
+
`Map<Specs, Map<Element, ...>>` structure. The outer map is keyed by the specs
|
|
521
|
+
array reference, so different spec configurations maintain separate caches. The
|
|
522
|
+
inner map is keyed by DOM Element reference, so re-querying the same element
|
|
523
|
+
within the same spec context is O(1).
|
|
524
|
+
|
|
525
|
+
**Deterministic keys:** Caches in items 1, 2, and 5 use string concatenation
|
|
526
|
+
for keys. Since `resolveNamespace` produces deterministic output for the same
|
|
527
|
+
inputs, and spec data does not mutate, these concatenated keys are stable.
|
|
528
|
+
|
|
529
|
+
### Data Flow with Caching
|
|
530
|
+
|
|
531
|
+
```
|
|
532
|
+
schemaToSpec() --> merged MLMLSpec (no cache; called once at startup)
|
|
533
|
+
|
|
|
534
|
+
+-----------+-----------+
|
|
535
|
+
| | |
|
|
536
|
+
getSpecByTagName getAttrSpecs getARIA
|
|
537
|
+
(cache 1) (cache 6) (cache 2)
|
|
538
|
+
| |
|
|
539
|
+
resolveNamespace resolveVersion
|
|
540
|
+
(cache 5) (pure; no cache)
|
|
541
|
+
|
|
|
542
|
+
getContentModel
|
|
543
|
+
(cache 3)
|
|
544
|
+
|
|
|
545
|
+
contentModelCategoryToTagNames
|
|
546
|
+
(cache 4)
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
Each arrow represents a function call. Caches intercept repeated calls at each
|
|
550
|
+
layer, so a second lookup for the same element hits cached results at every
|
|
551
|
+
level of the call chain.
|