@markuplint/ml-core 5.1.1 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,14 @@
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.2.0](https://github.com/markuplint/markuplint/compare/v5.1.1...v5.2.0) (2026-10-13)
7
+
8
+ ### Bug Fixes
9
+
10
+ - **ml-core:** resolve the namespace of a component and the children given to it ([00ecbba](https://github.com/markuplint/markuplint/commit/00ecbbae6095dd8eb7d10148633d6610bed7f1e2))
11
+ - **ml-core:** return the DOM values from MLAttr.namespaceURI ([05467ad](https://github.com/markuplint/markuplint/commit/05467ad28d61ae0a272c8335db9a0b2f614e5592))
12
+ - use caret ranges for third-party dependencies of published packages ([5fb5d2e](https://github.com/markuplint/markuplint/commit/5fb5d2ee81eaeea45fe7256a40dd41c95ff0d672))
13
+
6
14
  ## [5.1.1](https://github.com/markuplint/markuplint/compare/v5.1.0...v5.1.1) (2026-10-09)
7
15
 
8
16
  **Note:** Version bump only for package @markuplint/ml-core
@@ -98,7 +98,9 @@ export declare class MLAttr<T extends RuleConfigValue, O extends PlainData = und
98
98
  */
99
99
  get name(): string;
100
100
  /**
101
- * Returns the namespace URI of this attribute, resolved from the attribute name.
101
+ * Returns the namespace URI of this attribute: that of its prefix when the name has a known
102
+ * one (e.g. `xlink:href`), otherwise `null`. An attribute without a prefix has no namespace
103
+ * whichever element it is on, and so does a spread attribute.
102
104
  *
103
105
  * @implements DOM API: `Attr`
104
106
  * @see https://dom.spec.whatwg.org/#ref-for-dom-attr-namespaceuri
@@ -90,7 +90,7 @@ export class MLAttr extends MLNode {
90
90
  super(astToken, ownElement.ownerMLDocument);
91
91
  this.ownerElement = ownElement;
92
92
  if (this._astToken.type === 'spread') {
93
- this.#namespaceURI = ownElement.namespaceURI;
93
+ this.#namespaceURI = null;
94
94
  this.valueType = 'code';
95
95
  this.#localName = '#spread';
96
96
  this.#potentialName = '#spread';
@@ -158,7 +158,12 @@ export class MLAttr extends MLNode {
158
158
  }
159
159
  const ns = resolveNamespace(this.#potentialName, ownElement.namespaceURI);
160
160
  this.#localName = ns.localName;
161
- this.#namespaceURI = ns.namespaceURI;
161
+ // An attribute does not inherit the namespace of its element, so only a known prefix
162
+ // gives it one. `ns.namespace` equals the prefix exactly when the prefix is a known
163
+ // one; for an unknown prefix it falls back to the element's namespace.
164
+ const hasPrefix = ns.localName !== this.#potentialName;
165
+ const prefix = this.#potentialName.split(':')[0];
166
+ this.#namespaceURI = hasPrefix && ns.namespace === prefix ? ns.namespaceURI : null;
162
167
  }
163
168
  /**
164
169
  * Returns the local name portion of the attribute (without namespace prefix).
@@ -179,7 +184,9 @@ export class MLAttr extends MLNode {
179
184
  return this.#potentialName;
180
185
  }
181
186
  /**
182
- * Returns the namespace URI of this attribute, resolved from the attribute name.
187
+ * Returns the namespace URI of this attribute: that of its prefix when the name has a known
188
+ * one (e.g. `xlink:href`), otherwise `null`. An attribute without a prefix has no namespace
189
+ * whichever element it is on, and so does a spread attribute.
183
190
  *
184
191
  * @implements DOM API: `Attr`
185
192
  * @see https://dom.spec.whatwg.org/#ref-for-dom-attr-namespaceuri
@@ -36,16 +36,26 @@ export declare class MLElement<T extends RuleConfigValue, O extends PlainData =
36
36
  readonly elementType: ElementType;
37
37
  /**
38
38
  * Whether this element belongs to a non-HTML namespace (e.g., SVG or MathML).
39
+ *
40
+ * Unlike {@link namespaceURI}, it does not follow the pretender: it says how the tag
41
+ * written here is parsed (whether the case of its name is kept, whether `/>` closes
42
+ * it), and the tag of a component is not parsed as the element it renders. An element
43
+ * given to a component as a child is, as it ends up inside what the component renders.
39
44
  */
40
- readonly isForeignElement: boolean;
45
+ get isForeignElement(): boolean;
41
46
  /**
42
47
  * Whether this element was implicitly created (e.g., an omitted `<body>` tag in HTML).
43
48
  */
44
49
  readonly isOmitted: boolean;
45
50
  /**
46
51
  * The namespace URI of this element (e.g., `http://www.w3.org/1999/xhtml` for HTML elements).
52
+ *
53
+ * Like {@link localName}, it follows the pretender, so that a namespaced selector
54
+ * (`svg|svg`) and a spec lookup see the element the component renders. An element
55
+ * given to a component as a child takes the namespace of where the component renders
56
+ * it, resolved when the document pretends.
47
57
  */
48
- readonly namespaceURI: NamespaceURI;
58
+ get namespaceURI(): NamespaceURI;
49
59
  readonly ontouchcancel?: ((this: GlobalEventHandlers, ev: TouchEvent) => any) | null | undefined;
50
60
  readonly ontouchend?: ((this: GlobalEventHandlers, ev: TouchEvent) => any) | null | undefined;
51
61
  readonly ontouchmove?: ((this: GlobalEventHandlers, ev: TouchEvent) => any) | null | undefined;
@@ -1,6 +1,8 @@
1
1
  /* global StylePropertyMap, StylePropertyMapReadOnly */
2
2
  var _a;
3
3
  import { getSpecByTagName, resolveNamespace } from '@markuplint/ml-spec';
4
+ import { pretenderNamespaceToURI } from '@markuplint/ml-config';
5
+ import { getChildNamespace } from '@markuplint/parser-utils';
4
6
  import { matchSelector } from '@markuplint/selector';
5
7
  import { getAccname } from '../helper/accname.js';
6
8
  import { after, before, nextElementSibling, previousElementSibling, remove, replaceWith, } from '../manipulations/child-node-methods.js';
@@ -38,8 +40,15 @@ export class MLElement extends MLParentNode {
38
40
  #getChildElementsAndTextNodeWithoutWhitespacesCache = null;
39
41
  /**
40
42
  * Whether this element belongs to a non-HTML namespace (e.g., SVG or MathML).
43
+ *
44
+ * Unlike {@link namespaceURI}, it does not follow the pretender: it says how the tag
45
+ * written here is parsed (whether the case of its name is kept, whether `/>` closes
46
+ * it), and the tag of a component is not parsed as the element it renders. An element
47
+ * given to a component as a child is, as it ends up inside what the component renders.
41
48
  */
42
- isForeignElement;
49
+ get isForeignElement() {
50
+ return this.#namespaceURI !== HTML_NAMESPACE;
51
+ }
43
52
  /**
44
53
  * Whether this element was implicitly created (e.g., an omitted `<body>` tag in HTML).
45
54
  */
@@ -47,8 +56,19 @@ export class MLElement extends MLParentNode {
47
56
  #localName;
48
57
  /**
49
58
  * The namespace URI of this element (e.g., `http://www.w3.org/1999/xhtml` for HTML elements).
59
+ *
60
+ * Like {@link localName}, it follows the pretender, so that a namespaced selector
61
+ * (`svg|svg`) and a spec lookup see the element the component renders. An element
62
+ * given to a component as a child takes the namespace of where the component renders
63
+ * it, resolved when the document pretends.
50
64
  */
51
- namespaceURI;
65
+ get namespaceURI() {
66
+ if (this.pretenderContext?.type === 'pretender') {
67
+ return this.pretenderContext.as.namespaceURI;
68
+ }
69
+ return this.#namespaceURI;
70
+ }
71
+ #namespaceURI;
52
72
  /**
53
73
  * ## Why this cache exists
54
74
  *
@@ -122,10 +142,9 @@ export class MLElement extends MLParentNode {
122
142
  : null;
123
143
  this.closeTag = pairAstNode ? new MLElementCloseTag(pairAstNode, document, this) : null;
124
144
  const ns = resolveNamespace(astNode.nodeName, astNode.namespace);
125
- this.namespaceURI = astNode.namespace;
145
+ this.#namespaceURI = astNode.namespace;
126
146
  this.elementType = astNode.elementType;
127
147
  this.#localName = ns.localName;
128
- this.isForeignElement = this.namespaceURI !== HTML_NAMESPACE;
129
148
  this.isOmitted = astNode.isGhost;
130
149
  this.tagOpenChar = astNode.tagOpenChar;
131
150
  this.tagCloseChar = astNode.tagCloseChar;
@@ -2861,6 +2880,101 @@ export class MLElement extends MLParentNode {
2861
2880
  }
2862
2881
  return matched;
2863
2882
  }
2883
+ /**
2884
+ * The element this element is rendered in: when the parent is a component, the
2885
+ * element the component wraps its children with (the slot wrapper, or the element
2886
+ * it renders), otherwise the parent. `null` at the top level. `undefined` when the
2887
+ * parent is a component with more than one slot that give the children different
2888
+ * namespaces.
2889
+ *
2890
+ * A custom element is not a component here: it puts the children in its light DOM,
2891
+ * where they keep the namespace they are parsed in.
2892
+ *
2893
+ * The parent has pretended already: `MLDocument` calls `pretending()` in
2894
+ * document order.
2895
+ */
2896
+ #getRenderedParent() {
2897
+ const parent = this.parentElement;
2898
+ if (parent?.pretenderContext?.type !== 'pretender' || parent.elementType === 'web-component') {
2899
+ return parent;
2900
+ }
2901
+ const { as, slotContent } = parent.pretenderContext;
2902
+ if (!slotContent) {
2903
+ return as;
2904
+ }
2905
+ return slotContent.wrapper ?? parent.#sharedWrapper ?? undefined;
2906
+ }
2907
+ /**
2908
+ * For a component with more than one slot, whose wrapper of each child is unknown, a
2909
+ * wrapper that gives the children the same namespace as all the others do, so that
2910
+ * the children can still be resolved. `null` when they differ (`<svg>` and the `<div>`
2911
+ * inside its `foreignObject`).
2912
+ */
2913
+ #sharedWrapper = null;
2914
+ #findSharedWrapper(slots, as) {
2915
+ const wrappers = slots.map((slot, i) => this.#createVirtualElement(slot, `${this.uuid}_pretender_slot_${i}`, this.uuid, as));
2916
+ const [first] = wrappers;
2917
+ if (!first) {
2918
+ return null;
2919
+ }
2920
+ const namespace = first.#getChildNamespace(null);
2921
+ return wrappers.every(wrapper => wrapper.#getChildNamespace(null) === namespace) ? first : null;
2922
+ }
2923
+ /**
2924
+ * The namespace of an element named `name` rendered as a child of this element, from
2925
+ * its own resolved namespace rather than the one of what it pretends to be.
2926
+ * `null` takes the namespace this element gives any element.
2927
+ */
2928
+ #getChildNamespace(name) {
2929
+ return getChildNamespace(name, this.#namespaceURI, this._astToken.nodeName);
2930
+ }
2931
+ /**
2932
+ * Re-resolves the namespace for the element this element is rendered in.
2933
+ *
2934
+ * The parser gives the children written inside a component the namespace of where
2935
+ * the component is written, as it does not know what the component renders: in
2936
+ * `<Icon><path /></Icon>`, where `Icon` renders `<svg>`, `path` is an HTML element
2937
+ * to it. Its namespace is kept while the parent is where the parser put it, so that
2938
+ * where no component is involved, the namespace stays what the parser gave, and when
2939
+ * where the component renders it is unknown.
2940
+ *
2941
+ * `getChildNamespace` leaves foreign content only at `foreignObject`, so an element
2942
+ * re-resolved here does not follow the other integration points (`<mi>`, `<desc>`),
2943
+ * nor the HTML elements that break out of foreign content in an HTML document (`<div>`
2944
+ * inside `<svg>`).
2945
+ *
2946
+ * A component is given no name, so that its own name is not taken for an element:
2947
+ * `getChildNamespace` compares names in lowercase, so `<Math>` would enter MathML.
2948
+ * The parsers of this repository do the same, but the AST may come from a parser
2949
+ * that does not.
2950
+ */
2951
+ #resolveNamespace(renderedParent) {
2952
+ const parent = this.parentElement;
2953
+ if (!parent || !renderedParent) {
2954
+ return;
2955
+ }
2956
+ if (renderedParent === parent && parent.#namespaceURI === parent._astToken.namespace) {
2957
+ return;
2958
+ }
2959
+ this.#namespaceURI = renderedParent.#getChildNamespace(this.elementType === 'authored' ? null : this._astToken.nodeName);
2960
+ }
2961
+ /**
2962
+ * The namespace of an element named `name` rendered where this element is, for the
2963
+ * element this element pretends to be.
2964
+ *
2965
+ * Where this element is has the namespace of this element, so the element gets the
2966
+ * namespace an element written there gets. For a component, it is taken from the
2967
+ * parent instead, as a parser that does not keep the name of a component out of
2968
+ * the namespace may have taken it for an element (`<Svg>`, `<Math>`: the names are
2969
+ * compared in lowercase).
2970
+ */
2971
+ #getNamespaceInPlace(name, renderedParent) {
2972
+ let here = this.#namespaceURI;
2973
+ if (this.elementType === 'authored' && renderedParent !== undefined) {
2974
+ here = renderedParent ? renderedParent.#getChildNamespace(null) : HTML_NAMESPACE;
2975
+ }
2976
+ return getChildNamespace(name, here, null);
2977
+ }
2864
2978
  /**
2865
2979
  * Initializes the pretender context for this element based on the given pretender
2866
2980
  * configurations. If a matching pretender is found or the element has an `as` attribute,
@@ -2869,6 +2983,8 @@ export class MLElement extends MLParentNode {
2869
2983
  * @param pretenders - Optional array of pretender configurations to match against
2870
2984
  */
2871
2985
  pretending(pretenders) {
2986
+ const renderedParent = this.#getRenderedParent();
2987
+ this.#resolveNamespace(renderedParent);
2872
2988
  // Pretender must not apply to a recognised standard HTML element (e.g. <marquee>,
2873
2989
  // <h1>, <button>). Allowing such elements to masquerade as another would silently
2874
2990
  // mask spec-driven rules — deprecation, ARIA role restrictions, browser support —
@@ -2896,7 +3012,6 @@ export class MLElement extends MLParentNode {
2896
3012
  return;
2897
3013
  }
2898
3014
  let nodeName;
2899
- let namespace = 'http://www.w3.org/1999/xhtml';
2900
3015
  const attributes = [];
2901
3016
  let aria;
2902
3017
  if (typeof pretenderElement === 'string') {
@@ -2904,7 +3019,6 @@ export class MLElement extends MLParentNode {
2904
3019
  }
2905
3020
  else {
2906
3021
  nodeName = pretenderElement.element;
2907
- namespace = pretenderElement.namespace === 'svg' ? 'http://www.w3.org/2000/svg' : namespace;
2908
3022
  if (pretenderElement.inheritAttrs) {
2909
3023
  attributes.push(...this._astToken.attributes);
2910
3024
  }
@@ -2914,6 +3028,9 @@ export class MLElement extends MLParentNode {
2914
3028
  aria = pretenderElement.aria;
2915
3029
  }
2916
3030
  const slots = typeof pretenderElement === 'string' ? undefined : pretenderElement.slots;
3031
+ const namespace = typeof pretenderElement !== 'string' && pretenderElement.namespace
3032
+ ? pretenderNamespaceToURI(pretenderElement.namespace)
3033
+ : this.#getNamespaceInPlace(nodeName, renderedParent);
2917
3034
  const as = new _a({
2918
3035
  ...this._astToken,
2919
3036
  uuid: this.uuid + '_pretender',
@@ -2935,7 +3052,7 @@ export class MLElement extends MLParentNode {
2935
3052
  type: 'origin',
2936
3053
  origin: this,
2937
3054
  };
2938
- const slotContent = typeof pretenderElement === 'string' ? undefined : this.#createSlotContent(pretenderElement, as, namespace);
3055
+ const slotContent = typeof pretenderElement === 'string' ? undefined : this.#createSlotContent(pretenderElement, as);
2939
3056
  this.pretenderContext = {
2940
3057
  type: 'pretender',
2941
3058
  as,
@@ -3032,7 +3149,7 @@ export class MLElement extends MLParentNode {
3032
3149
  * constructor would otherwise re-register the origin's real close tag under
3033
3150
  * the virtual element.
3034
3151
  */
3035
- #createVirtualElement(spec, uuid, parentNodeUuid, fallbackNamespace) {
3152
+ #createVirtualElement(spec, uuid, parentNodeUuid, renderedIn) {
3036
3153
  const attributes = [];
3037
3154
  if (spec.inheritAttrs) {
3038
3155
  attributes.push(...this._astToken.attributes);
@@ -3045,7 +3162,9 @@ export class MLElement extends MLParentNode {
3045
3162
  uuid,
3046
3163
  raw: `<${spec.element}>`,
3047
3164
  nodeName: spec.element,
3048
- namespace: getVirtualNamespace(spec, fallbackNamespace),
3165
+ namespace: spec.namespace
3166
+ ? pretenderNamespaceToURI(spec.namespace)
3167
+ : renderedIn.#getChildNamespace(spec.element),
3049
3168
  elementType: spec.element.includes('-') ? 'web-component' : 'html',
3050
3169
  attributes,
3051
3170
  childNodes: [],
@@ -3064,7 +3183,7 @@ export class MLElement extends MLParentNode {
3064
3183
  * the children, so that the children given at the usage site stay the whole
3065
3184
  * content of the outermost element (the bare-string pretender behavior).
3066
3185
  */
3067
- #createSlotContent(node, as, namespace) {
3186
+ #createSlotContent(node, as) {
3068
3187
  const { slots } = node;
3069
3188
  let wrapper = as;
3070
3189
  let contents = node.contents;
@@ -3073,20 +3192,23 @@ export class MLElement extends MLParentNode {
3073
3192
  if (slots.length > 1 || !slot) {
3074
3193
  // The wrapper of each child is unknown, so the children are not validated,
3075
3194
  // but a slot may still render unknown content (which may be a name).
3195
+ this.#sharedWrapper = this.#findSharedWrapper(slots, as);
3076
3196
  return {
3077
3197
  wrapper: null,
3078
3198
  mutable: slots.some(s => s.contents?.some(entry => 'dynamic' in entry) ?? false),
3079
3199
  fill: (given) => given,
3080
3200
  };
3081
3201
  }
3082
- wrapper = this.#createVirtualElement(slot, `${this.uuid}_pretender_slot`, this.uuid, namespace);
3202
+ // The elements between the root and the wrapper are not known, so the wrapper is
3203
+ // taken to be a child of the root unless the definition states its namespace.
3204
+ wrapper = this.#createVirtualElement(slot, `${this.uuid}_pretender_slot`, this.uuid, as);
3083
3205
  contents = slot.contents ?? [{ slot: true }];
3084
3206
  }
3085
3207
  if (!contents) {
3086
3208
  return undefined;
3087
3209
  }
3088
3210
  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)
3211
+ ? this.#createVirtualElement(entry, `${this.uuid}_pretender_content_${i}`, wrapper === as ? this.uuid : wrapper.uuid, wrapper)
3090
3212
  : entry);
3091
3213
  return {
3092
3214
  wrapper,
@@ -3372,17 +3494,3 @@ export class MLElement extends MLParentNode {
3372
3494
  }
3373
3495
  }
3374
3496
  _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
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markuplint/ml-core",
3
- "version": "5.1.1",
3
+ "version": "5.2.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.1.1",
38
- "@markuplint/html-parser": "5.1.1",
39
- "@markuplint/html-spec": "5.1.1",
37
+ "@markuplint/config-presets": "5.2.0",
38
+ "@markuplint/html-parser": "5.2.0",
39
+ "@markuplint/html-spec": "5.2.0",
40
40
  "@markuplint/i18n": "5.1.1",
41
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
- "@types/debug": "4.1.13",
48
- "debug": "4.4.3",
49
- "is-plain-object": "5.0.0",
50
- "type-fest": "5.6.0"
51
- },
52
- "gitHead": "a940c8526e971d590d97395ce924910221208820"
42
+ "@markuplint/ml-config": "5.2.0",
43
+ "@markuplint/ml-spec": "5.2.0",
44
+ "@markuplint/parser-utils": "5.2.0",
45
+ "@markuplint/selector": "5.2.0",
46
+ "@markuplint/shared": "5.2.0",
47
+ "@types/debug": "^4.1.13",
48
+ "debug": "^4.4.3",
49
+ "is-plain-object": "^5.1.0",
50
+ "type-fest": "^5.10.0"
51
+ },
52
+ "gitHead": "d8b2feab77c9c862513615bea6b80d2817431878"
53
53
  }