@markuplint/ml-core 5.0.0 → 5.1.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 +20 -0
- package/lib/index.d.ts +1 -1
- package/lib/ml-core.d.ts +12 -2
- package/lib/ml-core.js +12 -1
- package/lib/ml-dom/helper/accname.js +16 -0
- package/lib/ml-dom/node/element.d.ts +6 -0
- package/lib/ml-dom/node/element.js +193 -52
- package/lib/ml-dom/node/node-store.d.ts +12 -3
- package/lib/ml-dom/node/node-store.js +12 -5
- package/lib/ml-dom/node/node.js +21 -4
- package/lib/ml-dom/node/types.d.ts +37 -0
- package/package.json +12 -12
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.0](https://github.com/markuplint/markuplint/compare/v5.0.1...v5.1.0) (2026-10-08)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
- **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)
|
|
11
|
+
- **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)
|
|
12
|
+
- **ml-core:** mark the slot content mutable when any of several slots has unknown content ([b657ab9](https://github.com/markuplint/markuplint/commit/b657ab97de39242ba3d26d5484693230d89400aa))
|
|
13
|
+
- **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)
|
|
14
|
+
- **ml-core:** omit a forwarded attribute the usage site does not write ([7be4d86](https://github.com/markuplint/markuplint/commit/7be4d867ed95f6e50f68a7bd267bf693a18e7c4c))
|
|
15
|
+
- **ml-core:** scope NodeStore per document instead of a process-wide singleton ([71e2042](https://github.com/markuplint/markuplint/commit/71e204262261ca4317bb1fbe8967e998dd24628f))
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
- **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)
|
|
20
|
+
- **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)
|
|
21
|
+
|
|
22
|
+
## [5.0.1](https://github.com/markuplint/markuplint/compare/v5.0.0...v5.0.1) (2026-09-22)
|
|
23
|
+
|
|
24
|
+
**Note:** Version bump only for package @markuplint/ml-core
|
|
25
|
+
|
|
6
26
|
# [5.0.0](https://github.com/markuplint/markuplint/compare/v5.0.0-rc.7...v5.0.0) (2026-09-11)
|
|
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
|
/**
|
|
@@ -96,9 +96,19 @@ export declare class MLCore {
|
|
|
96
96
|
/**
|
|
97
97
|
* Replaces the source code and re-parses the document.
|
|
98
98
|
*
|
|
99
|
+
* `options.pretenders` is for a host that re-resolves pretenders from the
|
|
100
|
+
* new source (`pretenders.auto` and selector disambiguation both read the
|
|
101
|
+
* file's own imports, see #4064): they take effect in this same re-parse.
|
|
102
|
+
* Routing them through {@link update} instead would also reset
|
|
103
|
+
* `configErrors` / `ruleDeprecations` to whatever that call carries — and
|
|
104
|
+
* those are config-time values an edit of the source does not change.
|
|
105
|
+
*
|
|
99
106
|
* @param sourceCode - The new markup source code
|
|
107
|
+
* @param options - `pretenders` replaces the current pretenders; omitted, they are kept
|
|
100
108
|
*/
|
|
101
|
-
setCode(sourceCode: string
|
|
109
|
+
setCode(sourceCode: string, options?: {
|
|
110
|
+
readonly pretenders?: readonly Pretender[];
|
|
111
|
+
}): void;
|
|
102
112
|
/**
|
|
103
113
|
* Updates the linting configuration and re-creates the document.
|
|
104
114
|
* Only re-parses if parser options have changed.
|
package/lib/ml-core.js
CHANGED
|
@@ -115,10 +115,21 @@ export class MLCore {
|
|
|
115
115
|
/**
|
|
116
116
|
* Replaces the source code and re-parses the document.
|
|
117
117
|
*
|
|
118
|
+
* `options.pretenders` is for a host that re-resolves pretenders from the
|
|
119
|
+
* new source (`pretenders.auto` and selector disambiguation both read the
|
|
120
|
+
* file's own imports, see #4064): they take effect in this same re-parse.
|
|
121
|
+
* Routing them through {@link update} instead would also reset
|
|
122
|
+
* `configErrors` / `ruleDeprecations` to whatever that call carries — and
|
|
123
|
+
* those are config-time values an edit of the source does not change.
|
|
124
|
+
*
|
|
118
125
|
* @param sourceCode - The new markup source code
|
|
126
|
+
* @param options - `pretenders` replaces the current pretenders; omitted, they are kept
|
|
119
127
|
*/
|
|
120
|
-
setCode(sourceCode) {
|
|
128
|
+
setCode(sourceCode, options) {
|
|
121
129
|
this.#sourceCode = sourceCode;
|
|
130
|
+
if (options?.pretenders) {
|
|
131
|
+
this.#pretenders = [...options.pretenders];
|
|
132
|
+
}
|
|
122
133
|
this.#parse();
|
|
123
134
|
this.#createDocument();
|
|
124
135
|
}
|
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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();
|
package/lib/ml-dom/node/node.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "5.1.0",
|
|
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.
|
|
38
|
-
"@markuplint/html-parser": "5.
|
|
39
|
-
"@markuplint/html-spec": "5.
|
|
40
|
-
"@markuplint/i18n": "5.
|
|
41
|
-
"@markuplint/ml-ast": "5.
|
|
42
|
-
"@markuplint/ml-config": "5.
|
|
43
|
-
"@markuplint/ml-spec": "5.
|
|
44
|
-
"@markuplint/parser-utils": "5.
|
|
45
|
-
"@markuplint/selector": "5.
|
|
46
|
-
"@markuplint/shared": "5.
|
|
37
|
+
"@markuplint/config-presets": "5.1.0",
|
|
38
|
+
"@markuplint/html-parser": "5.1.0",
|
|
39
|
+
"@markuplint/html-spec": "5.1.0",
|
|
40
|
+
"@markuplint/i18n": "5.1.0",
|
|
41
|
+
"@markuplint/ml-ast": "5.1.0",
|
|
42
|
+
"@markuplint/ml-config": "5.1.0",
|
|
43
|
+
"@markuplint/ml-spec": "5.1.0",
|
|
44
|
+
"@markuplint/parser-utils": "5.1.0",
|
|
45
|
+
"@markuplint/selector": "5.1.0",
|
|
46
|
+
"@markuplint/shared": "5.1.0",
|
|
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": "
|
|
52
|
+
"gitHead": "3a061784a8211034ad0c1cbd64798c0213414f65"
|
|
53
53
|
}
|