@markuplint/ml-ast 4.4.10-dev.350 → 4.4.11

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.
@@ -0,0 +1,485 @@
1
+ # Node Reference
2
+
3
+ Detailed reference for every AST node type defined in `@markuplint/ml-ast`.
4
+
5
+ ## Overview
6
+
7
+ All AST nodes use a **discriminated union** pattern based on the `type` field. This enables exhaustive type narrowing via TypeScript's `switch` statement:
8
+
9
+ ```typescript
10
+ import type { MLASTNode } from '@markuplint/ml-ast';
11
+
12
+ function handle(node: MLASTNode) {
13
+ switch (node.type) {
14
+ case 'doctype':
15
+ /* node is MLASTDoctype */ break;
16
+ case 'starttag':
17
+ /* node is MLASTElement */ break;
18
+ case 'endtag':
19
+ /* node is MLASTElementCloseTag */ break;
20
+ case 'comment':
21
+ /* node is MLASTComment */ break;
22
+ case 'text':
23
+ /* node is MLASTText */ break;
24
+ case 'psblock':
25
+ /* node is MLASTPreprocessorSpecificBlock */ break;
26
+ case 'invalid':
27
+ /* node is MLASTInvalid */ break;
28
+ case 'attr':
29
+ /* node is MLASTHTMLAttr */ break;
30
+ case 'spread':
31
+ /* node is MLASTSpreadAttr */ break;
32
+ }
33
+ }
34
+ ```
35
+
36
+ ## AST to MLDOM Mapping
37
+
38
+ Every AST node defined in this package is ultimately converted into an **MLDOM** node by `@markuplint/ml-core`. MLDOM is markuplint's DOM implementation that **conforms to the [DOM Standard](https://dom.spec.whatwg.org/)**. Each MLDOM class implements the corresponding DOM interface (`Node`, `Element`, `DocumentType`, `Comment`, `Text`, etc.) so that lint rules can use standard DOM APIs for inspection.
39
+
40
+ The mapping is performed by `createNode()` in `ml-core`:
41
+
42
+ | AST Type (`ml-ast`) | MLDOM Class (`ml-core`) | DOM Interface Implemented | `nodeType` |
43
+ | ----------------------------------- | ------------------------- | ------------------------- | ---------- |
44
+ | `MLASTDoctype` | `MLDocumentType` | `DocumentType` | `10` |
45
+ | `MLASTElement` | `MLElement` | `Element`, `HTMLElement` | `1` |
46
+ | `MLASTComment` | `MLComment` | `Comment` | `8` |
47
+ | `MLASTText` | `MLText` | `Text` | `3` |
48
+ | `MLASTPreprocessorSpecificBlock` | `MLBlock` | _(markuplint-specific)_ | `101` |
49
+ | `MLASTInvalid` (`kind: 'starttag'`) | `MLElement` (`x-invalid`) | `Element`, `HTMLElement` | `1` |
50
+ | `MLASTInvalid` (other) | `MLText` | `Text` | `3` |
51
+
52
+ ### Special Nodes
53
+
54
+ - **`MLASTElementCloseTag`** is **not** passed through `createNode()`. Instead, `MLElement` internally creates an `MLElementCloseTag` from its `pairNode` reference. `MLElementCloseTag` is not part of the DOM tree traversal; it exists only as a satellite of its paired element.
55
+ - **`MLASTPreprocessorSpecificBlock`** maps to `MLBlock`, which is a **markuplint-specific extension** with no DOM Standard equivalent. It uses a custom `nodeType` of `101` (beyond the DOM Standard range). `MLBlock` is transparent -- its children are treated as belonging to the parent node for tree traversal purposes.
56
+ - **`MLASTHTMLAttr`** and **`MLASTSpreadAttr`** map to `MLAttr`, which implements the DOM `Attr` interface (`nodeType: 2`). Attributes are accessed via `MLElement.attributes` (`MLNamedNodeMap`), not through `createNode()`.
57
+
58
+ ## Base Types
59
+
60
+ ### MLASTToken
61
+
62
+ The foundational interface for all positional information. Every AST node and sub-token extends this.
63
+
64
+ | Field | Type | Description |
65
+ | ------------- | -------- | ---------------------------------------------- |
66
+ | `uuid` | `string` | Unique identifier for this token instance |
67
+ | `raw` | `string` | The original raw source text |
68
+ | `startOffset` | `number` | Zero-based character offset of the token start |
69
+ | `endOffset` | `number` | Zero-based character offset of the token end |
70
+ | `startLine` | `number` | One-based line number where the token starts |
71
+ | `endLine` | `number` | One-based line number where the token ends |
72
+ | `startCol` | `number` | One-based column number where the token starts |
73
+ | `endCol` | `number` | One-based column number where the token ends |
74
+
75
+ **Coordinate system:** Offsets are zero-based (counting from 0), while lines and columns are one-based (counting from 1). This matches the conventions used by most text editors and error reporters.
76
+
77
+ ### MLASTAbstractNode
78
+
79
+ An internal (non-exported) base interface that extends `MLASTToken` with structural metadata. All concrete node types extend this.
80
+
81
+ | Field | Type | Description |
82
+ | ------------ | ------------------------- | ----------------------------------------------------------- |
83
+ | `type` | `MLASTNodeType` | Discriminant tag identifying the concrete node kind |
84
+ | `nodeName` | `string` | The node name (tag name, `#text`, `#comment`, etc.) |
85
+ | `parentNode` | `MLASTParentNode \| null` | Reference to the parent node, or `null` for top-level nodes |
86
+
87
+ ## MLASTDocument
88
+
89
+ **Role:** The root container returned by every parser. It is **not** a node in the AST tree itself, but rather the wrapper that holds the parse result.
90
+
91
+ | Field | Type | Description |
92
+ | ------------------- | ------------------------------ | ------------------------------------------------------------- |
93
+ | `raw` | `string` | The full original source code |
94
+ | `nodeList` | `readonly MLASTNodeTreeItem[]` | Flat list of top-level AST nodes in document order |
95
+ | `isFragment` | `boolean` | Whether the document is a fragment (no root element required) |
96
+ | `unknownParseError` | `string \| undefined` | A description of any unknown parse error |
97
+
98
+ **Important:** `nodeList` is a **flat list** of top-level nodes, not a tree. Child nodes are accessible via each element's `childNodes` property. The list contains nodes in document order (the order they appear in the source).
99
+
100
+ ## MLASTDoctype
101
+
102
+ **Type discriminant:** `'doctype'`
103
+
104
+ **Role:** Represents a DOCTYPE declaration (e.g., `<!DOCTYPE html>`). Always appears at the top level of the document.
105
+
106
+ | Field | Type | Description |
107
+ | ---------- | ----------- | ------------------------------------------------ |
108
+ | `type` | `'doctype'` | Discriminant tag |
109
+ | `depth` | `number` | Nesting depth (always 0 for DOCTYPE) |
110
+ | `name` | `string` | The declared document type name (e.g., `"html"`) |
111
+ | `publicId` | `string` | The public identifier of the DOCTYPE, if any |
112
+ | `systemId` | `string` | The system identifier of the DOCTYPE, if any |
113
+
114
+ **Example:**
115
+
116
+ ```html
117
+ <!DOCTYPE html>
118
+ ```
119
+
120
+ Produces a node with `name: "html"`, `publicId: ""`, `systemId: ""`.
121
+
122
+ ## MLASTElement
123
+
124
+ **Type discriminant:** `'starttag'`
125
+
126
+ **Role:** Represents an opening element tag (e.g., `<div class="foo">`). This is the primary element representation in the AST and is the most feature-rich node type. It owns child nodes, attributes, and maintains a reference to its matching closing tag.
127
+
128
+ | Field | Type | Description |
129
+ | -------------------- | ------------------------------ | --------------------------------------------------------------------------- |
130
+ | `type` | `'starttag'` | Discriminant tag |
131
+ | `depth` | `number` | Nesting depth in the document tree |
132
+ | `namespace` | `string` | Namespace URI (e.g., `"http://www.w3.org/1999/xhtml"`) |
133
+ | `elementType` | `ElementType` | Whether the element is `'html'`, `'web-component'`, or `'authored'` |
134
+ | `isFragment` | `boolean` | Whether the element acts as a fragment (e.g., React `<>`, Vue `<template>`) |
135
+ | `attributes` | `readonly MLASTAttr[]` | Attributes on this element |
136
+ | `hasSpreadAttr` | `boolean \| undefined` | Whether the element has one or more spread attributes |
137
+ | `childNodes` | `readonly MLASTChildNode[]` | Direct child nodes of this element |
138
+ | `pairNode` | `MLASTElementCloseTag \| null` | The matching closing tag, or `null` for void/self-closing elements |
139
+ | `selfClosingSolidus` | `MLASTToken \| undefined` | The self-closing solidus token (`/`), if present (e.g., `<br />`) |
140
+ | `tagOpenChar` | `string` | The characters that open this tag (usually `"<"`) |
141
+ | `tagCloseChar` | `string` | The characters that close this tag (usually `">"`) |
142
+ | `isGhost` | `boolean` | Whether this is a ghost node (omitted tag inferred by the parser) |
143
+
144
+ ### Element Type Classification
145
+
146
+ The `elementType` field classifies elements into three categories:
147
+
148
+ | Value | Description | Examples |
149
+ | ----------------- | ---------------------------------------------------------------- | ------------------------ |
150
+ | `'html'` | Native HTML element from the HTML Standard | `<div>`, `<span>`, `<p>` |
151
+ | `'web-component'` | Web Component according to the HTML Standard (contains a hyphen) | `<my-component>` |
152
+ | `'authored'` | Authored element through a view framework or template engine | `<MyComponent>` (JSX) |
153
+
154
+ ### Tag Delimiters
155
+
156
+ The `tagOpenChar` and `tagCloseChar` fields represent the actual characters that delimit the tag. For standard HTML these are `"<"` and `">"`, but template engines may use different delimiters.
157
+
158
+ ### Ghost Nodes (Omitted Tags)
159
+
160
+ When `isGhost` is `true`, the element was not explicitly written in the source but was inferred by the parser. In HTML, certain tags can be omitted (e.g., `<tbody>` inside `<table>`). Ghost nodes have an empty `raw` string.
161
+
162
+ ### Pair Node Relationship
163
+
164
+ The `pairNode` field creates a **bidirectional link** between opening and closing tags:
165
+
166
+ - `MLASTElement.pairNode` points to its `MLASTElementCloseTag`
167
+ - `MLASTElementCloseTag.pairNode` points back to the `MLASTElement`
168
+
169
+ For void elements (`<br>`, `<img>`, etc.) and self-closing elements, `pairNode` is `null`.
170
+
171
+ ### Fragment Elements
172
+
173
+ When `isFragment` is `true`, the element acts as a transparent wrapper with no actual DOM node. This is used for framework-specific constructs like React fragments (`<>...</>`) and Vue `<template>` wrappers.
174
+
175
+ **Example:**
176
+
177
+ ```html
178
+ <div class="container" id="main">
179
+ <p>Hello</p>
180
+ </div>
181
+ ```
182
+
183
+ The `<div>` produces an `MLASTElement` with:
184
+
185
+ - `nodeName: "div"`
186
+ - `elementType: "html"`
187
+ - `namespace: "http://www.w3.org/1999/xhtml"`
188
+ - `attributes`: array containing `class` and `id` attributes
189
+ - `childNodes`: array containing the `<p>` element and text nodes
190
+ - `pairNode`: reference to the `</div>` closing tag
191
+
192
+ ## MLASTElementCloseTag
193
+
194
+ **Type discriminant:** `'endtag'`
195
+
196
+ **Role:** Represents a closing element tag (e.g., `</div>`). Always paired with an `MLASTElement` via the `pairNode` field.
197
+
198
+ | Field | Type | Description |
199
+ | -------------- | -------------- | -------------------------------------------------- |
200
+ | `type` | `'endtag'` | Discriminant tag |
201
+ | `depth` | `number` | Nesting depth in the document tree |
202
+ | `parentNode` | `null` | Always `null` for closing tags |
203
+ | `pairNode` | `MLASTElement` | The matching opening element tag |
204
+ | `tagOpenChar` | `string` | The characters that open this tag (usually `"</"`) |
205
+ | `tagCloseChar` | `string` | The characters that close this tag (usually `">"`) |
206
+
207
+ **Why `parentNode` is always `null`:** In the AST model, only the opening tag (`MLASTElement`) participates in the parent-child tree structure. The closing tag exists as a separate node linked to the opening tag via `pairNode`, but it is not a child of any parent node. This avoids duplicating the element in the tree.
208
+
209
+ ## MLASTComment
210
+
211
+ **Type discriminant:** `'comment'`
212
+
213
+ **Role:** Represents an HTML comment (e.g., `<!-- ... -->`).
214
+
215
+ | Field | Type | Description |
216
+ | ---------- | ------------ | ---------------------------------------- |
217
+ | `type` | `'comment'` | Discriminant tag |
218
+ | `nodeName` | `'#comment'` | Always `'#comment'` |
219
+ | `depth` | `number` | Nesting depth in the document tree |
220
+ | `isBogus` | `boolean` | Whether the comment is bogus (malformed) |
221
+
222
+ ### Bogus Comments
223
+
224
+ When `isBogus` is `true`, the comment is malformed according to the HTML specification. Examples of bogus comments include:
225
+
226
+ - `<!...>` (not a valid DOCTYPE or comment)
227
+ - `<?xml version="1.0"?>` (processing instructions in HTML)
228
+
229
+ The parser still captures these as comment nodes but flags them as bogus so that lint rules can report them.
230
+
231
+ ## MLASTText
232
+
233
+ **Type discriminant:** `'text'`
234
+
235
+ **Role:** Represents character data between elements.
236
+
237
+ | Field | Type | Description |
238
+ | ---------- | --------- | ---------------------------------- |
239
+ | `type` | `'text'` | Discriminant tag |
240
+ | `nodeName` | `'#text'` | Always `'#text'` |
241
+ | `depth` | `number` | Nesting depth in the document tree |
242
+
243
+ The `raw` field (inherited from `MLASTToken`) contains the full text content, **including whitespace**. A text node between two elements may consist entirely of whitespace (newlines, indentation, etc.).
244
+
245
+ **Example:**
246
+
247
+ ```html
248
+ <p>Hello, world!</p>
249
+ ```
250
+
251
+ The text `Hello, world!` is represented as an `MLASTText` node with `raw: "Hello, world!"`.
252
+
253
+ ## MLASTPreprocessorSpecificBlock
254
+
255
+ **Type discriminant:** `'psblock'`
256
+
257
+ **Role:** Represents control-flow and iteration constructs from template engines and frameworks. These are syntax constructs that do not exist in standard HTML but are used by preprocessors like Svelte, Vue, EJS, ERB, and others.
258
+
259
+ | Field | Type | Description |
260
+ | ----------------- | ----------------------------------------------- | ----------------------------------------------------- |
261
+ | `type` | `'psblock'` | Discriminant tag |
262
+ | `conditionalType` | `MLASTPreprocessorSpecificBlockConditionalType` | The kind of conditional or iteration construct |
263
+ | `depth` | `number` | Nesting depth in the document tree |
264
+ | `nodeName` | `string` | The block's name as determined by the parser |
265
+ | `isFragment` | `boolean` | Whether this block acts as a transparent fragment |
266
+ | `childNodes` | `readonly MLASTChildNode[]` | Direct child nodes within this block |
267
+ | `isBogus` | `boolean` | Whether this block is bogus (unparsable or malformed) |
268
+
269
+ ### Conditional Type Values
270
+
271
+ The `conditionalType` field indicates the semantic role of the block:
272
+
273
+ | Value | Description | Example (Svelte) | Example (EJS/ERB) |
274
+ | ------------------ | ---------------------------------- | ----------------------- | ------------------- |
275
+ | `'if'` | Conditional branch (opening) | `{#if condition}` | `<% if (x) { %>` |
276
+ | `'if:elseif'` | Alternative conditional branch | `{:else if condition}` | `<% } else if { %>` |
277
+ | `'if:else'` | Default (else) branch | `{:else}` | `<% } else { %>` |
278
+ | `'switch:case'` | Switch case branch | -- | -- |
279
+ | `'switch:default'` | Switch default branch | -- | -- |
280
+ | `'each'` | Iteration (loop) block | `{#each items as item}` | `<% for (...) { %>` |
281
+ | `'each:empty'` | Empty state for an iteration block | `{:else}` (in `#each`) | -- |
282
+ | `'await'` | Asynchronous block (pending state) | `{#await promise}` | -- |
283
+ | `'await:then'` | Resolved state of an async block | `{:then value}` | -- |
284
+ | `'await:catch'` | Rejected state of an async block | `{:catch error}` | -- |
285
+ | `'end'` | Closing block | `{/if}`, `{/each}` | `<% } %>` |
286
+ | `null` | No specific conditional semantic | -- | `<%= expr %>` |
287
+
288
+ ### Framework-Specific Examples
289
+
290
+ **Svelte:**
291
+
292
+ ```svelte
293
+ {#if loggedIn}
294
+ <p>Welcome!</p>
295
+ {:else}
296
+ <p>Please log in.</p>
297
+ {/if}
298
+ ```
299
+
300
+ Produces three `psblock` nodes:
301
+
302
+ 1. `conditionalType: 'if'` for `{#if loggedIn}`
303
+ 2. `conditionalType: 'if:else'` for `{:else}`
304
+ 3. `conditionalType: 'end'` for `{/if}`
305
+
306
+ **Vue (v-if directive is handled differently -- via element attributes, not psblock).**
307
+
308
+ **EJS:**
309
+
310
+ ```ejs
311
+ <% if (user) { %>
312
+ <p><%= user.name %></p>
313
+ <% } %>
314
+ ```
315
+
316
+ Produces:
317
+
318
+ 1. `conditionalType: 'if'` for `<% if (user) { %>`
319
+ 2. `conditionalType: 'end'` for `<% } %>`
320
+ 3. `conditionalType: null` for `<%= user.name %>` (expression output, no conditional semantic)
321
+
322
+ ## MLASTInvalid
323
+
324
+ **Type discriminant:** `'invalid'`
325
+
326
+ **Role:** Represents markup that could not be parsed correctly. The parser captures unparsable content as invalid nodes rather than failing entirely, enabling lint rules to report the issue.
327
+
328
+ | Field | Type | Description |
329
+ | ---------- | --------------------------------------------------------- | ---------------------------------------- |
330
+ | `type` | `'invalid'` | Discriminant tag |
331
+ | `nodeName` | `'#invalid'` | Always `'#invalid'` |
332
+ | `depth` | `number` | Nesting depth in the document tree |
333
+ | `kind` | `Exclude<MLASTChildNode['type'], 'invalid'> \| undefined` | The kind of node this was intended to be |
334
+ | `isBogus` | `true` | Always `true` for invalid nodes |
335
+
336
+ ### The `kind` Field and ml-core Conversion
337
+
338
+ The `kind` field records what the parser believes the invalid content was intended to be. This information is used by `ml-core` when converting the AST into a DOM tree:
339
+
340
+ | `kind` Value | ml-core Conversion |
341
+ | ----------------------- | ------------------------------------------------------------------------------------------- |
342
+ | `'starttag'` | Converted to an `MLElement` with `nodeName: 'x-invalid'` and `elementType: 'web-component'` |
343
+ | Any other / `undefined` | Converted to an `MLText` node with `nodeName: '#text'` |
344
+
345
+ This conversion allows lint rules to still operate on invalid content, treating it as either an element or text depending on the parser's best guess.
346
+
347
+ ## MLASTHTMLAttr
348
+
349
+ **Type discriminant:** `'attr'`
350
+
351
+ **Role:** Represents a regular HTML attribute, fully decomposed into its constituent tokens. This granular decomposition enables lint rules to inspect and validate individual parts of an attribute (whitespace, quoting style, name, value).
352
+
353
+ | Field | Type | Description |
354
+ | ------------------- | ---------------------------------------------------------- | ----------------------------------------------------------------------- |
355
+ | `type` | `'attr'` | Discriminant tag |
356
+ | `nodeName` | `string` | The attribute name as a string |
357
+ | `spacesBeforeName` | `MLASTToken` | Whitespace token before the attribute name |
358
+ | `name` | `MLASTToken` | The attribute name token |
359
+ | `spacesBeforeEqual` | `MLASTToken` | Whitespace token between the name and the equal sign |
360
+ | `equal` | `MLASTToken` | The equal sign token |
361
+ | `spacesAfterEqual` | `MLASTToken` | Whitespace token between the equal sign and the value |
362
+ | `startQuote` | `MLASTToken` | The opening quote token |
363
+ | `value` | `MLASTToken` | The attribute value token |
364
+ | `endQuote` | `MLASTToken` | The closing quote token |
365
+ | `isDynamicValue` | `true \| undefined` | Whether the value is a dynamic expression (e.g., a framework binding) |
366
+ | `isDirective` | `true \| undefined` | Whether the attribute is a framework directive (e.g., `v-if`, `@click`) |
367
+ | `potentialName` | `string \| undefined` | The resolved attribute name when the actual name is a directive |
368
+ | `potentialValue` | `string \| undefined` | The resolved attribute value when the actual value is dynamic |
369
+ | `valueType` | `'string' \| 'number' \| 'boolean' \| 'code' \| undefined` | The semantic type of the attribute value |
370
+ | `candidate` | `string \| undefined` | A candidate attribute name for auto-correction |
371
+ | `isDuplicatable` | `boolean` | Whether this attribute is allowed to appear multiple times |
372
+
373
+ ### Attribute Decomposition
374
+
375
+ An attribute is decomposed into individual tokens, each with its own positional information:
376
+
377
+ ```
378
+ ·class="container"
379
+ ↑ ↑↑ ↑
380
+ │ ││ └─ endQuote (raw: '"')
381
+ │ │└─ value (raw: 'container')
382
+ │ └─ startQuote (raw: '"')
383
+ │ equal (raw: '=')
384
+ │ spacesBeforeEqual (raw: '')
385
+ │ spacesAfterEqual (raw: '')
386
+ └─ spacesBeforeName (raw: ' ')
387
+ name (raw: 'class')
388
+ ```
389
+
390
+ For boolean attributes without a value (e.g., `disabled`), the `equal`, `startQuote`, `value`, and `endQuote` tokens exist but have empty `raw` strings.
391
+
392
+ ### Framework Extension Fields
393
+
394
+ These fields are set by framework-specific parsers:
395
+
396
+ - **`isDynamicValue`**: `true` when the attribute value is a dynamic expression. For example, in Vue `<div :class="expr">`, the value `expr` is dynamic.
397
+ - **`isDirective`**: `true` when the attribute is a framework directive. For example, `v-if`, `v-for`, `@click` in Vue; `on:click` in Svelte.
398
+ - **`potentialName`**: The resolved standard attribute name. For example, `:class` resolves to `class`; `@click` resolves to `onclick`.
399
+ - **`potentialValue`**: The resolved attribute value when the dynamic expression can be statically analyzed.
400
+ - **`valueType`**: The semantic type of the value -- `'string'`, `'number'`, `'boolean'`, or `'code'` (an expression).
401
+ - **`candidate`**: A suggested correction for the attribute name, used by auto-fix rules.
402
+ - **`isDuplicatable`**: `true` when the attribute may appear multiple times on the same element (e.g., `class` in some template engines that merge values).
403
+
404
+ ## MLASTSpreadAttr
405
+
406
+ **Type discriminant:** `'spread'`
407
+
408
+ **Role:** Represents a spread attribute (e.g., `{...props}` in JSX). This is a minimal node type since spread attributes cannot be statically decomposed.
409
+
410
+ | Field | Type | Description |
411
+ | ---------- | ----------- | ------------------ |
412
+ | `type` | `'spread'` | Discriminant tag |
413
+ | `nodeName` | `'#spread'` | Always `'#spread'` |
414
+
415
+ Note that `MLASTSpreadAttr` extends `MLASTToken` directly (not `MLASTAbstractNode`), so it has positional information (`uuid`, `raw`, `startOffset`, etc.) but no `parentNode` or `depth`.
416
+
417
+ ## Union Types Reference
418
+
419
+ | Union Type | Members | Purpose |
420
+ | ------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- |
421
+ | `MLASTNode` | `MLASTDoctype \| MLASTTag \| MLASTComment \| MLASTText \| MLASTPreprocessorSpecificBlock \| MLASTInvalid \| MLASTAttr` | Every possible AST node type |
422
+ | `MLASTParentNode` | `MLASTElement \| MLASTPreprocessorSpecificBlock` | Nodes that can contain child nodes |
423
+ | `MLASTChildNode` | `MLASTTag \| MLASTText \| MLASTComment \| MLASTPreprocessorSpecificBlock \| MLASTInvalid` | Nodes that can appear as children |
424
+ | `MLASTNodeTreeItem` | `MLASTChildNode \| MLASTDoctype` | Top-level items in `MLASTDocument.nodeList` |
425
+ | `MLASTTag` | `MLASTElement \| MLASTElementCloseTag` | Tag nodes (opening or closing) |
426
+ | `MLASTAttr` | `MLASTHTMLAttr \| MLASTSpreadAttr` | Attribute nodes |
427
+
428
+ ## Type Narrowing Patterns
429
+
430
+ ### Narrowing by `type`
431
+
432
+ The most common pattern -- use a `switch` statement for exhaustive narrowing:
433
+
434
+ ```typescript
435
+ import type { MLASTChildNode } from '@markuplint/ml-ast';
436
+
437
+ function processChild(node: MLASTChildNode) {
438
+ switch (node.type) {
439
+ case 'starttag':
440
+ console.log(`Element: <${node.nodeName}>, attributes: ${node.attributes.length}`);
441
+ break;
442
+ case 'endtag':
443
+ console.log(`Closing tag: </${node.nodeName}>`);
444
+ break;
445
+ case 'text':
446
+ console.log(`Text: "${node.raw}"`);
447
+ break;
448
+ case 'comment':
449
+ console.log(`Comment (bogus: ${node.isBogus})`);
450
+ break;
451
+ case 'psblock':
452
+ console.log(`Block: ${node.nodeName}, conditional: ${node.conditionalType}`);
453
+ break;
454
+ case 'invalid':
455
+ console.log(`Invalid: kind=${node.kind}`);
456
+ break;
457
+ }
458
+ }
459
+ ```
460
+
461
+ ### Checking for parent nodes
462
+
463
+ ```typescript
464
+ import type { MLASTNode, MLASTParentNode } from '@markuplint/ml-ast';
465
+
466
+ function isParent(node: MLASTNode): node is MLASTParentNode {
467
+ return node.type === 'starttag' || node.type === 'psblock';
468
+ }
469
+ ```
470
+
471
+ ### Distinguishing attribute types
472
+
473
+ ```typescript
474
+ import type { MLASTAttr } from '@markuplint/ml-ast';
475
+
476
+ function processAttr(attr: MLASTAttr) {
477
+ if (attr.type === 'attr') {
478
+ // MLASTHTMLAttr -- has name, value, quotes, etc.
479
+ console.log(`${attr.name.raw}="${attr.value.raw}"`);
480
+ } else {
481
+ // MLASTSpreadAttr -- only has raw and positional info
482
+ console.log(`Spread: ${attr.raw}`);
483
+ }
484
+ }
485
+ ```