@markuplint/ml-spec 5.0.0-rc.4 → 5.0.0-rc.5

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.
Files changed (41) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +0 -8
  3. package/lib/algorithm/aria/accname/aria-steps.d.ts +0 -24
  4. package/lib/algorithm/aria/accname/aria-steps.js +0 -24
  5. package/lib/algorithm/aria/accname/compute.d.ts +0 -10
  6. package/lib/algorithm/aria/accname/compute.js +0 -10
  7. package/lib/algorithm/aria/accname/element-names.d.ts +0 -23
  8. package/lib/algorithm/aria/accname/element-names.js +0 -23
  9. package/lib/algorithm/aria/accname/helpers.d.ts +2 -64
  10. package/lib/algorithm/aria/accname/helpers.js +2 -72
  11. package/lib/algorithm/aria/accname/label-steps.d.ts +2 -18
  12. package/lib/algorithm/aria/accname/label-steps.js +5 -21
  13. package/lib/algorithm/aria/accname/types.d.ts +0 -3
  14. package/lib/algorithm/aria/get-explicit-role.d.ts +1 -8
  15. package/lib/algorithm/aria/get-explicit-role.js +1 -8
  16. package/lib/algorithm/aria/get-non-presentational-ancestor.d.ts +2 -10
  17. package/lib/algorithm/aria/get-non-presentational-ancestor.js +2 -10
  18. package/lib/algorithm/aria/get-permitted-roles-spec.js +1 -0
  19. package/lib/algorithm/aria/matches-context-role.d.ts +3 -10
  20. package/lib/algorithm/aria/matches-context-role.js +3 -10
  21. package/lib/algorithm/html/content-model-category-to-tag-names.d.ts +5 -0
  22. package/lib/algorithm/html/content-model-category-to-tag-names.js +5 -0
  23. package/lib/index.d.ts +28 -0
  24. package/lib/index.js +28 -0
  25. package/lib/types/index.d.ts +35 -4
  26. package/lib/utils/schema-to-spec.d.ts +10 -0
  27. package/lib/utils/schema-to-spec.js +10 -0
  28. package/package.json +6 -6
  29. package/ARCHITECTURE.ja.md +0 -267
  30. package/ARCHITECTURE.md +0 -267
  31. package/SKILL.md +0 -116
  32. package/docs/aria-algorithms.ja.md +0 -802
  33. package/docs/aria-algorithms.md +0 -804
  34. package/docs/html-algorithms.ja.md +0 -469
  35. package/docs/html-algorithms.md +0 -469
  36. package/docs/maintenance.ja.md +0 -359
  37. package/docs/maintenance.md +0 -359
  38. package/docs/spec-resolution.ja.md +0 -575
  39. package/docs/spec-resolution.md +0 -588
  40. package/docs/type-definitions.ja.md +0 -584
  41. package/docs/type-definitions.md +0 -584
