@markuplint/ml-ast 5.0.0-rc.4 → 5.0.0-rc.6
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 +11 -0
- package/README.md +0 -6
- package/lib/index.d.ts +16 -0
- package/lib/index.js +16 -0
- package/lib/types.d.ts +145 -8
- package/package.json +3 -3
- package/ARCHITECTURE.ja.md +0 -309
- package/ARCHITECTURE.md +0 -309
- package/SKILL.md +0 -123
- package/docs/maintenance.ja.md +0 -213
- package/docs/maintenance.md +0 -214
- package/docs/node-reference.ja.md +0 -487
- package/docs/node-reference.md +0 -487
package/docs/node-reference.md
DELETED
|
@@ -1,487 +0,0 @@
|
|
|
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` resolves the `pairNodeUuid` to look up the closing tag's AST node and creates an `MLElementCloseTag` from it. `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
|
-
| `offset` | `number` | Zero-based character offset of the token start |
|
|
69
|
-
| `line` | `number` | One-based line number where the token starts |
|
|
70
|
-
| `col` | `number` | One-based column number where the token starts |
|
|
71
|
-
|
|
72
|
-
End positions can be derived: `endOffset = offset + raw.length`. For `endLine` and `endCol`, use the helper functions from `@markuplint/parser-utils`.
|
|
73
|
-
|
|
74
|
-
**Coordinate system:** `offset` is zero-based (counting from 0), while `line` and `col` are one-based (counting from 1).
|
|
75
|
-
|
|
76
|
-
### MLASTAbstractNode
|
|
77
|
-
|
|
78
|
-
An internal (non-exported) base interface that extends `MLASTToken` with structural metadata. All concrete node types extend this.
|
|
79
|
-
|
|
80
|
-
| Field | Type | Description |
|
|
81
|
-
| ---------------- | ---------------- | ------------------------------------------------------ |
|
|
82
|
-
| `type` | `MLASTNodeType` | Discriminant tag identifying the concrete node kind |
|
|
83
|
-
| `nodeName` | `string` | The node name (tag name, `#text`, `#comment`, etc.) |
|
|
84
|
-
| `parentNodeUuid` | `string \| null` | UUID of the parent node, or `null` for top-level nodes |
|
|
85
|
-
|
|
86
|
-
## MLASTDocument
|
|
87
|
-
|
|
88
|
-
**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.
|
|
89
|
-
|
|
90
|
-
| Field | Type | Description |
|
|
91
|
-
| ------------------- | ------------------------------ | ------------------------------------------------------------- |
|
|
92
|
-
| `raw` | `string` | The full original source code |
|
|
93
|
-
| `nodeList` | `readonly MLASTNodeTreeItem[]` | Flat list of top-level AST nodes in document order |
|
|
94
|
-
| `isFragment` | `boolean` | Whether the document is a fragment (no root element required) |
|
|
95
|
-
| `unknownParseError` | `string \| undefined` | A description of any unknown parse error |
|
|
96
|
-
|
|
97
|
-
**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).
|
|
98
|
-
|
|
99
|
-
## MLASTDoctype
|
|
100
|
-
|
|
101
|
-
**Type discriminant:** `'doctype'`
|
|
102
|
-
|
|
103
|
-
**Role:** Represents a DOCTYPE declaration (e.g., `<!DOCTYPE html>`). Always appears at the top level of the document.
|
|
104
|
-
|
|
105
|
-
| Field | Type | Description |
|
|
106
|
-
| ---------- | ----------- | ------------------------------------------------ |
|
|
107
|
-
| `type` | `'doctype'` | Discriminant tag |
|
|
108
|
-
| `depth` | `number` | Nesting depth (always 0 for DOCTYPE) |
|
|
109
|
-
| `name` | `string` | The declared document type name (e.g., `"html"`) |
|
|
110
|
-
| `publicId` | `string` | The public identifier of the DOCTYPE, if any |
|
|
111
|
-
| `systemId` | `string` | The system identifier of the DOCTYPE, if any |
|
|
112
|
-
|
|
113
|
-
**Example:**
|
|
114
|
-
|
|
115
|
-
```html
|
|
116
|
-
<!DOCTYPE html>
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
Produces a node with `name: "html"`, `publicId: ""`, `systemId: ""`.
|
|
120
|
-
|
|
121
|
-
## MLASTElement
|
|
122
|
-
|
|
123
|
-
**Type discriminant:** `'starttag'`
|
|
124
|
-
|
|
125
|
-
**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.
|
|
126
|
-
|
|
127
|
-
| Field | Type | Description |
|
|
128
|
-
| --------------- | ---------------------------- | --------------------------------------------------------------------------- |
|
|
129
|
-
| `type` | `'starttag'` | Discriminant tag |
|
|
130
|
-
| `depth` | `number` | Nesting depth in the document tree |
|
|
131
|
-
| `namespace` | `string` | Namespace URI (e.g., `"http://www.w3.org/1999/xhtml"`) |
|
|
132
|
-
| `elementType` | `ElementType` | Whether the element is `'html'`, `'web-component'`, or `'authored'` |
|
|
133
|
-
| `isFragment` | `boolean` | Whether the element acts as a fragment (e.g., React `<>`, Vue `<template>`) |
|
|
134
|
-
| `attributes` | `readonly MLASTAttr[]` | Attributes on this element |
|
|
135
|
-
| `hasSpreadAttr` | `boolean \| undefined` | Whether the element has one or more spread attributes |
|
|
136
|
-
| `childNodes` | `readonly MLASTChildNode[]` | Direct child nodes of this element |
|
|
137
|
-
| `blockBehavior` | `MLASTBlockBehavior \| null` | Block behavior associated with this element, if any |
|
|
138
|
-
| `pairNodeUuid` | `string \| null` | UUID of the matching closing tag, or `null` for void/self-closing elements |
|
|
139
|
-
| `tagOpenChar` | `string` | The characters that open this tag (usually `"<"`) |
|
|
140
|
-
| `tagCloseChar` | `string` | The characters that close this tag (usually `">"`) |
|
|
141
|
-
| `isGhost` | `boolean` | Whether this is a ghost node (omitted tag inferred by the parser) |
|
|
142
|
-
|
|
143
|
-
### Element Type Classification
|
|
144
|
-
|
|
145
|
-
The `elementType` field classifies elements into three categories:
|
|
146
|
-
|
|
147
|
-
| Value | Description | Examples |
|
|
148
|
-
| ----------------- | ---------------------------------------------------------------- | ------------------------ |
|
|
149
|
-
| `'html'` | Native HTML element from the HTML Standard | `<div>`, `<span>`, `<p>` |
|
|
150
|
-
| `'web-component'` | Web Component according to the HTML Standard (contains a hyphen) | `<my-component>` |
|
|
151
|
-
| `'authored'` | Authored element through a view framework or template engine | `<MyComponent>` (JSX) |
|
|
152
|
-
|
|
153
|
-
### Tag Delimiters
|
|
154
|
-
|
|
155
|
-
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.
|
|
156
|
-
|
|
157
|
-
### Ghost Nodes (Omitted Tags)
|
|
158
|
-
|
|
159
|
-
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.
|
|
160
|
-
|
|
161
|
-
### Pair Node Relationship
|
|
162
|
-
|
|
163
|
-
The `pairNodeUuid` field creates a **bidirectional link** between opening and closing tags via UUID strings (not object references, enabling JSON serialization):
|
|
164
|
-
|
|
165
|
-
- `MLASTElement.pairNodeUuid` contains the UUID of its `MLASTElementCloseTag`
|
|
166
|
-
- `MLASTElementCloseTag.pairNodeUuid` contains the UUID of its `MLASTElement`
|
|
167
|
-
|
|
168
|
-
To resolve a UUID to the actual node, look it up in `MLASTDocument.nodeList` by matching the `uuid` field.
|
|
169
|
-
|
|
170
|
-
For void elements (`<br>`, `<img>`, etc.) and self-closing elements, `pairNodeUuid` is `null`.
|
|
171
|
-
|
|
172
|
-
### Fragment Elements
|
|
173
|
-
|
|
174
|
-
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.
|
|
175
|
-
|
|
176
|
-
**Example:**
|
|
177
|
-
|
|
178
|
-
```html
|
|
179
|
-
<div class="container" id="main">
|
|
180
|
-
<p>Hello</p>
|
|
181
|
-
</div>
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
The `<div>` produces an `MLASTElement` with:
|
|
185
|
-
|
|
186
|
-
- `nodeName: "div"`
|
|
187
|
-
- `elementType: "html"`
|
|
188
|
-
- `namespace: "http://www.w3.org/1999/xhtml"`
|
|
189
|
-
- `attributes`: array containing `class` and `id` attributes
|
|
190
|
-
- `childNodes`: array containing the `<p>` element and text nodes
|
|
191
|
-
- `pairNodeUuid`: UUID of the `</div>` closing tag
|
|
192
|
-
|
|
193
|
-
## MLASTElementCloseTag
|
|
194
|
-
|
|
195
|
-
**Type discriminant:** `'endtag'`
|
|
196
|
-
|
|
197
|
-
**Role:** Represents a closing element tag (e.g., `</div>`). Always paired with an `MLASTElement` via the `pairNodeUuid` field.
|
|
198
|
-
|
|
199
|
-
| Field | Type | Description |
|
|
200
|
-
| ---------------- | ---------------- | --------------------------------------------------------------- |
|
|
201
|
-
| `type` | `'endtag'` | Discriminant tag |
|
|
202
|
-
| `depth` | `number` | Nesting depth in the document tree |
|
|
203
|
-
| `parentNodeUuid` | `string \| null` | UUID of the parent node (same parent as the paired opening tag) |
|
|
204
|
-
| `pairNodeUuid` | `string \| null` | UUID of the matching opening element tag |
|
|
205
|
-
| `tagOpenChar` | `string` | The characters that open this tag (usually `"</"`) |
|
|
206
|
-
| `tagCloseChar` | `string` | The characters that close this tag (usually `">"`) |
|
|
207
|
-
|
|
208
|
-
## MLASTComment
|
|
209
|
-
|
|
210
|
-
**Type discriminant:** `'comment'`
|
|
211
|
-
|
|
212
|
-
**Role:** Represents an HTML comment (e.g., `<!-- ... -->`).
|
|
213
|
-
|
|
214
|
-
| Field | Type | Description |
|
|
215
|
-
| ---------- | ------------ | ---------------------------------------- |
|
|
216
|
-
| `type` | `'comment'` | Discriminant tag |
|
|
217
|
-
| `nodeName` | `'#comment'` | Always `'#comment'` |
|
|
218
|
-
| `depth` | `number` | Nesting depth in the document tree |
|
|
219
|
-
| `isBogus` | `boolean` | Whether the comment is bogus (malformed) |
|
|
220
|
-
|
|
221
|
-
### Bogus Comments
|
|
222
|
-
|
|
223
|
-
When `isBogus` is `true`, the comment is malformed according to the HTML specification. Examples of bogus comments include:
|
|
224
|
-
|
|
225
|
-
- `<!...>` (not a valid DOCTYPE or comment)
|
|
226
|
-
- `<?xml version="1.0"?>` (processing instructions in HTML)
|
|
227
|
-
|
|
228
|
-
The parser still captures these as comment nodes but flags them as bogus so that lint rules can report them.
|
|
229
|
-
|
|
230
|
-
## MLASTText
|
|
231
|
-
|
|
232
|
-
**Type discriminant:** `'text'`
|
|
233
|
-
|
|
234
|
-
**Role:** Represents character data between elements.
|
|
235
|
-
|
|
236
|
-
| Field | Type | Description |
|
|
237
|
-
| ---------- | --------- | ---------------------------------- |
|
|
238
|
-
| `type` | `'text'` | Discriminant tag |
|
|
239
|
-
| `nodeName` | `'#text'` | Always `'#text'` |
|
|
240
|
-
| `depth` | `number` | Nesting depth in the document tree |
|
|
241
|
-
|
|
242
|
-
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.).
|
|
243
|
-
|
|
244
|
-
**Example:**
|
|
245
|
-
|
|
246
|
-
```html
|
|
247
|
-
<p>Hello, world!</p>
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
The text `Hello, world!` is represented as an `MLASTText` node with `raw: "Hello, world!"`.
|
|
251
|
-
|
|
252
|
-
## MLASTPreprocessorSpecificBlock
|
|
253
|
-
|
|
254
|
-
**Type discriminant:** `'psblock'`
|
|
255
|
-
|
|
256
|
-
**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.
|
|
257
|
-
|
|
258
|
-
| Field | Type | Description |
|
|
259
|
-
| --------------- | ---------------------------- | ------------------------------------------------------------ |
|
|
260
|
-
| `type` | `'psblock'` | Discriminant tag |
|
|
261
|
-
| `blockBehavior` | `MLASTBlockBehavior \| null` | Block behavior describing the control-flow construct, if any |
|
|
262
|
-
| `depth` | `number` | Nesting depth in the document tree |
|
|
263
|
-
| `nodeName` | `string` | The block's name as determined by the parser |
|
|
264
|
-
| `isFragment` | `boolean` | Whether this block acts as a transparent fragment |
|
|
265
|
-
| `childNodes` | `readonly MLASTChildNode[]` | Direct child nodes within this block |
|
|
266
|
-
| `isBogus` | `boolean` | Whether this block is bogus (unparsable or malformed) |
|
|
267
|
-
|
|
268
|
-
### Block Behavior Types
|
|
269
|
-
|
|
270
|
-
The `blockBehavior` field is an `MLASTBlockBehavior` object (or `null` for blocks with no control-flow semantic). When present, `blockBehavior.type` indicates the semantic role of the block, and `blockBehavior.expression` contains the source expression that drives the construct:
|
|
271
|
-
|
|
272
|
-
| `blockBehavior.type` | Description | Example (Svelte) | Example (EJS/ERB) |
|
|
273
|
-
| -------------------- | ---------------------------------- | ----------------------- | ------------------- |
|
|
274
|
-
| `'if'` | Conditional branch (opening) | `{#if condition}` | `<% if (x) { %>` |
|
|
275
|
-
| `'if:elseif'` | Alternative conditional branch | `{:else if condition}` | `<% } else if { %>` |
|
|
276
|
-
| `'if:else'` | Default (else) branch | `{:else}` | `<% } else { %>` |
|
|
277
|
-
| `'switch:case'` | Switch case branch | -- | -- |
|
|
278
|
-
| `'switch:default'` | Switch default branch | -- | -- |
|
|
279
|
-
| `'each'` | Iteration (loop) block | `{#each items as item}` | `<% for (...) { %>` |
|
|
280
|
-
| `'each:empty'` | Empty state for an iteration block | `{:else}` (in `#each`) | -- |
|
|
281
|
-
| `'await'` | Asynchronous block (pending state) | `{#await promise}` | -- |
|
|
282
|
-
| `'await:then'` | Resolved state of an async block | `{:then value}` | -- |
|
|
283
|
-
| `'await:catch'` | Rejected state of an async block | `{:catch error}` | -- |
|
|
284
|
-
| `'end'` | Closing block | `{/if}`, `{/each}` | `<% } %>` |
|
|
285
|
-
|
|
286
|
-
When `blockBehavior` is `null`, the block has no specific control-flow semantic (e.g., `<%= expr %>` in EJS is a pure expression output).
|
|
287
|
-
|
|
288
|
-
The `expression` field in `MLASTBlockBehavior` contains the raw source expression associated with the block (e.g., `'{#if loggedIn}'` for a Svelte if-block).
|
|
289
|
-
|
|
290
|
-
### Framework-Specific Examples
|
|
291
|
-
|
|
292
|
-
**Svelte:**
|
|
293
|
-
|
|
294
|
-
```svelte
|
|
295
|
-
{#if loggedIn}
|
|
296
|
-
<p>Welcome!</p>
|
|
297
|
-
{:else}
|
|
298
|
-
<p>Please log in.</p>
|
|
299
|
-
{/if}
|
|
300
|
-
```
|
|
301
|
-
|
|
302
|
-
Produces three `psblock` nodes:
|
|
303
|
-
|
|
304
|
-
1. `blockBehavior: { type: 'if', expression: '{#if loggedIn}' }` for `{#if loggedIn}`
|
|
305
|
-
2. `blockBehavior: { type: 'if:else', expression: '{:else}' }` for `{:else}`
|
|
306
|
-
3. `blockBehavior: { type: 'end', expression: '{/if}' }` for `{/if}`
|
|
307
|
-
|
|
308
|
-
**Vue (v-if directive is handled differently -- via element attributes, not psblock).**
|
|
309
|
-
|
|
310
|
-
**EJS:**
|
|
311
|
-
|
|
312
|
-
```ejs
|
|
313
|
-
<% if (user) { %>
|
|
314
|
-
<p><%= user.name %></p>
|
|
315
|
-
<% } %>
|
|
316
|
-
```
|
|
317
|
-
|
|
318
|
-
Produces:
|
|
319
|
-
|
|
320
|
-
1. `blockBehavior: { type: 'if', expression: '<% if (user) { %>' }` for `<% if (user) { %>`
|
|
321
|
-
2. `blockBehavior: { type: 'end', expression: '<% } %>' }` for `<% } %>`
|
|
322
|
-
3. `blockBehavior: null` for `<%= user.name %>` (expression output, no control-flow semantic)
|
|
323
|
-
|
|
324
|
-
## MLASTInvalid
|
|
325
|
-
|
|
326
|
-
**Type discriminant:** `'invalid'`
|
|
327
|
-
|
|
328
|
-
**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.
|
|
329
|
-
|
|
330
|
-
| Field | Type | Description |
|
|
331
|
-
| ---------- | --------------------------------------------------------- | ---------------------------------------- |
|
|
332
|
-
| `type` | `'invalid'` | Discriminant tag |
|
|
333
|
-
| `nodeName` | `'#invalid'` | Always `'#invalid'` |
|
|
334
|
-
| `depth` | `number` | Nesting depth in the document tree |
|
|
335
|
-
| `kind` | `Exclude<MLASTChildNode['type'], 'invalid'> \| undefined` | The kind of node this was intended to be |
|
|
336
|
-
| `isBogus` | `true` | Always `true` for invalid nodes |
|
|
337
|
-
|
|
338
|
-
### The `kind` Field and ml-core Conversion
|
|
339
|
-
|
|
340
|
-
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:
|
|
341
|
-
|
|
342
|
-
| `kind` Value | ml-core Conversion |
|
|
343
|
-
| ----------------------- | ------------------------------------------------------------------------------------------- |
|
|
344
|
-
| `'starttag'` | Converted to an `MLElement` with `nodeName: 'x-invalid'` and `elementType: 'web-component'` |
|
|
345
|
-
| Any other / `undefined` | Converted to an `MLText` node with `nodeName: '#text'` |
|
|
346
|
-
|
|
347
|
-
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.
|
|
348
|
-
|
|
349
|
-
## MLASTHTMLAttr
|
|
350
|
-
|
|
351
|
-
**Type discriminant:** `'attr'`
|
|
352
|
-
|
|
353
|
-
**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).
|
|
354
|
-
|
|
355
|
-
| Field | Type | Description |
|
|
356
|
-
| ------------------- | ---------------------------------------------------------- | ----------------------------------------------------------------------- |
|
|
357
|
-
| `type` | `'attr'` | Discriminant tag |
|
|
358
|
-
| `nodeName` | `string` | The attribute name as a string |
|
|
359
|
-
| `spacesBeforeName` | `MLASTToken` | Whitespace token before the attribute name |
|
|
360
|
-
| `name` | `MLASTToken` | The attribute name token |
|
|
361
|
-
| `spacesBeforeEqual` | `MLASTToken` | Whitespace token between the name and the equal sign |
|
|
362
|
-
| `equal` | `MLASTToken` | The equal sign token |
|
|
363
|
-
| `spacesAfterEqual` | `MLASTToken` | Whitespace token between the equal sign and the value |
|
|
364
|
-
| `startQuote` | `MLASTToken` | The opening quote token |
|
|
365
|
-
| `value` | `MLASTToken` | The attribute value token |
|
|
366
|
-
| `endQuote` | `MLASTToken` | The closing quote token |
|
|
367
|
-
| `isDynamicValue` | `true \| undefined` | Whether the value is a dynamic expression (e.g., a framework binding) |
|
|
368
|
-
| `isDirective` | `true \| undefined` | Whether the attribute is a framework directive (e.g., `v-if`, `@click`) |
|
|
369
|
-
| `potentialName` | `string \| undefined` | The resolved attribute name when the actual name is a directive |
|
|
370
|
-
| `potentialValue` | `string \| undefined` | The resolved attribute value when the actual value is dynamic |
|
|
371
|
-
| `valueType` | `'string' \| 'number' \| 'boolean' \| 'code' \| undefined` | The semantic type of the attribute value |
|
|
372
|
-
| `candidate` | `string \| undefined` | A candidate attribute name for auto-correction |
|
|
373
|
-
| `isDuplicatable` | `boolean` | Whether this attribute is allowed to appear multiple times |
|
|
374
|
-
|
|
375
|
-
### Attribute Decomposition
|
|
376
|
-
|
|
377
|
-
An attribute is decomposed into individual tokens, each with its own positional information:
|
|
378
|
-
|
|
379
|
-
```
|
|
380
|
-
·class="container"
|
|
381
|
-
↑ ↑↑ ↑
|
|
382
|
-
│ ││ └─ endQuote (raw: '"')
|
|
383
|
-
│ │└─ value (raw: 'container')
|
|
384
|
-
│ └─ startQuote (raw: '"')
|
|
385
|
-
│ equal (raw: '=')
|
|
386
|
-
│ spacesBeforeEqual (raw: '')
|
|
387
|
-
│ spacesAfterEqual (raw: '')
|
|
388
|
-
└─ spacesBeforeName (raw: ' ')
|
|
389
|
-
name (raw: 'class')
|
|
390
|
-
```
|
|
391
|
-
|
|
392
|
-
For boolean attributes without a value (e.g., `disabled`), the `equal`, `startQuote`, `value`, and `endQuote` tokens exist but have empty `raw` strings.
|
|
393
|
-
|
|
394
|
-
### Framework Extension Fields
|
|
395
|
-
|
|
396
|
-
These fields are set by framework-specific parsers:
|
|
397
|
-
|
|
398
|
-
- **`isDynamicValue`**: `true` when the attribute value is a dynamic expression. For example, in Vue `<div :class="expr">`, the value `expr` is dynamic.
|
|
399
|
-
- **`isDirective`**: `true` when the attribute is a framework directive. For example, `v-if`, `v-for`, `@click` in Vue; `on:click` in Svelte.
|
|
400
|
-
- **`potentialName`**: The resolved standard attribute name. For example, `:class` resolves to `class`; `@click` resolves to `onclick`.
|
|
401
|
-
- **`potentialValue`**: The resolved attribute value when the dynamic expression can be statically analyzed.
|
|
402
|
-
- **`valueType`**: The semantic type of the value -- `'string'`, `'number'`, `'boolean'`, or `'code'` (an expression).
|
|
403
|
-
- **`candidate`**: A suggested correction for the attribute name, used by auto-fix rules.
|
|
404
|
-
- **`isDuplicatable`**: `true` when the attribute may appear multiple times on the same element (e.g., `class` in some template engines that merge values).
|
|
405
|
-
|
|
406
|
-
## MLASTSpreadAttr
|
|
407
|
-
|
|
408
|
-
**Type discriminant:** `'spread'`
|
|
409
|
-
|
|
410
|
-
**Role:** Represents a spread attribute (e.g., `{...props}` in JSX). This is a minimal node type since spread attributes cannot be statically decomposed.
|
|
411
|
-
|
|
412
|
-
| Field | Type | Description |
|
|
413
|
-
| ---------- | ----------- | ------------------ |
|
|
414
|
-
| `type` | `'spread'` | Discriminant tag |
|
|
415
|
-
| `nodeName` | `'#spread'` | Always `'#spread'` |
|
|
416
|
-
|
|
417
|
-
Note that `MLASTSpreadAttr` extends `MLASTToken` directly (not `MLASTAbstractNode`), so it has positional information (`uuid`, `raw`, `offset`, etc.) but no `parentNodeUuid` or `depth`.
|
|
418
|
-
|
|
419
|
-
## Union Types Reference
|
|
420
|
-
|
|
421
|
-
| Union Type | Members | Purpose |
|
|
422
|
-
| ------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- |
|
|
423
|
-
| `MLASTNode` | `MLASTDoctype \| MLASTTag \| MLASTComment \| MLASTText \| MLASTPreprocessorSpecificBlock \| MLASTInvalid \| MLASTAttr` | Every possible AST node type |
|
|
424
|
-
| `MLASTParentNode` | `MLASTElement \| MLASTPreprocessorSpecificBlock` | Nodes that can contain child nodes |
|
|
425
|
-
| `MLASTChildNode` | `MLASTTag \| MLASTText \| MLASTComment \| MLASTPreprocessorSpecificBlock \| MLASTInvalid` | Nodes that can appear as children |
|
|
426
|
-
| `MLASTNodeTreeItem` | `MLASTChildNode \| MLASTDoctype` | Top-level items in `MLASTDocument.nodeList` |
|
|
427
|
-
| `MLASTTag` | `MLASTElement \| MLASTElementCloseTag` | Tag nodes (opening or closing) |
|
|
428
|
-
| `MLASTAttr` | `MLASTHTMLAttr \| MLASTSpreadAttr` | Attribute nodes |
|
|
429
|
-
|
|
430
|
-
## Type Narrowing Patterns
|
|
431
|
-
|
|
432
|
-
### Narrowing by `type`
|
|
433
|
-
|
|
434
|
-
The most common pattern -- use a `switch` statement for exhaustive narrowing:
|
|
435
|
-
|
|
436
|
-
```typescript
|
|
437
|
-
import type { MLASTChildNode } from '@markuplint/ml-ast';
|
|
438
|
-
|
|
439
|
-
function processChild(node: MLASTChildNode) {
|
|
440
|
-
switch (node.type) {
|
|
441
|
-
case 'starttag':
|
|
442
|
-
console.log(`Element: <${node.nodeName}>, attributes: ${node.attributes.length}`);
|
|
443
|
-
break;
|
|
444
|
-
case 'endtag':
|
|
445
|
-
console.log(`Closing tag: </${node.nodeName}>`);
|
|
446
|
-
break;
|
|
447
|
-
case 'text':
|
|
448
|
-
console.log(`Text: "${node.raw}"`);
|
|
449
|
-
break;
|
|
450
|
-
case 'comment':
|
|
451
|
-
console.log(`Comment (bogus: ${node.isBogus})`);
|
|
452
|
-
break;
|
|
453
|
-
case 'psblock':
|
|
454
|
-
console.log(`Block: ${node.nodeName}, behavior: ${node.blockBehavior?.type ?? 'none'}`);
|
|
455
|
-
break;
|
|
456
|
-
case 'invalid':
|
|
457
|
-
console.log(`Invalid: kind=${node.kind}`);
|
|
458
|
-
break;
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
```
|
|
462
|
-
|
|
463
|
-
### Checking for parent nodes
|
|
464
|
-
|
|
465
|
-
```typescript
|
|
466
|
-
import type { MLASTNode, MLASTParentNode } from '@markuplint/ml-ast';
|
|
467
|
-
|
|
468
|
-
function isParent(node: MLASTNode): node is MLASTParentNode {
|
|
469
|
-
return node.type === 'starttag' || node.type === 'psblock';
|
|
470
|
-
}
|
|
471
|
-
```
|
|
472
|
-
|
|
473
|
-
### Distinguishing attribute types
|
|
474
|
-
|
|
475
|
-
```typescript
|
|
476
|
-
import type { MLASTAttr } from '@markuplint/ml-ast';
|
|
477
|
-
|
|
478
|
-
function processAttr(attr: MLASTAttr) {
|
|
479
|
-
if (attr.type === 'attr') {
|
|
480
|
-
// MLASTHTMLAttr -- has name, value, quotes, etc.
|
|
481
|
-
console.log(`${attr.name.raw}="${attr.value.raw}"`);
|
|
482
|
-
} else {
|
|
483
|
-
// MLASTSpreadAttr -- only has raw and positional info
|
|
484
|
-
console.log(`Spread: ${attr.raw}`);
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
```
|