@markuplint/ml-core 5.5.0 → 5.6.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.
@@ -1,8 +1,9 @@
1
1
  import { exchangeValueOnRule, mergeRule } from '@markuplint/ml-config';
2
- import { schemaToSpec, getComputedRole, mayBeFocusable, getComputedAriaProps, isExposed, getSpecByTagName, ARIA_RECOMMENDED_VERSION, } from '@markuplint/ml-spec';
2
+ import { schemaToSpec, getComputedRole, mayBeFocusable, getComputedAriaProps, isExposed, isShadowRoot, getSpecByTagName, ARIA_RECOMMENDED_VERSION, } from '@markuplint/ml-spec';
3
3
  import { ConfigParserError, isComponentName } from '@markuplint/parser-utils';
4
4
  import { InvalidSelectorError } from '@markuplint/selector';
5
5
  import { log as coreLog } from '../../debug.js';
6
+ import { searchNodeAtLocation } from '../../utils/search-node-at-location.js';
6
7
  import { createNode } from '../helper/create-node.js';
7
8
  import { nodeListToDebugMaps } from '../helper/debug.js';
8
9
  import { sequentialWalker, syncWalk } from '../helper/walkers.js';
@@ -48,6 +49,11 @@ export class MLDocument extends MLParentNode {
48
49
  * this.
49
50
  */
50
51
  literalValues;
52
+ /**
53
+ * What the `slot` element is in the language of the document (`MLParser#slotElement`): the HTML
54
+ * element, or the placeholder of a framework's component, whose slot assignment is not computed.
55
+ */
56
+ slotElement;
51
57
  /**
52
58
  * The rule currently being evaluated during a lint pass.
53
59
  * Set by the linting engine before rule evaluation begins and used by nodes
@@ -159,6 +165,7 @@ export class MLDocument extends MLParentNode {
159
165
  this.booleanish = options?.booleanish ?? false;
160
166
  this.attrExpressionLanguage = options?.attrExpressionLanguage;
161
167
  this.literalValues = options?.literalValues;
168
+ this.slotElement = options?.slotElement ?? 'html';
162
169
  this.endTag = options?.endTag ?? 'omittable';
163
170
  this.#filename = options?.filename;
164
171
  this.tagNameCaseSensitive = options?.tagNameCaseSensitive ?? false;
@@ -179,8 +186,10 @@ export class MLDocument extends MLParentNode {
179
186
  return createNode(astNode, this);
180
187
  })
181
188
  .filter((n) => !!n));
182
- this.#pretending(options?.pretenders);
189
+ // A selector of the configuration is matched here, by a pretender and by a rule, so an
190
+ // invalid one is the fault of the configuration in both.
183
191
  try {
192
+ this.#pretending(options?.pretenders);
184
193
  this.#ruleMapping(ruleset);
185
194
  }
186
195
  catch (error) {
@@ -2133,7 +2142,16 @@ export class MLDocument extends MLParentNode {
2133
2142
  const focusable = mayBeFocusable(node, node.ownerMLDocument.specs);
2134
2143
  const nameRequired = role.role?.accessibleNameRequired ?? false;
2135
2144
  const nameProhibited = role.role?.accessibleNameProhibited ?? false;
2136
- const hasSlot = !!node.querySelector('slot');
2145
+ // A slot whose assignment is known is read by `getAccessibleName` above. The slot of a shadow root is in
2146
+ // a tree of its own, so every tree is searched, except the content of an ordinary `template` element,
2147
+ // which is not rendered where it is written.
2148
+ const hasSlot = node.deepQuerySelectorAll('slot').some(slot => {
2149
+ if (slot.getAssignedNodes() !== null) {
2150
+ return false;
2151
+ }
2152
+ const root = slot.getRootNode();
2153
+ return isShadowRoot(root) || !root.is(root.DOCUMENT_FRAGMENT_NODE);
2154
+ });
2137
2155
  aria.role = role.role?.name;
2138
2156
  aria.nameRequired = nameRequired;
2139
2157
  aria.nameProhibited = nameProhibited;
@@ -2338,18 +2356,17 @@ export class MLDocument extends MLParentNode {
2338
2356
  /**
2339
2357
  * Searches for a node at the given source location (line and column).
2340
2358
  *
2359
+ * The node is the innermost one whose source range, from its start, included,
2360
+ * to its end, excluded, holds the position; a position in an end tag is its
2361
+ * element. The rule is `searchNodeAtLocation`'s.
2362
+ *
2341
2363
  * @implements `@markuplint/ml-core` API: `MLDocument`
2342
2364
  * @param line - The 1-based line number to search at
2343
2365
  * @param col - The 1-based column number to search at
2344
2366
  * @returns The node at the given location, or null if not found
2345
2367
  */
2346
2368
  searchNodeByLocation(line, col) {
2347
- for (const node of this.nodeList) {
2348
- if (node.startLine <= line && line <= node.endLine && node.startCol <= col && col <= node.endCol) {
2349
- return node;
2350
- }
2351
- }
2352
- return null;
2369
+ return searchNodeAtLocation(this.nodeList, line, col);
2353
2370
  }
2354
2371
  /**
2355
2372
  * Sets the currently active rule for this document. Called by the linting engine
@@ -1,4 +1,5 @@
1
1
  import type { DeclarativeShadowRootAttachment } from '../helper/tree-scope.js';
2
+ import type { MLChildNode } from './child-node.js';
2
3
  import type { MLDocument } from './document.js';
3
4
  import type { MLNamedNodeMap } from './named-node-map.js';
4
5
  import type { MLNode } from './node.js';
@@ -604,9 +605,10 @@ export declare class MLElement<T extends RuleConfigValue, O extends PlainData =
604
605
  */
605
606
  get ariaValueText(): string | null;
606
607
  /**
607
- * **IT THROWS AN ERROR WHEN CALLING THIS.**
608
+ * The slot of an open declarative shadow root that this element is assigned to, or `null` when no
609
+ * slot takes it, the root is closed (the DOM does not expose it), or the assignment is not known
610
+ * (see {@link MLElement.getFlatTreeSlot}).
608
611
  *
609
- * @unsupported
610
612
  * @implements DOM API: `Element`
611
613
  * @see https://dom.spec.whatwg.org/#dom-slotable-assignedslot
612
614
  */
@@ -1938,9 +1940,35 @@ export declare class MLElement<T extends RuleConfigValue, O extends PlainData =
1938
1940
  */
1939
1941
  animate(keyframes: Keyframe[] | PropertyIndexedKeyframes | null, options?: number | KeyframeAnimationOptions): Animation;
1940
1942
  /**
1943
+ * The nodes this `slot` places, in tree order, or with `flatten` what it renders in the flat tree. It
1944
+ * is empty when the assignment is not known; use {@link MLElement.getAssignedNodes} to tell the two apart.
1945
+ *
1946
+ * @implements DOM API: `HTMLSlotElement`
1941
1947
  * @see https://html.spec.whatwg.org/multipage/scripting.html#dom-slot-assignednodes
1942
1948
  */
1943
- assignedNodes(): never[];
1949
+ assignedNodes(options?: AssignedNodesOptions): MLChildNode<T, O>[];
1950
+ /**
1951
+ * The nodes this `slot` places, by the named slot assignment of the declarative shadow root it is in.
1952
+ * A slot that takes none has `[]`, and renders its own children. `null` when the assignment is not
1953
+ * known; what makes it so is in the module comment of `helper/slot-assignment.ts`.
1954
+ *
1955
+ * @implements `@markuplint/ml-core` API: `MLElement`
1956
+ * @see https://dom.spec.whatwg.org/#find-slotables
1957
+ */
1958
+ getAssignedNodes(): readonly MLChildNode<T, O>[] | null;
1959
+ /**
1960
+ * The slot that places this element in the flat tree: the slot of the shadow root of its parent that
1961
+ * takes it. `null` when its parent is a shadow host and no slot takes it, so that it is not rendered;
1962
+ * `undefined` when its parent is not a shadow host, or the assignment is not known
1963
+ * (see {@link MLElement.getAssignedNodes}).
1964
+ *
1965
+ * `getComposedParentElement` of `@markuplint/ml-spec` reads it as the parent that the accessibility
1966
+ * tree has for the element.
1967
+ *
1968
+ * @implements `@markuplint/ml-core` API: `MLElement`
1969
+ * @see https://dom.spec.whatwg.org/#find-a-slot
1970
+ */
1971
+ getFlatTreeSlot(): MLElement<T, O> | null | undefined;
1944
1972
  /**
1945
1973
  * **IT THROWS AN ERROR WHEN CALLING THIS.**
1946
1974
  *
@@ -2229,6 +2257,21 @@ export declare class MLElement<T extends RuleConfigValue, O extends PlainData =
2229
2257
  * @returns True if any attribute is dynamic or lacks a name node (spread)
2230
2258
  */
2231
2259
  hasMutableAttributes(): boolean;
2260
+ /**
2261
+ * Checks whether this element is a component whose rendered output is not known: the
2262
+ * parser sees it as a component (`elementType` is `'authored'`) and no pretender stands
2263
+ * for an element of HTML, which includes a pretender whose own target is a component
2264
+ * (the chain of `pretenders.auto` stopped at `Tabs.Root`).
2265
+ *
2266
+ * A web component is not one: it is the element that it is written as. The criterion of
2267
+ * `isTreatedAsCustom` in `@markuplint/rules` (`permitted-contents`) includes it, because
2268
+ * a content model accepts a custom element wherever it accepts flow content; change them
2269
+ * together.
2270
+ *
2271
+ * @implements `@markuplint/ml-core` API: `MLElement`
2272
+ * @returns True if what this element renders is not known
2273
+ */
2274
+ isUnresolvedComponent(): boolean;
2232
2275
  /**
2233
2276
  * Checks whether this element has children that are potentially mutable,
2234
2277
  * such as preprocessor-specific blocks, slot elements, or (optionally) elements
@@ -2243,15 +2286,26 @@ export declare class MLElement<T extends RuleConfigValue, O extends PlainData =
2243
2286
  * A component that no pretender resolved renders something that is not known, however its
2244
2287
  * attributes are written (`size={size}`, `size={16}` and `size="16"` tell the same): a rule that
2245
2288
  * asks about what the descendants are or have (the accessible name, the owned elements) cannot decide
2246
- * on it, so it is mutable when `attr` is true. A component that a pretender resolved is the element
2247
- * that it renders, which the rules read themselves.
2289
+ * on it, so it is mutable when `attr` is true ({@link MLElement.isUnresolvedComponent}). A component
2290
+ * that a pretender resolved is the element that it renders, which the rules read themselves.
2291
+ *
2292
+ * A rule that asks which children an element has (the required children, the elements that
2293
+ * satisfy a query) does not pass `attr`: an attribute that cannot be known does not decide whether
2294
+ * a child exists. It looks for the unresolved components among the children itself.
2295
+ *
2296
+ * Every `slot` is mutable by default, as a `slot` that is not in a declarative shadow root renders what
2297
+ * is given to it, and a rule that reads the children as they are written (`require-datetime`) cannot
2298
+ * know it. A rule that reads what a slot of a shadow root renders by `MLElement#getAssignedNodes` says
2299
+ * which slots are with `isMutableSlot`.
2300
+ *
2248
2301
  *
2249
2302
  * @implements `@markuplint/ml-core` API: `MLElement`
2250
2303
  * @param attr - When true, also considers children with mutable attributes, and components that no
2251
2304
  * pretender resolved, as mutable
2305
+ * @param isMutableSlot - Whether a `slot` among the descendants may render what is not known
2252
2306
  * @returns True if this element has potentially mutable children
2253
2307
  */
2254
- hasMutableChildren(attr?: boolean): boolean;
2308
+ hasMutableChildren(attr?: boolean, isMutableSlot?: (slot: MLElement<T, O>) => boolean): boolean;
2255
2309
  /**
2256
2310
  * **IT THROWS AN ERROR WHEN CALLING THIS.**
2257
2311
  *
@@ -2,11 +2,12 @@
2
2
  var _a;
3
3
  import { getSpecByTagName, resolveNamespace } from '@markuplint/ml-spec';
4
4
  import { pretenderNamespaceToURI } from '@markuplint/ml-config';
5
- import { getChildNamespace } from '@markuplint/parser-utils';
6
- import { matchSelector, typeNameMatches } from '@markuplint/selector';
5
+ import { ConfigParserError, getChildNamespace } from '@markuplint/parser-utils';
6
+ import { InvalidSelectorError, matchSelector, typeNameMatches } from '@markuplint/selector';
7
7
  import { getAccname, getAccnameVisits, getHostLanguageLabelSource, getReferencedTextAlternative, } from '../helper/accname.js';
8
8
  import { literalToExpression } from '../helper/literal-values.js';
9
9
  import { markPretendedAttr } from '../helper/pretended-attrs.js';
10
+ import { getAssignedNodes, getAssignedSlot, getFlatTreeSlot, getFlattenedSlottables, } from '../helper/slot-assignment.js';
10
11
  import { getDeclarativeShadowRootAttachment, getDeclarativeShadowRootMode, getShadowRootTemplate, isHTMLTemplateElement, } from '../helper/tree-scope.js';
11
12
  import { after, before, nextElementSibling, previousElementSibling, remove, replaceWith, } from '../manipulations/child-node-methods.js';
12
13
  import { MLAttr } from './attr.js';
@@ -17,7 +18,7 @@ import { toNamedNodeMap } from './named-node-map.js';
17
18
  import { toHTMLCollection, toNodeList } from './node-list.js';
18
19
  import { MLParentNode } from './parent-node.js';
19
20
  import { MLShadowRoot } from './shadow-root.js';
20
- import { UnexpectedCallError, asciiLowerCase, vuePropKey } from '@markuplint/shared';
21
+ import { UnexpectedCallError, asciiLowerCase, isFatalError, vuePropKey } from '@markuplint/shared';
21
22
  const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml';
22
23
  /**
23
24
  * Represents a DOM Element node wrapper in the markuplint DOM tree.
@@ -835,14 +836,15 @@ export class MLElement extends MLParentNode {
835
836
  throw new UnexpectedCallError('Not supported "ariaValueText" property');
836
837
  }
837
838
  /**
838
- * **IT THROWS AN ERROR WHEN CALLING THIS.**
839
+ * The slot of an open declarative shadow root that this element is assigned to, or `null` when no
840
+ * slot takes it, the root is closed (the DOM does not expose it), or the assignment is not known
841
+ * (see {@link MLElement.getFlatTreeSlot}).
839
842
  *
840
- * @unsupported
841
843
  * @implements DOM API: `Element`
842
844
  * @see https://dom.spec.whatwg.org/#dom-slotable-assignedslot
843
845
  */
844
846
  get assignedSlot() {
845
- throw new UnexpectedCallError('Not supported "assignedSlot" property');
847
+ return getAssignedSlot(this);
846
848
  }
847
849
  /**
848
850
  * **IT THROWS AN ERROR WHEN CALLING THIS.**
@@ -2566,13 +2568,43 @@ export class MLElement extends MLParentNode {
2566
2568
  throw new UnexpectedCallError('Not supported "animate" method');
2567
2569
  }
2568
2570
  /**
2571
+ * The nodes this `slot` places, in tree order, or with `flatten` what it renders in the flat tree. It
2572
+ * is empty when the assignment is not known; use {@link MLElement.getAssignedNodes} to tell the two apart.
2573
+ *
2574
+ * @implements DOM API: `HTMLSlotElement`
2569
2575
  * @see https://html.spec.whatwg.org/multipage/scripting.html#dom-slot-assignednodes
2570
2576
  */
2571
- assignedNodes() {
2577
+ assignedNodes(options) {
2572
2578
  if (this.localName !== 'slot') {
2573
2579
  throw new TypeError('assignedNodes is not a function');
2574
2580
  }
2575
- return [];
2581
+ return [...(options?.flatten ? getFlattenedSlottables(this) : (this.getAssignedNodes() ?? []))];
2582
+ }
2583
+ /**
2584
+ * The nodes this `slot` places, by the named slot assignment of the declarative shadow root it is in.
2585
+ * A slot that takes none has `[]`, and renders its own children. `null` when the assignment is not
2586
+ * known; what makes it so is in the module comment of `helper/slot-assignment.ts`.
2587
+ *
2588
+ * @implements `@markuplint/ml-core` API: `MLElement`
2589
+ * @see https://dom.spec.whatwg.org/#find-slotables
2590
+ */
2591
+ getAssignedNodes() {
2592
+ return getAssignedNodes(this);
2593
+ }
2594
+ /**
2595
+ * The slot that places this element in the flat tree: the slot of the shadow root of its parent that
2596
+ * takes it. `null` when its parent is a shadow host and no slot takes it, so that it is not rendered;
2597
+ * `undefined` when its parent is not a shadow host, or the assignment is not known
2598
+ * (see {@link MLElement.getAssignedNodes}).
2599
+ *
2600
+ * `getComposedParentElement` of `@markuplint/ml-spec` reads it as the parent that the accessibility
2601
+ * tree has for the element.
2602
+ *
2603
+ * @implements `@markuplint/ml-core` API: `MLElement`
2604
+ * @see https://dom.spec.whatwg.org/#find-a-slot
2605
+ */
2606
+ getFlatTreeSlot() {
2607
+ return getFlatTreeSlot(this);
2576
2608
  }
2577
2609
  /**
2578
2610
  * **IT THROWS AN ERROR WHEN CALLING THIS.**
@@ -3036,6 +3068,26 @@ export class MLElement extends MLParentNode {
3036
3068
  }
3037
3069
  return false;
3038
3070
  }
3071
+ /**
3072
+ * Checks whether this element is a component whose rendered output is not known: the
3073
+ * parser sees it as a component (`elementType` is `'authored'`) and no pretender stands
3074
+ * for an element of HTML, which includes a pretender whose own target is a component
3075
+ * (the chain of `pretenders.auto` stopped at `Tabs.Root`).
3076
+ *
3077
+ * A web component is not one: it is the element that it is written as. The criterion of
3078
+ * `isTreatedAsCustom` in `@markuplint/rules` (`permitted-contents`) includes it, because
3079
+ * a content model accepts a custom element wherever it accepts flow content; change them
3080
+ * together.
3081
+ *
3082
+ * @implements `@markuplint/ml-core` API: `MLElement`
3083
+ * @returns True if what this element renders is not known
3084
+ */
3085
+ isUnresolvedComponent() {
3086
+ if (this.pretenderContext?.type === 'pretender') {
3087
+ return this.pretenderContext.as.elementType === 'authored';
3088
+ }
3089
+ return this.elementType === 'authored';
3090
+ }
3039
3091
  /**
3040
3092
  * Checks whether this element has children that are potentially mutable,
3041
3093
  * such as preprocessor-specific blocks, slot elements, or (optionally) elements
@@ -3050,15 +3102,26 @@ export class MLElement extends MLParentNode {
3050
3102
  * A component that no pretender resolved renders something that is not known, however its
3051
3103
  * attributes are written (`size={size}`, `size={16}` and `size="16"` tell the same): a rule that
3052
3104
  * asks about what the descendants are or have (the accessible name, the owned elements) cannot decide
3053
- * on it, so it is mutable when `attr` is true. A component that a pretender resolved is the element
3054
- * that it renders, which the rules read themselves.
3105
+ * on it, so it is mutable when `attr` is true ({@link MLElement.isUnresolvedComponent}). A component
3106
+ * that a pretender resolved is the element that it renders, which the rules read themselves.
3107
+ *
3108
+ * A rule that asks which children an element has (the required children, the elements that
3109
+ * satisfy a query) does not pass `attr`: an attribute that cannot be known does not decide whether
3110
+ * a child exists. It looks for the unresolved components among the children itself.
3111
+ *
3112
+ * Every `slot` is mutable by default, as a `slot` that is not in a declarative shadow root renders what
3113
+ * is given to it, and a rule that reads the children as they are written (`require-datetime`) cannot
3114
+ * know it. A rule that reads what a slot of a shadow root renders by `MLElement#getAssignedNodes` says
3115
+ * which slots are with `isMutableSlot`.
3116
+ *
3055
3117
  *
3056
3118
  * @implements `@markuplint/ml-core` API: `MLElement`
3057
3119
  * @param attr - When true, also considers children with mutable attributes, and components that no
3058
3120
  * pretender resolved, as mutable
3121
+ * @param isMutableSlot - Whether a `slot` among the descendants may render what is not known
3059
3122
  * @returns True if this element has potentially mutable children
3060
3123
  */
3061
- hasMutableChildren(attr = false) {
3124
+ hasMutableChildren(attr = false, isMutableSlot = () => true) {
3062
3125
  for (const child of this.getPureChildNodes()) {
3063
3126
  if (child.is(child.MARKUPLINT_PREPROCESSOR_BLOCK)) {
3064
3127
  if (child.blockBehavior) {
@@ -3070,13 +3133,13 @@ export class MLElement extends MLParentNode {
3070
3133
  if (attr && child.hasMutableAttributes()) {
3071
3134
  return true;
3072
3135
  }
3073
- if (attr && child.elementType === 'authored' && child.pretenderContext?.type !== 'pretender') {
3136
+ if (attr && child.isUnresolvedComponent()) {
3074
3137
  return true;
3075
3138
  }
3076
- if (child.hasMutableChildren(attr)) {
3139
+ if (child.hasMutableChildren(attr, isMutableSlot)) {
3077
3140
  return true;
3078
3141
  }
3079
- if (child.localName === 'slot') {
3142
+ if (child.localName === 'slot' && isMutableSlot(child)) {
3080
3143
  return true;
3081
3144
  }
3082
3145
  }
@@ -3335,7 +3398,19 @@ export class MLElement extends MLParentNode {
3335
3398
  if (this.elementType === 'html' && this.ownerMLDocument.isNativeTag(this.localName, this.namespaceURI)) {
3336
3399
  return;
3337
3400
  }
3338
- const pretenderConfig = pretenders?.find(option => this.matches(option.selector));
3401
+ const pretenderConfig = pretenders?.find(option => {
3402
+ try {
3403
+ return this.matches(option.selector);
3404
+ }
3405
+ catch (error) {
3406
+ // An entry that a scan or a file made names the file that declares its component, which is
3407
+ // where the user fixes it. `MLDocument` names the configuration for any other.
3408
+ if (!isFatalError(error) && error instanceof InvalidSelectorError && option.filePath) {
3409
+ throw new ConfigParserError(error.message, { filePath: option.filePath, raw: option.selector });
3410
+ }
3411
+ throw error;
3412
+ }
3413
+ });
3339
3414
  const asAttrValue = this.getAttribute('as');
3340
3415
  const pretenderElement = pretenderConfig?.as ??
3341
3416
  (this.elementType === 'html' || !asAttrValue
@@ -256,8 +256,8 @@ export declare abstract class MLNode<T extends RuleConfigValue, O extends PlainD
256
256
  * A `template` element that is in no tree has none either.
257
257
  *
258
258
  * The selector engine and `closest()` read it, so that `section p` does not match a `p` in the
259
- * content of a `template` in a `section`. {@link MLNode.parentElement} stays the syntactical one
260
- * because the content model of a `template` element is validated against what is in it.
259
+ * content of a `template` in a `section`. {@link MLNode.parentElement} does not stop at that
260
+ * boundary because the content model of a `template` element is validated against what is in it.
261
261
  *
262
262
  * `getTreeParentElement` of `@markuplint/ml-spec`, which cannot use this getter, derives the same
263
263
  * answer from `getRootNode()` for the algorithms of that package: a change of the boundary here
@@ -269,6 +269,10 @@ export declare abstract class MLNode<T extends RuleConfigValue, O extends PlainD
269
269
  /**
270
270
  * The parent node that may be `Element`, `Document`, `DocumentFragment`, and `null`.
271
271
  *
272
+ * A block of a template language is walked through (`MLBlock#isTransparent`): a node in a block
273
+ * has the node that contains the block as its parent. A node at the top of a fragment document
274
+ * has none.
275
+ *
272
276
  * ## HTML:
273
277
  *
274
278
  * ```html
@@ -289,10 +293,10 @@ export declare abstract class MLNode<T extends RuleConfigValue, O extends PlainD
289
293
  *
290
294
  * ```jsx
291
295
  * <> // => null
292
- * <div> // => #document-fragment
296
+ * <div> // => <>
293
297
  * {items.map(item => {
294
298
  * return (
295
- * <span /> // => null
299
+ * <span /> // => <div>
296
300
  * )
297
301
  * })}
298
302
  * </div>
@@ -356,7 +360,7 @@ export declare abstract class MLNode<T extends RuleConfigValue, O extends PlainD
356
360
  *
357
361
  * ```jsx
358
362
  * <> // => #document
359
- * <div> // => #document-fragment
363
+ * <div> // => <>
360
364
  * {items.map(item => {
361
365
  * return (
362
366
  * <span /> // => #ml-block
@@ -540,6 +544,9 @@ export declare abstract class MLNode<T extends RuleConfigValue, O extends PlainD
540
544
  */
541
545
  isEqualNode(otherNode: Node | null): boolean;
542
546
  /**
547
+ * Whether the document of this node is a fragment of a document (a partial, a component) and
548
+ * not a whole one. The parser of the document decides it (`MLDocument#isFragment`).
549
+ *
543
550
  * @implements `@markuplint/ml-core` API: `MLNode`
544
551
  */
545
552
  isInFragmentDocument(): boolean;
@@ -333,8 +333,8 @@ export class MLNode extends MLToken {
333
333
  * A `template` element that is in no tree has none either.
334
334
  *
335
335
  * The selector engine and `closest()` read it, so that `section p` does not match a `p` in the
336
- * content of a `template` in a `section`. {@link MLNode.parentElement} stays the syntactical one
337
- * because the content model of a `template` element is validated against what is in it.
336
+ * content of a `template` in a `section`. {@link MLNode.parentElement} does not stop at that
337
+ * boundary because the content model of a `template` element is validated against what is in it.
338
338
  *
339
339
  * `getTreeParentElement` of `@markuplint/ml-spec`, which cannot use this getter, derives the same
340
340
  * answer from `getRootNode()` for the algorithms of that package: a change of the boundary here
@@ -355,6 +355,10 @@ export class MLNode extends MLToken {
355
355
  /**
356
356
  * The parent node that may be `Element`, `Document`, `DocumentFragment`, and `null`.
357
357
  *
358
+ * A block of a template language is walked through (`MLBlock#isTransparent`): a node in a block
359
+ * has the node that contains the block as its parent. A node at the top of a fragment document
360
+ * has none.
361
+ *
358
362
  * ## HTML:
359
363
  *
360
364
  * ```html
@@ -375,10 +379,10 @@ export class MLNode extends MLToken {
375
379
  *
376
380
  * ```jsx
377
381
  * <> // => null
378
- * <div> // => #document-fragment
382
+ * <div> // => <>
379
383
  * {items.map(item => {
380
384
  * return (
381
- * <span /> // => null
385
+ * <span /> // => <div>
382
386
  * )
383
387
  * })}
384
388
  * </div>
@@ -511,7 +515,7 @@ export class MLNode extends MLToken {
511
515
  *
512
516
  * ```jsx
513
517
  * <> // => #document
514
- * <div> // => #document-fragment
518
+ * <div> // => <>
515
519
  * {items.map(item => {
516
520
  * return (
517
521
  * <span /> // => #ml-block
@@ -907,6 +911,9 @@ export class MLNode extends MLToken {
907
911
  throw new UnexpectedCallError('Not supported "isEqualNode" method');
908
912
  }
909
913
  /**
914
+ * Whether the document of this node is a fragment of a document (a partial, a component) and
915
+ * not a whole one. The parser of the document decides it (`MLDocument#isFragment`).
916
+ *
910
917
  * @implements `@markuplint/ml-core` API: `MLNode`
911
918
  */
912
919
  isInFragmentDocument() {
@@ -1,3 +1,4 @@
1
+ import type { MLElement } from './element.js';
1
2
  import type { TextNodeType } from './types.js';
2
3
  import type { MLASTText } from '@markuplint/ml-ast';
3
4
  import type { PlainData, RuleConfigValue } from '@markuplint/ml-config';
@@ -12,12 +13,20 @@ import { MLCharacterData } from './character-data.js';
12
13
  */
13
14
  export declare class MLText<T extends RuleConfigValue, O extends PlainData = undefined> extends MLCharacterData<T, O, MLASTText> implements Text {
14
15
  /**
15
- * **IT THROWS AN ERROR WHEN CALLING THIS.**
16
+ * The slot of an open declarative shadow root that this text is assigned to, or `null` when no
17
+ * slot takes it, the root is closed (the DOM does not expose it), or the assignment is not known.
16
18
  *
17
- * @unsupported
18
19
  * @implements DOM API: `Text`
20
+ * @see https://dom.spec.whatwg.org/#dom-slotable-assignedslot
21
+ */
22
+ get assignedSlot(): HTMLSlotElement | null;
23
+ /**
24
+ * The slot that places this text in the flat tree. See {@link MLElement.getFlatTreeSlot}.
25
+ *
26
+ * @implements `@markuplint/ml-core` API: `MLText`
27
+ * @see https://dom.spec.whatwg.org/#find-a-slot
19
28
  */
20
- get assignedSlot(): HTMLSlotElement;
29
+ getFlatTreeSlot(): MLElement<T, O> | null | undefined;
21
30
  /**
22
31
  * Returns `true` if this text node originated from an invalid (bogus) AST
23
32
  * node, such as an orphaned end tag.
@@ -1,3 +1,4 @@
1
+ import { getAssignedSlot, getFlatTreeSlot } from '../helper/slot-assignment.js';
1
2
  import { MLCharacterData } from './character-data.js';
2
3
  import { UnexpectedCallError } from '@markuplint/shared';
3
4
  /**
@@ -16,13 +17,23 @@ const rawTextElements = new Set(['script', 'style']);
16
17
  */
17
18
  export class MLText extends MLCharacterData {
18
19
  /**
19
- * **IT THROWS AN ERROR WHEN CALLING THIS.**
20
+ * The slot of an open declarative shadow root that this text is assigned to, or `null` when no
21
+ * slot takes it, the root is closed (the DOM does not expose it), or the assignment is not known.
20
22
  *
21
- * @unsupported
22
23
  * @implements DOM API: `Text`
24
+ * @see https://dom.spec.whatwg.org/#dom-slotable-assignedslot
23
25
  */
24
26
  get assignedSlot() {
25
- throw new UnexpectedCallError('Not supported "assignedSlot" property');
27
+ return getAssignedSlot(this);
28
+ }
29
+ /**
30
+ * The slot that places this text in the flat tree. See {@link MLElement.getFlatTreeSlot}.
31
+ *
32
+ * @implements `@markuplint/ml-core` API: `MLText`
33
+ * @see https://dom.spec.whatwg.org/#find-a-slot
34
+ */
35
+ getFlatTreeSlot() {
36
+ return getFlatTreeSlot(this);
26
37
  }
27
38
  /**
28
39
  * Returns `true` if this text node originated from an invalid (bogus) AST
package/lib/test/index.js CHANGED
@@ -22,6 +22,7 @@ export function createTestDocument(sourceCode, options) {
22
22
  booleanish: usedParser.booleanish,
23
23
  attrExpressionLanguage: usedParser.attrExpressionLanguage,
24
24
  literalValues: usedParser.literalValues,
25
+ slotElement: usedParser.slotElement,
25
26
  tagNameCaseSensitive: usedParser.tagNameCaseSensitive,
26
27
  componentNameVariants: usedParser.componentNameVariants,
27
28
  isComponentName: usedParser.isComponentName?.bind(usedParser),
@@ -1 +1,3 @@
1
1
  export { getLocationFromChars } from './get-location-from-chars.js';
2
+ export type { LocatedNode, LocatedToken } from './search-node-at-location.js';
3
+ export { searchNodeAtLocation } from './search-node-at-location.js';
@@ -1 +1,2 @@
1
1
  export { getLocationFromChars } from './get-location-from-chars.js';
2
+ export { searchNodeAtLocation } from './search-node-at-location.js';
@@ -0,0 +1,39 @@
1
+ /**
2
+ * A source range: from its start, included, to its end, excluded (the end is
3
+ * where the next token starts). Without an end, it holds only its start.
4
+ */
5
+ export interface LocatedToken {
6
+ readonly startLine: number;
7
+ readonly startCol: number;
8
+ readonly endLine?: number;
9
+ readonly endCol?: number;
10
+ }
11
+ /**
12
+ * A node with its source range. The range of an element is its start tag; its
13
+ * end tag, which is not in a document's node list, is `closeTag`.
14
+ */
15
+ export interface LocatedNode extends LocatedToken {
16
+ readonly closeTag?: LocatedToken | null;
17
+ }
18
+ /**
19
+ * Finds the node of `nodeList` at the source location `line`:`col` (both 1-based).
20
+ *
21
+ * The node is the one whose range holds the position, or whose end tag does: a
22
+ * document's node list has no attribute nodes and no end tags, so a position at
23
+ * an attribute, inside its value or in an end tag is the element that has it. Of
24
+ * several such nodes the last in the list is taken: the innermost. A block
25
+ * (`#ml-block`) holds the elements nested in it, and an omitted element
26
+ * (`<tbody>` with an empty source) starts where its first child does.
27
+ *
28
+ * The list is in document order, as the parser sorts its nodes by offset, so the
29
+ * search stops at the first node that starts after the position. It works on the
30
+ * structural `LocatedNode` so that it reads the node list of any version of the
31
+ * engine: the language server searches the document of the markuplint the user
32
+ * installed, and the suppressions search a node list cast to their own type.
33
+ *
34
+ * @param nodeList - The nodes of a document, in document order.
35
+ * @param line - The 1-based line number to search at.
36
+ * @param col - The 1-based column number to search at.
37
+ * @returns The node at the location, or `null` if there is none.
38
+ */
39
+ export declare function searchNodeAtLocation<N extends LocatedNode>(nodeList: Iterable<N>, line: number, col: number): N | null;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Finds the node of `nodeList` at the source location `line`:`col` (both 1-based).
3
+ *
4
+ * The node is the one whose range holds the position, or whose end tag does: a
5
+ * document's node list has no attribute nodes and no end tags, so a position at
6
+ * an attribute, inside its value or in an end tag is the element that has it. Of
7
+ * several such nodes the last in the list is taken: the innermost. A block
8
+ * (`#ml-block`) holds the elements nested in it, and an omitted element
9
+ * (`<tbody>` with an empty source) starts where its first child does.
10
+ *
11
+ * The list is in document order, as the parser sorts its nodes by offset, so the
12
+ * search stops at the first node that starts after the position. It works on the
13
+ * structural `LocatedNode` so that it reads the node list of any version of the
14
+ * engine: the language server searches the document of the markuplint the user
15
+ * installed, and the suppressions search a node list cast to their own type.
16
+ *
17
+ * @param nodeList - The nodes of a document, in document order.
18
+ * @param line - The 1-based line number to search at.
19
+ * @param col - The 1-based column number to search at.
20
+ * @returns The node at the location, or `null` if there is none.
21
+ */
22
+ export function searchNodeAtLocation(nodeList, line, col) {
23
+ let found = null;
24
+ for (const node of nodeList) {
25
+ if (isBefore(line, col, node)) {
26
+ break;
27
+ }
28
+ if (holdsLocation(node, line, col) || (node.closeTag != null && holdsLocation(node.closeTag, line, col))) {
29
+ found = node;
30
+ }
31
+ }
32
+ return found;
33
+ }
34
+ /** Whether `line`:`col` comes before the start of `token`. */
35
+ function isBefore(line, col, token) {
36
+ return line < token.startLine || (line === token.startLine && col < token.startCol);
37
+ }
38
+ /**
39
+ * Whether `line`:`col` is the start of `token` or in its range. The start is
40
+ * compared first: the end of an `MLNode` is computed from its source on every read.
41
+ */
42
+ function holdsLocation(token, line, col) {
43
+ if (isBefore(line, col, token)) {
44
+ return false;
45
+ }
46
+ if (line === token.startLine && col === token.startCol) {
47
+ return true;
48
+ }
49
+ if (token.endLine === undefined || token.endCol === undefined) {
50
+ return false;
51
+ }
52
+ return line < token.endLine || (line === token.endLine && col < token.endCol);
53
+ }