@@ -1,588 +0,0 @@
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 four arrays -- `roles`,
115
- `props`, `graphicsRoles`, and `dpubRoles` -- 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
- dpubRoles: mergeArray(def['#aria']['1.1'].dpubRoles, extendedSpec.def['#aria']['1.1'].dpubRoles),
124
- },
125
- // same for 1.2 and 1.3
126
- };
127
- ```
128
-
129
- `mergeArray` uses name-based matching (see [Array Merging](#array-merging-mergearray)),
130
- so an extension can both add new ARIA roles/properties and override existing
131
- definitions.
132
-
133
- #### 4. Content Models
134
-
135
- All content model categories are unioned. For each category key across both the
136
- base and extension, the selector arrays are concatenated:
137
-
138
- ```ts
139
- const keys = new Set([...Object.keys(def['#contentModels']), ...Object.keys(extendedSpec.def['#contentModels'])]);
140
-
141
- for (const modelName of keys) {
142
- models[modelName] = [...(mainModel ?? []), ...(exModel ?? [])];
143
- }
144
- ```
145
-
146
- This means a framework can add its custom elements to existing categories
147
- (e.g., adding `<router-link>` to `#phrasing`) or define entirely new
148
- categories.
149
-
150
- #### 5. Directive Patterns
151
-
152
- If the extended spec provides `directivePatterns`, they are concatenated onto the
153
- existing array:
154
-
155
- ```ts
156
- result.directivePatterns = [...(result.directivePatterns ?? []), ...extendedSpec.directivePatterns];
157
- ```
158
-
159
- This is simple array concatenation. The core engine evaluates patterns in order
160
- (first match wins), so framework specs should define their patterns from most
161
- specific to most general.
162
-
163
- #### 6. `acceptedAttrNames`
164
-
165
- If the extended spec explicitly sets `acceptedAttrNames`, it overrides the
166
- current value:
167
-
168
- ```ts
169
- if (extendedSpec.acceptedAttrNames != null) {
170
- result.acceptedAttrNames = extendedSpec.acceptedAttrNames;
171
- }
172
- ```
173
-
174
- This is a last-write-wins semantic with explicit `null` guard -- omitting the
175
- property does not reset a previously set value. The value controls how
176
- `@markuplint/ml-core`'s `MLAttr` constructor resolves attribute names:
177
-
178
- - `'idl'` -- Enables IDL-to-content attribute name resolution (e.g.,
179
- `className` -> `class`) and suggests IDL names as candidates when the
180
- content attribute name is used (e.g., `tabindex` -> "Did you mean
181
- `tabIndex`?"). Used by React, where only IDL property names are accepted.
182
- - `'both'` -- Enables IDL-to-content attribute name resolution but does
183
- **not** suggest IDL names as candidates. Both content attribute names
184
- (e.g., `class`) and IDL property names (e.g., `className`) are accepted
185
- without warnings. Used by Svelte.
186
-
187
- #### 7. Element Specs
188
-
189
- Elements are matched by name (case-insensitive comparison). For each element in
190
- the base spec:
191
-
192
- - If no matching extension element exists, the base element is kept as-is.
193
- - If a match is found, the specs are merged:
194
-
195
- ```ts
196
- specs.push({
197
- ...elSpec, // base element spread
198
- ...exSpec, // extension overrides top-level properties
199
- globalAttrs: {
200
- ...elSpec.globalAttrs,
201
- ...exSpec?.globalAttrs,
202
- },
203
- attributes: mergeAttrSpec(elSpec.attributes, exSpec?.attributes),
204
- categories: mergeArray(elSpec.categories, exSpec?.categories),
205
- });
206
- ```
207
-
208
- The helper `mergeAttrSpec` unions all attribute keys and spreads the extension
209
- attribute onto the base attribute for each key, allowing partial overrides of
210
- individual attribute definitions.
211
-
212
- ---
213
-
214
- ## Element Spec Lookup
215
-
216
- **Files:** `src/utils/get-spec.ts`, `src/utils/get-spec-by-tag-name.ts`
217
-
218
- The element lookup API has two layers:
219
-
220
- ### DOM Wrapper: `getSpec`
221
-
222
- ```ts
223
- function getSpec<K extends keyof ElementSpec>(
224
- el: Element,
225
- specs: readonly Pick<ElementSpec, 'name' | K>[],
226
- ): Pick<ElementSpec, 'name' | K> | null;
227
- ```
228
-
229
- A convenience wrapper that extracts `el.localName` and `el.namespaceURI` from a
230
- DOM `Element` and delegates to `getSpecByTagName`.
231
-
232
- ### Core Lookup: `getSpecByTagName`
233
-
234
- ```ts
235
- function getSpecByTagName<K extends keyof ElementSpec>(
236
- specs: readonly Pick<ElementSpec, 'name' | K>[],
237
- localName: string,
238
- namespace: string | null,
239
- ): Pick<ElementSpec, 'name' | K> | null;
240
- ```
241
-
242
- Steps:
243
-
244
- 1. Call `resolveNamespace(localName, namespace)` to get the namespace-qualified
245
- name (e.g., `"svg:circle"` for an SVG circle element, or `"div"` for an HTML div).
246
- 2. Check the module-level `Map<string, ElementSpec | null>` cache using the
247
- qualified name as key.
248
- 3. If not cached, perform a linear search through `specs` matching by `name`.
249
- 4. Store the result (including `null` for miss) in the cache and return.
250
-
251
- The generic parameter `K` allows callers to request only specific keys from
252
- `ElementSpec`, reducing the amount of data carried through the type system while
253
- still preserving type safety.
254
-
255
- ---
256
-
257
- ## Namespace Resolution
258
-
259
- **Files:** `src/utils/resolve-namespace.ts`, `src/utils/get-ns.ts`
260
-
261
- ### `resolveNamespace`
262
-
263
- ```ts
264
- function resolveNamespace(
265
- name: string,
266
- namespaceURI: string | null = 'http://www.w3.org/1999/xhtml',
267
- ): NamespacedElementName;
268
- ```
269
-
270
- Resolves an element name and optional namespace URI into a fully normalized
271
- form:
272
-
273
- ```ts
274
- type NamespacedElementName = {
275
- localNameWithNS: string; // e.g., "svg:circle" or "div"
276
- localName: string; // e.g., "circle" or "div"
277
- namespace: Namespace; // "html" | "svg" | "mml" | "xlink"
278
- namespaceURI: NamespaceURI; // full URI string
279
- };
280
- ```
281
-
282
- Resolution logic:
283
-
284
- 1. **Split on colon** -- If `name` contains a colon (e.g., `"svg:circle"`), the
285
- prefix is treated as an explicit namespace hint and the suffix as the local
286
- name.
287
- 2. **Determine namespace** -- The namespace is resolved from the explicit prefix
288
- or by calling `getNS(namespaceURI)`. If neither yields a recognized
289
- namespace, it defaults to `'html'`.
290
- 3. **Build qualified name** -- For HTML namespace, the qualified name is just the
291
- local name (no prefix). For all other namespaces, the shorthand is prepended:
292
- `"svg:circle"`, `"mml:math"`, etc.
293
- 4. **Cache** -- Results are cached in a `Map<string, NamespacedElementName>`
294
- keyed by the concatenation `name + namespaceURI`.
295
-
296
- ### Namespace URI Mapping
297
-
298
- | Namespace URI | Shorthand |
299
- | ------------------------------------ | --------- |
300
- | `http://www.w3.org/1999/xhtml` | `html` |
301
- | `http://www.w3.org/2000/svg` | `svg` |
302
- | `http://www.w3.org/1998/Math/MathML` | `mml` |
303
- | `http://www.w3.org/1999/xlink` | `xlink` |
304
-
305
- ### `getNS` Helper
306
-
307
- ```ts
308
- function getNS(namespaceURI: string | null): Namespace;
309
- ```
310
-
311
- A simple switch-case that maps a namespace URI string to its shorthand. Any
312
- unrecognized URI (including `null`) returns `'html'`.
313
-
314
- ---
315
-
316
- ## Attribute Spec Resolution
317
-
318
- **Files:** `src/utils/get-attr-specs.ts` (DOM wrapper), `src/utils/get-attr-specs-spec.ts` (core)
319
-
320
- ### DOM Wrapper: `getAttrSpecs` (from `get-attr-specs.ts`)
321
-
322
- ```ts
323
- function getAttrSpecs(el: Element, schema: MLMLSpec): readonly Attribute[] | null;
324
- ```
325
-
326
- Extracts `el.localName` and `el.namespaceURI`, then delegates to the core
327
- function.
328
-
329
- ### Core Function: `getAttrSpecs` (from `get-attr-specs-spec.ts`)
330
-
331
- ```ts
332
- function getAttrSpecs(localName: string, namespace: NamespaceURI | null, schema: MLMLSpec): readonly Attribute[] | null;
333
- ```
334
-
335
- Resolution steps:
336
-
337
- 1. **Schema invalidation** -- If the `schema` reference has changed (checked via
338
- a `WeakSet<MLMLSpec>`), the entire attribute cache is cleared. This ensures
339
- correctness when specs are re-merged.
340
-
341
- 2. **Cache check** -- Look up the namespace-qualified name in
342
- `Map<string, readonly Attribute[] | null>`.
343
-
344
- 3. **Find element spec** -- Search `schema.specs` for a matching element by
345
- namespace-qualified name. Return `null` (and cache it) if not found.
346
-
347
- 4. **Collect global attributes** -- Iterate over the element's `globalAttrs`
348
- selection map. For each category:
349
- - `false` -- skip the category entirely
350
- - `true` -- include all attributes from that global category
351
- - `string[]` -- include only the named attributes from that category
352
-
353
- ```ts
354
- for (const catName in elSpec.globalAttrs) {
355
- const catAttrs = elSpec.globalAttrs[catName];
356
- if (catAttrs === false) continue;
357
- if (typeof catAttrs === 'boolean') {
358
- attrs = { ...attrs, ...global };
359
- }
360
- if (Array.isArray(catAttrs)) {
361
- for (const selectedName of catAttrs) {
362
- attrs[selectedName] = { ...attrs[selectedName], ...global[selectedName] };
363
- }
364
- }
365
- }
366
- ```
367
-
368
- 5. **Merge element-specific attributes** -- The element's own `attributes` are
369
- spread on top of the collected globals, so element-specific definitions
370
- override globals:
371
-
372
- ```ts
373
- attrs[attrName] = {
374
- description: '',
375
- ...current, // from globals
376
- ...attr, // from element spec
377
- };
378
- ```
379
-
380
- 6. **Convert to sorted array** -- The attribute map is converted to an
381
- `Attribute[]`, giving each entry a default `type: 'Any'` if none was
382
- provided, then sorted alphabetically (case-insensitive) using `nameCompare`.
383
-
384
- 7. **Cache and return** -- The sorted array is stored in the cache and returned.
385
-
386
- ### `nameCompare`
387
-
388
- ```ts
389
- function nameCompare(a: HasName | string, b: HasName | string): number;
390
- ```
391
-
392
- Case-insensitive sort comparator. Extracts the `name` property (or uses the
393
- string directly), converts to uppercase, and performs standard lexicographic
394
- comparison.
395
-
396
- ---
397
-
398
- ## ARIA Version Resolution
399
-
400
- **Files:** `src/utils/resolve-version.ts`, `src/utils/aria-version.ts`,
401
- `src/utils/validate-aria-version.ts`, `src/algorithm/aria/get-aria.ts`
402
-
403
- ### `resolveVersion`
404
-
405
- ```ts
406
- function resolveVersion(aria: ReadonlyDeep<ARIA>, version: ARIAVersion): Omit<ReadonlyDeep<ARIA>, ARIAVersion>;
407
- ```
408
-
409
- Extracts a version-specific ARIA definition from a multi-version `ARIA` object.
410
- For each property, the version-specific value is used if present; otherwise the
411
- base (version-agnostic) value applies:
412
-
413
- ```ts
414
- const implicitRole = aria[version]?.implicitRole ?? aria.implicitRole;
415
- const permittedRoles = aria[version]?.permittedRoles ?? aria.permittedRoles;
416
- // ...etc
417
- ```
418
-
419
- Special case: `namingProhibited` for version `'1.1'` always uses the base
420
- value, because the naming prohibition concept was not formalized until ARIA 1.2:
421
-
422
- ```ts
423
- const namingProhibited =
424
- version === '1.1' ? aria.namingProhibited : (aria[version]?.namingProhibited ?? aria.namingProhibited);
425
- ```
426
-
427
- The returned object contains only the resolved properties -- the version keys
428
- (`'1.1'`, `'1.2'`, `'1.3'`) are stripped from the type.
429
-
430
- ### `getARIA`
431
-
432
- ```ts
433
- function getARIA(
434
- specs: MLMLSpec,
435
- localName: string,
436
- namespace: string | null,
437
- version: ARIAVersion,
438
- matches: Matches,
439
- ): Omit<ReadonlyDeep<ARIA>, ARIAVersion | 'conditions'> | null;
440
- ```
441
-
442
- The high-level ARIA resolver. It:
443
-
444
- 1. Calls `getVersionResolvedARIA` (internal) to get the version-resolved spec.
445
- 2. If the spec has `conditions` (CSS-selector-keyed overrides), iterates through
446
- them and applies the first matching condition's properties. This handles
447
- cases like `<input type="checkbox">` having different ARIA semantics than
448
- `<input type="text">`.
449
- 3. Optimizes `permittedRoles` -- if both `"presentation"` and `"none"` are in
450
- the list, ensures both synonyms are present (per WAI-ARIA 1.2 note).
451
-
452
- The internal `getVersionResolvedARIA` function caches results in a
453
- `Map<string, ARIA | null>` keyed by `localName + namespace + version`.
454
-
455
- ### Version Constants and Validation
456
-
457
- ```ts
458
- // aria-version.ts
459
- const ariaVersions = ['1.1', '1.2', '1.3'] as const;
460
- const ARIA_RECOMMENDED_VERSION = '1.2';
461
-
462
- // validate-aria-version.ts
463
- function validateAriaVersion(version: string): version is ARIAVersion;
464
- ```
465
-
466
- `validateAriaVersion` is a type guard that checks whether a string is a member
467
- of the `ariaVersions` tuple. It is used at configuration boundaries to validate
468
- user-supplied version strings before they enter the typed pipeline.
469
-
470
- ---
471
-
472
- ## Array Merging (`mergeArray`)
473
-
474
- **File:** `src/utils/merge-array.ts`
475
-
476
- ```ts
477
- function mergeArray<T extends NamedDefinition>(a: readonly T[], b: readonly T[] | null | undefined): readonly T[];
478
- ```
479
-
480
- Where `NamedDefinition = string | { readonly name: string }`.
481
-
482
- This is the core merge utility used throughout the spec merging pipeline. It
483
- performs **name-based merging** rather than simple concatenation:
484
-
485
- ### Algorithm
486
-
487
- 1. If `b` is `null` or `undefined`, return `a` unchanged.
488
- 2. Start with a copy of `a`.
489
- 3. For each item in `b`:
490
- - Extract the name using `getName()` (case-insensitive, trimmed).
491
- - Search for an item with the same name in the result.
492
- - **No match found:** append the extension item.
493
- - **Match found (both are strings):** the string is a simple identifier with
494
- no additional data, so the base item is kept (the splice removes it, then
495
- the loop continues without pushing a replacement, effectively keeping the
496
- base version).
497
- - **Match found (base is string, extension is object):** replace with the
498
- richer object form from the extension.
499
- - **Match found (both are objects):** spread-merge the two objects, with the
500
- extension's properties taking precedence:
501
- ```ts
502
- const exItem = { ...aItem, ...bItem };
503
- ```
504
-
505
- ### `getName` Helper
506
-
507
- ```ts
508
- function getName(def: NamedDefinition): string {
509
- const result = typeof def === 'string' ? def : def.name;
510
- return result.toLowerCase().trim();
511
- }
512
- ```
513
-
514
- Names are normalized to lowercase and trimmed before comparison, ensuring
515
- case-insensitive matching.
516
-
517
- ---
518
-
519
- ## Caching Strategy
520
-
521
- The package uses multiple cache layers to avoid redundant computation. Since
522
- specs are typically loaded once and reused for the lifetime of a lint run, these
523
- caches provide significant performance benefits.
524
-
525
- ### Cache Inventory
526
-
527
- | # | Location | Cache Type | Key | Value | Invalidation |
528
- | --- | ----------------------------------------------- | ----------------------------------------------------------------- | -------------------------------------------------------- | ---------------------------------------------- | --------------------------------------------------------- |
529
- | 1 | `getSpecByTagName` | `Map<string, any>` | Namespace-qualified name (e.g., `"svg:circle"`) | `ElementSpec \| null` | Module lifetime (never cleared) |
530
- | 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) |
531
- | 3 | `getContentModel` | `WeakMap<Element, ...>` | DOM Element reference | `PermittedContentPattern[] \| boolean \| null` | Entries automatically removed when Element is GC'd |
532
- | 4 | `contentModelCategoryToTagNames` | `Map<Category, ReadonlyArray<string>>` | Category string (e.g., `"#flow"`) | Frozen sorted array of tag names | Module lifetime (never cleared) |
533
- | 5 | `resolveNamespace` | `Map<string, NamespacedElementName>` | `name + namespaceURI` (string concatenation) | Resolved namespace object | Module lifetime (never cleared) |
534
- | 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) |
535
-
536
- ### Cache Characteristics
537
-
538
- **No explicit eviction:** Most caches are module-level `Map` instances that
539
- persist for the entire process lifetime. This is appropriate because:
540
-
541
- - Spec data is immutable after merging.
542
- - The number of unique elements/attributes is bounded (HTML has ~120 elements).
543
- - A lint run typically processes one configuration.
544
-
545
- **Schema-aware invalidation:** The `getAttrSpecs` cache (item 6) is the
546
- exception. It uses a `WeakSet<MLMLSpec>` to detect when the schema reference
547
- changes. If a new schema is passed (e.g., when running different file patterns
548
- with different framework specs), the entire `cacheMap` is cleared before
549
- proceeding:
550
-
551
- ```ts
552
- if (!schemaCache.has(schema)) {
553
- cacheMap.clear();
554
- }
555
- ```
556
-
557
- **WeakMap caching:** The `getContentModel` cache (item 3) uses a
558
- `WeakMap<Element, ...>` keyed by DOM Element reference. Re-querying the same
559
- element is O(1). When elements are garbage-collected (e.g., after a re-parse),
560
- their cache entries are automatically removed, preventing memory leaks.
561
-
562
- **Deterministic keys:** Caches in items 1, 2, and 5 use string concatenation
563
- for keys. Since `resolveNamespace` produces deterministic output for the same
564
- inputs, and spec data does not mutate, these concatenated keys are stable.
565
-
566
- ### Data Flow with Caching
567
-
568
- ```
569
- schemaToSpec() --> merged MLMLSpec (no cache; called once at startup)
570
- |
571
- +-----------+-----------+
572
- | | |
573
- getSpecByTagName getAttrSpecs getARIA
574
- (cache 1) (cache 6) (cache 2)
575
- | |
576
- resolveNamespace resolveVersion
577
- (cache 5) (pure; no cache)
578
- |
579
- getContentModel
580
- (cache 3)
581
- |
582
- contentModelCategoryToTagNames
583
- (cache 4)
584
- ```
585
-
586
- Each arrow represents a function call. Caches intercept repeated calls at each
587
- layer, so a second lookup for the same element hits cached results at every
588
- level of the call chain.