@markuplint/ml-core 5.0.1 → 5.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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.1.1](https://github.com/markuplint/markuplint/compare/v5.1.0...v5.1.1) (2026-10-09)
7
+
8
+ **Note:** Version bump only for package @markuplint/ml-core
9
+
10
+ # [5.1.0](https://github.com/markuplint/markuplint/compare/v5.0.1...v5.1.0) (2026-10-08)
11
+
12
+ ### Bug Fixes
13
+
14
+ - **ml-core:** compute the accessible name of a pretender from what it renders ([22ea8f9](https://github.com/markuplint/markuplint/commit/22ea8f9da831a06084e5748ec5db44c6e1e66e24)), closes [#4056](https://github.com/markuplint/markuplint/issues/4056)
15
+ - **ml-core:** ignore the children of a component that never renders them in its name ([9867097](https://github.com/markuplint/markuplint/commit/986709732e69ed2bc63d7261efbaeb1567ab6505)), closes [#4056](https://github.com/markuplint/markuplint/issues/4056)
16
+ - **ml-core:** mark the slot content mutable when any of several slots has unknown content ([b657ab9](https://github.com/markuplint/markuplint/commit/b657ab97de39242ba3d26d5484693230d89400aa))
17
+ - **ml-core:** narrow getNodeStoreFor()'s parameter to MLDocument ([bbdbc44](https://github.com/markuplint/markuplint/commit/bbdbc444a2e5267418ef8adedb9e3e40aa42b6e6)), closes [#4081](https://github.com/markuplint/markuplint/issues/4081) [/github.com/markuplint/markuplint/pull/4081#discussion_r4162890932](https://github.com//github.com/markuplint/markuplint/pull/4081/issues/discussion_r4162890932)
18
+ - **ml-core:** omit a forwarded attribute the usage site does not write ([7be4d86](https://github.com/markuplint/markuplint/commit/7be4d867ed95f6e50f68a7bd267bf693a18e7c4c))
19
+ - **ml-core:** scope NodeStore per document instead of a process-wide singleton ([71e2042](https://github.com/markuplint/markuplint/commit/71e204262261ca4317bb1fbe8967e998dd24628f))
20
+
21
+ ### Features
22
+
23
+ - **ml-core:** build slot wrappers and static contents for pretenders ([7770c19](https://github.com/markuplint/markuplint/commit/7770c197e76f6cc6f1f8380c4b8221d9e878e058)), closes [#4054](https://github.com/markuplint/markuplint/issues/4054)
24
+ - **ml-core:** let setCode swap in pretenders within the same re-parse ([0b8a3c5](https://github.com/markuplint/markuplint/commit/0b8a3c5e8b9ce048dedae43e8ba1cc91f9ec5e31)), closes [#4064](https://github.com/markuplint/markuplint/issues/4064)
25
+
6
26
  ## [5.0.1](https://github.com/markuplint/markuplint/compare/v5.0.0...v5.0.1) (2026-09-22)
7
27
 
8
28
  **Note:** Version bump only for package @markuplint/ml-core
package/lib/index.d.ts CHANGED
@@ -15,4 +15,4 @@ export type * from './types.js';
15
15
  export * from './utils/index.js';
16
16
  export * from './violation-collector.js';
17
17
  export * from './virtual-rule.js';
18
- export type { AccessibilityProperties } from './ml-dom/node/types.js';
18
+ export type { AccessibilityProperties, PretenderSlotContent } from './ml-dom/node/types.js';
package/lib/ml-core.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { MLFabric } from './types.js';
2
- import type { PlainData, RuleConfigValue, TextEdit, Violation } from '@markuplint/ml-config';
2
+ import type { PlainData, Pretender, RuleConfigValue, TextEdit, Violation } from '@markuplint/ml-config';
3
3
  import { ParserError } from '@markuplint/parser-utils';
4
4
  import { Document } from './ml-dom/index.js';
5
5
  /**
@@ -8,6 +8,11 @@ import { Document } from './ml-dom/index.js';
8
8
  * config-level violations (e.g. the CLI's per-run dedupe and failed-file
9
9
  * counting in `packages/markuplint/src/cli/command.ts`) reference the same
10
10
  * literal `MLCore.verify()` emits, instead of duplicating the string.
11
+ *
12
+ * Two severities share it. `MLCore.verify()` emits `warning`: the rest of the
13
+ * config is still usable, so the file is linted. `MLEngine` emits `error`,
14
+ * as the file's only violation, when the config cannot be loaded at all
15
+ * (`ConfigLoadError`): no rule has looked at the file, so it must not pass.
11
16
  */
12
17
  export declare const CONFIG_ERROR_RULE_ID = "config-error";
13
18
  /**
@@ -96,9 +101,19 @@ export declare class MLCore {
96
101
  /**
97
102
  * Replaces the source code and re-parses the document.
98
103
  *
104
+ * `options.pretenders` is for a host that re-resolves pretenders from the
105
+ * new source (`pretenders.auto` and selector disambiguation both read the
106
+ * file's own imports, see #4064): they take effect in this same re-parse.
107
+ * Routing them through {@link update} instead would also reset
108
+ * `configErrors` / `ruleDeprecations` to whatever that call carries — and
109
+ * those are config-time values an edit of the source does not change.
110
+ *
99
111
  * @param sourceCode - The new markup source code
112
+ * @param options - `pretenders` replaces the current pretenders; omitted, they are kept
100
113
  */
101
- setCode(sourceCode: string): void;
114
+ setCode(sourceCode: string, options?: {
115
+ readonly pretenders?: readonly Pretender[];
116
+ }): void;
102
117
  /**
103
118
  * Updates the linting configuration and re-creates the document.
104
119
  * Only re-parses if parser options have changed.
package/lib/ml-core.js CHANGED
@@ -10,6 +10,11 @@ const resultLog = log.extend('result');
10
10
  * config-level violations (e.g. the CLI's per-run dedupe and failed-file
11
11
  * counting in `packages/markuplint/src/cli/command.ts`) reference the same
12
12
  * literal `MLCore.verify()` emits, instead of duplicating the string.
13
+ *
14
+ * Two severities share it. `MLCore.verify()` emits `warning`: the rest of the
15
+ * config is still usable, so the file is linted. `MLEngine` emits `error`,
16
+ * as the file's only violation, when the config cannot be loaded at all
17
+ * (`ConfigLoadError`): no rule has looked at the file, so it must not pass.
13
18
  */
14
19
  export const CONFIG_ERROR_RULE_ID = 'config-error';
15
20
  /**
@@ -115,10 +120,21 @@ export class MLCore {
115
120
  /**
116
121
  * Replaces the source code and re-parses the document.
117
122
  *
123
+ * `options.pretenders` is for a host that re-resolves pretenders from the
124
+ * new source (`pretenders.auto` and selector disambiguation both read the
125
+ * file's own imports, see #4064): they take effect in this same re-parse.
126
+ * Routing them through {@link update} instead would also reset
127
+ * `configErrors` / `ruleDeprecations` to whatever that call carries — and
128
+ * those are config-time values an edit of the source does not change.
129
+ *
118
130
  * @param sourceCode - The new markup source code
131
+ * @param options - `pretenders` replaces the current pretenders; omitted, they are kept
119
132
  */
120
- setCode(sourceCode) {
133
+ setCode(sourceCode, options) {
121
134
  this.#sourceCode = sourceCode;
135
+ if (options?.pretenders) {
136
+ this.#pretenders = [...options.pretenders];
137
+ }
122
138
  this.#parse();
123
139
  this.#createDocument();
124
140
  }
@@ -78,5 +78,21 @@ el, version) {
78
78
  const attrValue = targetEl.getAttributePretended(attrName);
79
79
  return attrValue || null;
80
80
  },
81
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
82
+ getChildNodes(target) {
83
+ // TODO: Remove cast when AccnameResolver is generic (#3178)
84
+ const targetEl = target;
85
+ const context = targetEl.pretenderContext;
86
+ if (context?.type !== 'pretender') {
87
+ return targetEl.childNodes;
88
+ }
89
+ // The children the component renders: `as.childNodes` is empty when it never
90
+ // renders its children (`slots: null`), so text written inside `<Img>Save</Img>`
91
+ // does not become its name.
92
+ const given = [...context.as.childNodes];
93
+ // A pretended component renders its own elements (`contents`) around the
94
+ // children given at the usage site; the name can come from either.
95
+ return context.slotContent ? context.slotContent.fill(given) : given;
96
+ },
81
97
  };
82
98
  }
@@ -57,6 +57,12 @@ export declare class MLElement<T extends RuleConfigValue, O extends PlainData =
57
57
  * The virtual element created by `pretending()` is not registered in the
58
58
  * document's `nodeList`; walkers visit only the original element, which
59
59
  * delegates its name and attribute getters to the virtual element.
60
+ *
61
+ * The children a rule should validate are not `as.childNodes` (which mirrors
62
+ * only whether the component accepts children). They are
63
+ * `slotContent.fill(childNodes)` when the pretender describes `slots` /
64
+ * `contents`, because the component renders its own elements around the
65
+ * children given at the usage site.
60
66
  */
61
67
  pretenderContext: PretenderContext<MLElement<T, O>, T, O> | null;
62
68
  /**
@@ -1,4 +1,5 @@
1
1
  /* global StylePropertyMap, StylePropertyMapReadOnly */
2
+ var _a;
2
3
  import { getSpecByTagName, resolveNamespace } from '@markuplint/ml-spec';
3
4
  import { matchSelector } from '@markuplint/selector';
4
5
  import { getAccname } from '../helper/accname.js';
@@ -85,6 +86,12 @@ export class MLElement extends MLParentNode {
85
86
  * The virtual element created by `pretending()` is not registered in the
86
87
  * document's `nodeList`; walkers visit only the original element, which
87
88
  * delegates its name and attribute getters to the virtual element.
89
+ *
90
+ * The children a rule should validate are not `as.childNodes` (which mirrors
91
+ * only whether the component accepts children). They are
92
+ * `slotContent.fill(childNodes)` when the pretender describes `slots` /
93
+ * `contents`, because the component renders its own elements around the
94
+ * children given at the usage site.
88
95
  */
89
96
  pretenderContext = null;
90
97
  /**
@@ -2902,62 +2909,12 @@ export class MLElement extends MLParentNode {
2902
2909
  attributes.push(...this._astToken.attributes);
2903
2910
  }
2904
2911
  if (pretenderElement.attrs) {
2905
- attributes.push(...pretenderElement.attrs.map(({ name, value }, i) => {
2906
- const _value = value == null
2907
- ? ''
2908
- : typeof value === 'string'
2909
- ? value
2910
- : (this.getAttribute(value.fromAttr) ?? '');
2911
- return {
2912
- ...this._astToken,
2913
- uuid: `${this.uuid}_attr_${i}`,
2914
- type: 'attr',
2915
- nodeName: name,
2916
- spacesBeforeName: {
2917
- ...this._astToken,
2918
- raw: '',
2919
- },
2920
- name: {
2921
- ...this._astToken,
2922
- raw: name,
2923
- },
2924
- spacesBeforeEqual: {
2925
- ...this._astToken,
2926
- raw: '',
2927
- },
2928
- equal: {
2929
- ...this._astToken,
2930
- raw: '',
2931
- },
2932
- spacesAfterEqual: {
2933
- ...this._astToken,
2934
- raw: '',
2935
- },
2936
- startQuote: {
2937
- ...this._astToken,
2938
- raw: '',
2939
- },
2940
- value: {
2941
- ...this._astToken,
2942
- raw: _value,
2943
- },
2944
- endQuote: {
2945
- ...this._astToken,
2946
- raw: '',
2947
- },
2948
- isDuplicatable: true,
2949
- parentNode: null,
2950
- nextNode: null,
2951
- prevNode: null,
2952
- isFragment: false,
2953
- isGhost: false,
2954
- };
2955
- }));
2912
+ attributes.push(...this.#createPretenderAttrs(pretenderElement.attrs, this.uuid));
2956
2913
  }
2957
2914
  aria = pretenderElement.aria;
2958
2915
  }
2959
2916
  const slots = typeof pretenderElement === 'string' ? undefined : pretenderElement.slots;
2960
- const as = new MLElement({
2917
+ const as = new _a({
2961
2918
  ...this._astToken,
2962
2919
  uuid: this.uuid + '_pretender',
2963
2920
  raw: `<${nodeName}>`,
@@ -2978,10 +2935,179 @@ export class MLElement extends MLParentNode {
2978
2935
  type: 'origin',
2979
2936
  origin: this,
2980
2937
  };
2938
+ const slotContent = typeof pretenderElement === 'string' ? undefined : this.#createSlotContent(pretenderElement, as, namespace);
2981
2939
  this.pretenderContext = {
2982
2940
  type: 'pretender',
2983
2941
  as,
2984
2942
  aria,
2943
+ ...(slotContent ? { slotContent } : {}),
2944
+ };
2945
+ }
2946
+ /**
2947
+ * Converts pretender attributes to AST attributes for a virtual element.
2948
+ *
2949
+ * `{ fromAttr }` reads this (the origin) element's attribute. When that attribute is an
2950
+ * expression (`kind={kind}`), the result is dynamic as well: the value the component
2951
+ * receives is unknown, and the source text of the expression is not a value to validate.
2952
+ * When the attribute is not written and `omitIfMissing` is set, the virtual element gets
2953
+ * no such attribute, as a component that forwards an `undefined` prop renders none.
2954
+ * Without `omitIfMissing`, it is an empty attribute.
2955
+ *
2956
+ * `{ dynamic: true }` yields an attribute flagged `isDynamicValue`, which rules do not
2957
+ * validate.
2958
+ */
2959
+ #createPretenderAttrs(attrs, uuidBase) {
2960
+ return attrs.flatMap(({ name, value }, i) => {
2961
+ let isDynamic = value != null && typeof value === 'object' && 'dynamic' in value;
2962
+ let _value = '';
2963
+ if (typeof value === 'string') {
2964
+ _value = value;
2965
+ }
2966
+ else if (value != null && 'fromAttr' in value) {
2967
+ const fromAttr = value.fromAttr.toLowerCase();
2968
+ const origin = this.#attributes.find(attr => attr.name.toLowerCase() === fromAttr);
2969
+ if (!origin && value.omitIfMissing) {
2970
+ return [];
2971
+ }
2972
+ if (origin?.isDynamicValue) {
2973
+ isDynamic = true;
2974
+ }
2975
+ else {
2976
+ _value = origin?.value ?? '';
2977
+ }
2978
+ }
2979
+ return [
2980
+ {
2981
+ ...this._astToken,
2982
+ uuid: `${uuidBase}_attr_${i}`,
2983
+ type: 'attr',
2984
+ nodeName: name,
2985
+ spacesBeforeName: {
2986
+ ...this._astToken,
2987
+ raw: '',
2988
+ },
2989
+ name: {
2990
+ ...this._astToken,
2991
+ raw: name,
2992
+ },
2993
+ spacesBeforeEqual: {
2994
+ ...this._astToken,
2995
+ raw: '',
2996
+ },
2997
+ equal: {
2998
+ ...this._astToken,
2999
+ raw: '',
3000
+ },
3001
+ spacesAfterEqual: {
3002
+ ...this._astToken,
3003
+ raw: '',
3004
+ },
3005
+ startQuote: {
3006
+ ...this._astToken,
3007
+ raw: '',
3008
+ },
3009
+ value: {
3010
+ ...this._astToken,
3011
+ raw: _value,
3012
+ },
3013
+ endQuote: {
3014
+ ...this._astToken,
3015
+ raw: '',
3016
+ },
3017
+ isDuplicatable: true,
3018
+ ...(isDynamic ? { isDynamicValue: true } : {}),
3019
+ parentNode: null,
3020
+ nextNode: null,
3021
+ prevNode: null,
3022
+ isFragment: false,
3023
+ isGhost: false,
3024
+ },
3025
+ ];
3026
+ });
3027
+ }
3028
+ /**
3029
+ * Creates a childless virtual element that is not registered in the document's
3030
+ * `nodeList`, used for a slot wrapper or a static entry of `contents`.
3031
+ * Its close tag is deliberately dropped (`pairNodeUuid: null`): the MLElement
3032
+ * constructor would otherwise re-register the origin's real close tag under
3033
+ * the virtual element.
3034
+ */
3035
+ #createVirtualElement(spec, uuid, parentNodeUuid, fallbackNamespace) {
3036
+ const attributes = [];
3037
+ if (spec.inheritAttrs) {
3038
+ attributes.push(...this._astToken.attributes);
3039
+ }
3040
+ if (spec.attrs) {
3041
+ attributes.push(...this.#createPretenderAttrs(spec.attrs, uuid));
3042
+ }
3043
+ const virtual = new _a({
3044
+ ...this._astToken,
3045
+ uuid,
3046
+ raw: `<${spec.element}>`,
3047
+ nodeName: spec.element,
3048
+ namespace: getVirtualNamespace(spec, fallbackNamespace),
3049
+ elementType: spec.element.includes('-') ? 'web-component' : 'html',
3050
+ attributes,
3051
+ childNodes: [],
3052
+ pairNodeUuid: null,
3053
+ parentNodeUuid,
3054
+ }, this.ownerMLDocument);
3055
+ virtual.resetChildren(toNodeList([]));
3056
+ // Like the pretender element `as`, it stands for this element, so that a report
3057
+ // on it can be moved to where the user wrote the component.
3058
+ virtual.pretenderContext = { type: 'origin', origin: this };
3059
+ return virtual;
3060
+ }
3061
+ /**
3062
+ * Builds {@link PretenderSlotContent} from the pretender definition.
3063
+ * Returns `undefined` when the definition says nothing about what surrounds
3064
+ * the children, so that the children given at the usage site stay the whole
3065
+ * content of the outermost element (the bare-string pretender behavior).
3066
+ */
3067
+ #createSlotContent(node, as, namespace) {
3068
+ const { slots } = node;
3069
+ let wrapper = as;
3070
+ let contents = node.contents;
3071
+ if (slots != null && slots !== true && slots.length > 0) {
3072
+ const [slot] = slots;
3073
+ if (slots.length > 1 || !slot) {
3074
+ // The wrapper of each child is unknown, so the children are not validated,
3075
+ // but a slot may still render unknown content (which may be a name).
3076
+ return {
3077
+ wrapper: null,
3078
+ mutable: slots.some(s => s.contents?.some(entry => 'dynamic' in entry) ?? false),
3079
+ fill: (given) => given,
3080
+ };
3081
+ }
3082
+ wrapper = this.#createVirtualElement(slot, `${this.uuid}_pretender_slot`, this.uuid, namespace);
3083
+ contents = slot.contents ?? [{ slot: true }];
3084
+ }
3085
+ if (!contents) {
3086
+ return undefined;
3087
+ }
3088
+ const entries = contents.map((entry, i) => 'element' in entry
3089
+ ? this.#createVirtualElement(entry, `${this.uuid}_pretender_content_${i}`, wrapper === as ? this.uuid : wrapper.uuid, namespace)
3090
+ : entry);
3091
+ return {
3092
+ wrapper,
3093
+ mutable: entries.some(entry => !(entry instanceof _a) && 'dynamic' in entry),
3094
+ fill: (given) => {
3095
+ const filled = [];
3096
+ let placed = false;
3097
+ for (const entry of entries) {
3098
+ if (entry instanceof _a) {
3099
+ filled.push(entry);
3100
+ }
3101
+ else if ('slot' in entry && !placed) {
3102
+ filled.push(...given);
3103
+ placed = true;
3104
+ }
3105
+ }
3106
+ if (!placed) {
3107
+ filled.push(...given);
3108
+ }
3109
+ return filled;
3110
+ },
2985
3111
  };
2986
3112
  }
2987
3113
  /**
@@ -3245,3 +3371,18 @@ export class MLElement extends MLParentNode {
3245
3371
  throw new UnexpectedCallError('Not supported "webkitMatchesSelector" method');
3246
3372
  }
3247
3373
  }
3374
+ _a = MLElement;
3375
+ /**
3376
+ * The namespace of a virtual element made from a pretender definition.
3377
+ * `svg` and `math` are foreign elements wherever they are written, and a
3378
+ * definition can only state `namespace: 'svg'`, so the name decides.
3379
+ */
3380
+ function getVirtualNamespace(spec, fallback) {
3381
+ if (spec.namespace === 'svg' || spec.element === 'svg') {
3382
+ return 'http://www.w3.org/2000/svg';
3383
+ }
3384
+ if (spec.element === 'math') {
3385
+ return 'http://www.w3.org/1998/Math/MathML';
3386
+ }
3387
+ return fallback;
3388
+ }
@@ -2,11 +2,20 @@ import type { MLNode } from './node.js';
2
2
  import type { MappedNode } from './types.js';
3
3
  import type { MLASTNode } from '@markuplint/ml-ast';
4
4
  import type { PlainData, RuleConfigValue } from '@markuplint/ml-config';
5
- declare class NodeStore {
5
+ /**
6
+ * Maps the AST nodes of one document to the `MLNode`s built from them.
7
+ *
8
+ * Owned by a single `MLDocument` and shared with its nodes through `MLNode`'s
9
+ * constructor. A node is only ever looked up through its own document
10
+ * (`syntacticalParentNode` resolves `parentNodeUuid`, `getPureChildNodes`
11
+ * resolves child AST nodes), so the store must not be held anywhere that
12
+ * outlives that document: a longer-lived holder keeps every node — and through
13
+ * `ownerMLDocument` the whole tree — reachable, which an engine that calls
14
+ * `setCode()` / `exec()` repeatedly accumulates without bound (see #4074).
15
+ */
16
+ export declare class NodeStore {
6
17
  #private;
7
18
  getNode<N extends MLASTNode, T extends RuleConfigValue, O extends PlainData = undefined>(astNode: N): MappedNode<N, T, O>;
8
19
  getNodeByUuid<T extends RuleConfigValue, O extends PlainData = undefined>(uuid: string): MLNode<T, O, any>;
9
20
  setNode<A extends MLASTNode, T extends RuleConfigValue, O extends PlainData = undefined>(astNode: A, node: MLNode<T, O, A>): void;
10
21
  }
11
- export declare const nodeStore: NodeStore;
12
- export {};
@@ -2,7 +2,18 @@ import { TargetParserError } from '@markuplint/parser-utils';
2
2
  import { log } from '../../debug.js';
3
3
  const nodeStoreLog = log.extend('node-store');
4
4
  const nodeStoreError = nodeStoreLog.extend('error');
5
- class NodeStore {
5
+ /**
6
+ * Maps the AST nodes of one document to the `MLNode`s built from them.
7
+ *
8
+ * Owned by a single `MLDocument` and shared with its nodes through `MLNode`'s
9
+ * constructor. A node is only ever looked up through its own document
10
+ * (`syntacticalParentNode` resolves `parentNodeUuid`, `getPureChildNodes`
11
+ * resolves child AST nodes), so the store must not be held anywhere that
12
+ * outlives that document: a longer-lived holder keeps every node — and through
13
+ * `ownerMLDocument` the whole tree — reachable, which an engine that calls
14
+ * `setCode()` / `exec()` repeatedly accumulates without bound (see #4074).
15
+ */
16
+ export class NodeStore {
6
17
  #store = new Map();
7
18
  getNode(astNode) {
8
19
  // console.log(`Get: ${astNode.uuid} -> ${astNode.raw.trim()}(${astNode.type})`);
@@ -35,9 +46,6 @@ class NodeStore {
35
46
  setNode(astNode,
36
47
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
37
48
  node) {
38
- if (node.is(node.DOCUMENT_NODE)) {
39
- return;
40
- }
41
49
  if (!astNode.uuid) {
42
50
  nodeStoreError('UUID is invalid: %s (%s: "%s")', astNode.uuid, astNode.nodeName, astNode.raw);
43
51
  nodeStoreError('Invalid node: %O', node);
@@ -46,4 +54,3 @@ class NodeStore {
46
54
  this.#store.set(astNode.uuid, node);
47
55
  }
48
56
  }
49
- export const nodeStore = new NodeStore();
@@ -2,7 +2,7 @@ import { branchesToPatterns, UnexpectedCallError } from '@markuplint/shared';
2
2
  import { MLToken } from '../token/token.js';
3
3
  import { isChildNode } from './child-node.js';
4
4
  import { toNodeList } from './node-list.js';
5
- import { nodeStore } from './node-store.js';
5
+ import { NodeStore } from './node-store.js';
6
6
  /**
7
7
  * Abstract base class for all markuplint DOM node wrappers.
8
8
  * Extends `MLToken` with DOM `Node` interface compliance, tree traversal,
@@ -128,6 +128,12 @@ export class MLNode extends MLToken {
128
128
  */
129
129
  isFragment;
130
130
  #ownerDocument;
131
+ /**
132
+ * Scoped to the owning document so the mapping is reclaimed together with
133
+ * it, and held directly on each node so the hot `syntacticalParentNode` /
134
+ * `getPureChildNodes` paths reach it without a lookup.
135
+ */
136
+ #nodeStore;
131
137
  #prevToken;
132
138
  #conditionalChildNodes;
133
139
  /**
@@ -144,7 +150,18 @@ export class MLNode extends MLToken {
144
150
  this._astToken = astNode;
145
151
  this.#ownerDocument = document;
146
152
  this.isFragment = !!isFragment;
147
- nodeStore.setNode(astNode, this);
153
+ // `document` is `null` only for the `MLDocument` being constructed (see
154
+ // its `super(ast, null)` call), which creates the store its nodes share.
155
+ // The document node is deliberately not registered: its AST node has no
156
+ // `uuid`, and nothing resolves it through the store (see
157
+ // `syntacticalParentNode`, which returns `ownerMLDocument` directly).
158
+ if (document) {
159
+ this.#nodeStore = document.#nodeStore;
160
+ this.#nodeStore.setNode(astNode, this);
161
+ }
162
+ else {
163
+ this.#nodeStore = new NodeStore();
164
+ }
148
165
  }
149
166
  /**
150
167
  * **IT THROWS AN ERROR WHEN CALLING THIS.**
@@ -493,7 +510,7 @@ export class MLNode extends MLToken {
493
510
  if (!this._astToken.parentNodeUuid) {
494
511
  return this.ownerMLDocument;
495
512
  }
496
- return nodeStore.getNodeByUuid(this._astToken.parentNodeUuid);
513
+ return this.#nodeStore.getNodeByUuid(this._astToken.parentNodeUuid);
497
514
  }
498
515
  /**
499
516
  * Return the text content.
@@ -722,7 +739,7 @@ export class MLNode extends MLToken {
722
739
  return node;
723
740
  }) ?? [];
724
741
  const childNodes = astChildren
725
- .map(node => nodeStore.getNode(node))
742
+ .map(node => this.#nodeStore.getNode(node))
726
743
  .filter(node => isChildNode(node));
727
744
  // Cache
728
745
  this.#pureChildNodesCache = toNodeList(childNodes);
@@ -72,6 +72,43 @@ export type PretenderContextPretender<N extends MLElement<T, O>, T extends RuleC
72
72
  readonly type: 'pretender';
73
73
  readonly as: N;
74
74
  readonly aria?: PretenderARIA;
75
+ /**
76
+ * Present only when the pretender describes what the component renders around
77
+ * its children (`contents`, or exactly-one/multiple `slots` wrappers).
78
+ * Absent means the children given at the usage site are the whole content of
79
+ * the outermost element, which is the behavior of a bare-string pretender.
80
+ */
81
+ readonly slotContent?: PretenderSlotContent<N, T, O>;
82
+ };
83
+ /**
84
+ * What a rule needs to evaluate the children given to a pretended component
85
+ * the way the component actually renders them.
86
+ *
87
+ * @template N - The element type
88
+ * @template T - The rule configuration value type
89
+ * @template O - The rule options type
90
+ */
91
+ export type PretenderSlotContent<N extends MLElement<T, O>, T extends RuleConfigValue, O extends PlainData = undefined> = {
92
+ /**
93
+ * The element whose content model governs the evaluated children:
94
+ * the pretender itself, or the virtual element for the single slot wrapper.
95
+ * `null` when the wrapper is unknown (several slot wrappers), in which case
96
+ * the children must not be validated.
97
+ */
98
+ readonly wrapper: N | null;
99
+ /**
100
+ * `true` when `contents` (of any slot when there are several) has an entry
101
+ * whose content is unknown at scan time, so a required child, or a name,
102
+ * may be rendered there.
103
+ */
104
+ readonly mutable: boolean;
105
+ /**
106
+ * Builds the children of {@link wrapper} from the nodes given at the usage
107
+ * site: the static entries of `contents` as virtual elements, with the given
108
+ * nodes placed at the slot position (appended when there is no slot entry).
109
+ * Virtual elements are created once and reused on each call.
110
+ */
111
+ readonly fill: <C>(given: readonly C[]) => readonly (C | N)[];
75
112
  };
76
113
  /**
77
114
  * Context for the original element that has been pretended by another element.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markuplint/ml-core",
3
- "version": "5.0.1",
3
+ "version": "5.1.1",
4
4
  "description": "The core module of markuplint",
5
5
  "repository": {
6
6
  "type": "git",
@@ -34,20 +34,20 @@
34
34
  "./lib/configs.js": "./lib/configs.browser.js"
35
35
  },
36
36
  "dependencies": {
37
- "@markuplint/config-presets": "5.0.1",
38
- "@markuplint/html-parser": "5.0.1",
39
- "@markuplint/html-spec": "5.0.1",
40
- "@markuplint/i18n": "5.0.1",
41
- "@markuplint/ml-ast": "5.0.1",
42
- "@markuplint/ml-config": "5.0.1",
43
- "@markuplint/ml-spec": "5.0.1",
44
- "@markuplint/parser-utils": "5.0.1",
45
- "@markuplint/selector": "5.0.1",
46
- "@markuplint/shared": "5.0.1",
37
+ "@markuplint/config-presets": "5.1.1",
38
+ "@markuplint/html-parser": "5.1.1",
39
+ "@markuplint/html-spec": "5.1.1",
40
+ "@markuplint/i18n": "5.1.1",
41
+ "@markuplint/ml-ast": "5.1.1",
42
+ "@markuplint/ml-config": "5.1.1",
43
+ "@markuplint/ml-spec": "5.1.1",
44
+ "@markuplint/parser-utils": "5.1.1",
45
+ "@markuplint/selector": "5.1.1",
46
+ "@markuplint/shared": "5.1.1",
47
47
  "@types/debug": "4.1.13",
48
48
  "debug": "4.4.3",
49
49
  "is-plain-object": "5.0.0",
50
50
  "type-fest": "5.6.0"
51
51
  },
52
- "gitHead": "edbaeedca5588fb291ea9d4e21a4ab2bf41fc9cb"
52
+ "gitHead": "a940c8526e971d590d97395ce924910221208820"
53
53
  }