@markuplint/astro-parser 5.2.0 → 5.3.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 +14 -0
- package/lib/astro-parser.d.ts +9 -12
- package/lib/astro-parser.js +60 -11
- package/lib/component-scanner.js +8 -0
- package/lib/parser.d.ts +10 -1
- package/lib/parser.js +19 -14
- package/lib/to-nodes.d.ts +52 -0
- package/lib/to-nodes.js +120 -0
- package/package.json +9 -12
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,20 @@
|
|
|
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.3.0](https://github.com/markuplint/markuplint/compare/v5.2.0...v5.3.0) (2026-10-21)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **astro-parser:** detect a frontmatter fence with trailing spaces and pin the guard boundaries ([23e6e0c](https://github.com/markuplint/markuplint/commit/23e6e0c3628ed3c9b3bad20d9381a505919146b1))
|
|
11
|
+
* **astro-parser:** do not report a root that is neither a component nor an element ([fdf141b](https://github.com/markuplint/markuplint/commit/fdf141b02eb0c9bb26812c4be748feac1c1584d6))
|
|
12
|
+
* **astro-parser:** migrate to astro-eslint-parser v3 ([f76792a](https://github.com/markuplint/markuplint/commit/f76792a01f823bb4e29103d1b4ce7ef6e33c8bda))
|
|
13
|
+
* make published packages loadable from require() and put types first ([723106f](https://github.com/markuplint/markuplint/commit/723106f51aac4e60dc02db2c1a894617368b0253)), closes [#4198](https://github.com/markuplint/markuplint/issues/4198)
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* **astro-parser:** answer what a component name is by the rule of Astro ([5399070](https://github.com/markuplint/markuplint/commit/5399070fa1797cbd502379e9c469aba51693b92b))
|
|
18
|
+
|
|
19
|
+
|
|
6
20
|
# [5.2.0](https://github.com/markuplint/markuplint/compare/v5.1.1...v5.2.0) (2026-10-13)
|
|
7
21
|
|
|
8
22
|
### Bug Fixes
|
package/lib/astro-parser.d.ts
CHANGED
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
/**
|
|
3
|
-
* `@astrojs/compiler` is a dev dependency used only for these type
|
|
4
|
-
* definitions; the runtime AST actually comes from `astro-eslint-parser`.
|
|
5
|
-
* When upgrading `astro-eslint-parser`, also update `@astrojs/compiler` to
|
|
6
|
-
* the version it uses internally, otherwise the types drift from the
|
|
7
|
-
* runtime AST shape.
|
|
8
|
-
*/
|
|
9
|
-
export type { RootNode, ElementNode, CustomElementNode, ComponentNode, FragmentNode, AttributeNode, Node, } from '@astrojs/compiler/types';
|
|
1
|
+
import type { AstroRoot } from './to-nodes.js';
|
|
10
2
|
/**
|
|
11
3
|
* ## Diagnostic handling policy
|
|
12
4
|
*
|
|
@@ -25,8 +17,8 @@ export type { RootNode, ElementNode, CustomElementNode, ComponentNode, FragmentN
|
|
|
25
17
|
*
|
|
26
18
|
* `parseTemplate()` itself raises an `astro-eslint-parser` `ParseError`
|
|
27
19
|
* (extending `SyntaxError`) on severity=Error diagnostics and on raw
|
|
28
|
-
* template syntax errors (
|
|
29
|
-
*
|
|
20
|
+
* template syntax errors (unclosed expressions, mismatched end tags, etc.).
|
|
21
|
+
* Those — and only those — are normalized to `ParserError`, so the
|
|
30
22
|
* caller sees a single Tier-3 (per-file violation) error type for every
|
|
31
23
|
* parse failure. Any other throw, including a genuine `SyntaxError` from a
|
|
32
24
|
* markuplint invariant break, is allowed to propagate so the standard
|
|
@@ -36,10 +28,15 @@ export type { RootNode, ElementNode, CustomElementNode, ComponentNode, FragmentN
|
|
|
36
28
|
* code today (upstream gates internally) but is retained as a safety net
|
|
37
29
|
* if a future `astro-eslint-parser` ever stops gating severity=Error.
|
|
38
30
|
*
|
|
31
|
+
* Two malformed inputs that v3 accepts without any signal — an unterminated
|
|
32
|
+
* frontmatter and an unclosed HTML comment — are detected here and raised as
|
|
33
|
+
* `ParserError` too (see `assertFrontmatterTerminated` and
|
|
34
|
+
* `assertParsedToTheEnd`), so no template is silently skipped.
|
|
35
|
+
*
|
|
39
36
|
* @see https://github.com/markuplint/markuplint/issues/3823
|
|
40
37
|
* @see https://docs.astro.build/en/reference/directives-reference/#isinline
|
|
41
38
|
* @see https://docs.astro.build/en/guides/client-side-scripts/#script-processing
|
|
42
39
|
*
|
|
43
40
|
* @throws {ParserError} on severity=Error Astro diagnostics or on raw upstream syntax errors
|
|
44
41
|
*/
|
|
45
|
-
export declare function astroParse(code: string):
|
|
42
|
+
export declare function astroParse(code: string): AstroRoot;
|
package/lib/astro-parser.js
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import { ParserError } from '@markuplint/parser-utils';
|
|
2
2
|
import { parseTemplate } from 'astro-eslint-parser';
|
|
3
3
|
/**
|
|
4
|
-
* Astro tags each diagnostic with a
|
|
5
|
-
*
|
|
6
|
-
* enum
|
|
7
|
-
* value to import — so the fatal level is mirrored as a literal here.
|
|
8
|
-
*
|
|
9
|
-
* @see https://github.com/withastro/compiler/blob/main/packages/compiler/shared/types.ts
|
|
4
|
+
* Astro tags each diagnostic with a severity of `'error'`, `'warning'`,
|
|
5
|
+
* `'information'` or `'hint'` (`DiagnosticSeverity` of `@astrojs/compiler-binding`,
|
|
6
|
+
* a `const enum`, i.e. types-only), so the fatal level is mirrored as a literal here.
|
|
10
7
|
*/
|
|
11
|
-
const ASTRO_DIAGNOSTIC_SEVERITY_ERROR =
|
|
8
|
+
const ASTRO_DIAGNOSTIC_SEVERITY_ERROR = 'error';
|
|
12
9
|
/**
|
|
13
10
|
* Type guard for `astro-eslint-parser`'s `ParseError` class. The class
|
|
14
11
|
* extends `SyntaxError` and carries a unique `originalAST` instance
|
|
@@ -44,8 +41,8 @@ function isAstroEslintParseError(error) {
|
|
|
44
41
|
*
|
|
45
42
|
* `parseTemplate()` itself raises an `astro-eslint-parser` `ParseError`
|
|
46
43
|
* (extending `SyntaxError`) on severity=Error diagnostics and on raw
|
|
47
|
-
* template syntax errors (
|
|
48
|
-
*
|
|
44
|
+
* template syntax errors (unclosed expressions, mismatched end tags, etc.).
|
|
45
|
+
* Those — and only those — are normalized to `ParserError`, so the
|
|
49
46
|
* caller sees a single Tier-3 (per-file violation) error type for every
|
|
50
47
|
* parse failure. Any other throw, including a genuine `SyntaxError` from a
|
|
51
48
|
* markuplint invariant break, is allowed to propagate so the standard
|
|
@@ -55,6 +52,11 @@ function isAstroEslintParseError(error) {
|
|
|
55
52
|
* code today (upstream gates internally) but is retained as a safety net
|
|
56
53
|
* if a future `astro-eslint-parser` ever stops gating severity=Error.
|
|
57
54
|
*
|
|
55
|
+
* Two malformed inputs that v3 accepts without any signal — an unterminated
|
|
56
|
+
* frontmatter and an unclosed HTML comment — are detected here and raised as
|
|
57
|
+
* `ParserError` too (see `assertFrontmatterTerminated` and
|
|
58
|
+
* `assertParsedToTheEnd`), so no template is silently skipped.
|
|
59
|
+
*
|
|
58
60
|
* @see https://github.com/markuplint/markuplint/issues/3823
|
|
59
61
|
* @see https://docs.astro.build/en/reference/directives-reference/#isinline
|
|
60
62
|
* @see https://docs.astro.build/en/guides/client-side-scripts/#script-processing
|
|
@@ -80,10 +82,57 @@ export function astroParse(code) {
|
|
|
80
82
|
// past the wrapper as a non-fatal pass-through.
|
|
81
83
|
const fatal = result.diagnostics.find(d => d.severity === ASTRO_DIAGNOSTIC_SEVERITY_ERROR);
|
|
82
84
|
if (fatal) {
|
|
85
|
+
// A label's column is 0-based; `ParserError` takes a 1-based one.
|
|
86
|
+
const label = fatal.labels[0];
|
|
83
87
|
throw new ParserError(fatal.text, {
|
|
84
|
-
line:
|
|
85
|
-
col:
|
|
88
|
+
line: label?.line ?? 1,
|
|
89
|
+
col: label ? label.column + 1 : 1,
|
|
86
90
|
});
|
|
87
91
|
}
|
|
92
|
+
assertFrontmatterTerminated(code, result.ast);
|
|
93
|
+
assertParsedToTheEnd(code, result.ast);
|
|
88
94
|
return result.ast;
|
|
89
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* A leading `---` line, which opens a frontmatter block. Spaces may follow the fence
|
|
98
|
+
* (`astro-eslint-parser` accepts `--- ` as one), and the source may end right after it.
|
|
99
|
+
*/
|
|
100
|
+
const RE_FRONTMATTER_FENCE = /^(?:\s*\n)?---[ \t]*(?:\r?\n|$)/;
|
|
101
|
+
/**
|
|
102
|
+
* `astro-eslint-parser` v3 reports an unterminated frontmatter (for example,
|
|
103
|
+
* a closing `---` hidden inside an unterminated block comment) neither
|
|
104
|
+
* as an error nor as a diagnostic: the fence and everything after it silently
|
|
105
|
+
* becomes template text. Every earlier version, and Astro itself, rejects it.
|
|
106
|
+
*
|
|
107
|
+
* @throws {ParserError} when the source starts with a frontmatter fence that no frontmatter was parsed from
|
|
108
|
+
*/
|
|
109
|
+
function assertFrontmatterTerminated(code, ast) {
|
|
110
|
+
const { frontmatter } = ast;
|
|
111
|
+
if (frontmatter && frontmatter.end === frontmatter.start && RE_FRONTMATTER_FENCE.test(code)) {
|
|
112
|
+
throw new ParserError('Unterminated frontmatter', locate(code, 0));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* `astro-eslint-parser` v3 reports an unclosed HTML comment (`<!-- ...`)
|
|
117
|
+
* neither as an error nor as a diagnostic: it drops the comment and
|
|
118
|
+
* everything after it, so a template that is not checked at all would pass.
|
|
119
|
+
* A node list that stops before the end of the source is that situation.
|
|
120
|
+
*
|
|
121
|
+
* Whitespace after the last node is not content: the newline that follows a
|
|
122
|
+
* closing frontmatter fence belongs to no node.
|
|
123
|
+
*
|
|
124
|
+
* @throws {ParserError} when the AST does not reach the end of the source
|
|
125
|
+
*/
|
|
126
|
+
function assertParsedToTheEnd(code, ast) {
|
|
127
|
+
const parsedEnd = ast.body.at(-1)?.end ?? ast.frontmatter?.end ?? 0;
|
|
128
|
+
if (parsedEnd < code.length && code.slice(parsedEnd).trim() !== '') {
|
|
129
|
+
throw new ParserError('Unterminated syntax: the template is not parsed to its end', locate(code, parsedEnd));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* The 1-based line and column of an offset.
|
|
134
|
+
*/
|
|
135
|
+
function locate(code, offset) {
|
|
136
|
+
const lines = code.slice(0, offset).split('\n');
|
|
137
|
+
return { line: lines.length, col: (lines.at(-1)?.length ?? 0) + 1 };
|
|
138
|
+
}
|
package/lib/component-scanner.js
CHANGED
|
@@ -5,6 +5,14 @@ function extractComponentInfo(doc) {
|
|
|
5
5
|
if (!root) {
|
|
6
6
|
return null;
|
|
7
7
|
}
|
|
8
|
+
// The parser says the root is authored, yet its name is not the one of a component
|
|
9
|
+
// (`component`, `slot` of Vue): it renders what it is given, so there is no element
|
|
10
|
+
// to pretend to be. `pretenders` would otherwise emit that name as the target, and
|
|
11
|
+
// nothing tells it from the name of an element. What a component's name is, is the
|
|
12
|
+
// parser's rule (`Parser#isComponentName`).
|
|
13
|
+
if (root.elementType === 'authored' && !parser.isComponentName(root.nodeName)) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
8
16
|
const spreads = extractSpreads(root);
|
|
9
17
|
return {
|
|
10
18
|
rootElement: root.nodeName,
|
package/lib/parser.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Node } from './
|
|
1
|
+
import type { Node } from './to-nodes.js';
|
|
2
2
|
import type { MLASTNodeTreeItem, MLASTParentNode } from '@markuplint/ml-ast';
|
|
3
3
|
import type { ChildToken, Token } from '@markuplint/parser-utils';
|
|
4
4
|
import { Parser } from '@markuplint/parser-utils';
|
|
@@ -106,6 +106,15 @@ declare class AstroParser extends Parser<Node> {
|
|
|
106
106
|
* @param defaultPattern
|
|
107
107
|
*/
|
|
108
108
|
detectElementType(nodeName: string): import("@markuplint/ml-ast").ElementType;
|
|
109
|
+
/**
|
|
110
|
+
* A variable used as a tag must be capitalized in Astro, as `detectElementType` takes it. Unlike the
|
|
111
|
+
* default of the base class, a `.` alone does not make a name a component: `<ui.Item>` is an
|
|
112
|
+
* HTML element there.
|
|
113
|
+
*
|
|
114
|
+
* @param name - A tag name as written, case kept
|
|
115
|
+
* @returns Whether the name is a component's
|
|
116
|
+
*/
|
|
117
|
+
isComponentName(name: string): boolean;
|
|
109
118
|
}
|
|
110
119
|
export declare const parser: AstroParser;
|
|
111
120
|
export {};
|
package/lib/parser.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { AttrState, Parser, ParserError } from '@markuplint/parser-utils';
|
|
1
|
+
import { AttrState, Parser, ParserError, isPotentialCustomElementName } from '@markuplint/parser-utils';
|
|
2
2
|
import { astroParse } from './astro-parser.js';
|
|
3
3
|
import { detectBlockBehavior } from './detect-block-behavior.js';
|
|
4
4
|
import { extractSpreadAttribute } from './spread-attr.js';
|
|
5
|
+
import { toNodes } from './to-nodes.js';
|
|
5
6
|
/**
|
|
6
7
|
* Parser implementation for Astro component templates.
|
|
7
8
|
* Extends the base Parser to handle Astro-specific syntax including frontmatter blocks,
|
|
@@ -26,7 +27,7 @@ class AstroParser extends Parser {
|
|
|
26
27
|
}
|
|
27
28
|
tokenize() {
|
|
28
29
|
return {
|
|
29
|
-
ast: astroParse(this.rawCode)
|
|
30
|
+
ast: toNodes(astroParse(this.rawCode)),
|
|
30
31
|
isFragment: true,
|
|
31
32
|
};
|
|
32
33
|
}
|
|
@@ -43,11 +44,8 @@ class AstroParser extends Parser {
|
|
|
43
44
|
nodeize(
|
|
44
45
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
45
46
|
originNode, parentNode, depth) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
const offset = originNode.position.start.offset;
|
|
50
|
-
const endOffset = originNode.position.end?.offset;
|
|
47
|
+
const offset = originNode.start;
|
|
48
|
+
const endOffset = originNode.end;
|
|
51
49
|
const token = this.sliceFragment(offset, endOffset);
|
|
52
50
|
switch (originNode.type) {
|
|
53
51
|
case 'frontmatter': {
|
|
@@ -86,13 +84,9 @@ class AstroParser extends Parser {
|
|
|
86
84
|
parentNode,
|
|
87
85
|
});
|
|
88
86
|
}
|
|
89
|
-
case 'component':
|
|
90
|
-
case 'custom-element':
|
|
91
|
-
case 'fragment':
|
|
92
87
|
case 'element': {
|
|
93
|
-
const tagToken = token.raw ? token : this.sliceFragment(0);
|
|
94
88
|
return this.visitElement({
|
|
95
|
-
...
|
|
89
|
+
...token,
|
|
96
90
|
depth,
|
|
97
91
|
parentNode,
|
|
98
92
|
}, originNode.children);
|
|
@@ -106,12 +100,12 @@ class AstroParser extends Parser {
|
|
|
106
100
|
const nodes = [];
|
|
107
101
|
let closeExpressionLocation = null;
|
|
108
102
|
if (firstChild && lastChild && firstChild !== lastChild) {
|
|
109
|
-
const startExpressionEndOffset = firstChild.
|
|
103
|
+
const startExpressionEndOffset = firstChild.end;
|
|
110
104
|
const startExpressionLocation = this.sliceFragment(offset, startExpressionEndOffset);
|
|
111
105
|
startExpressionRaw = startExpressionLocation.raw;
|
|
112
106
|
startExpressionStartLine = startExpressionLocation.line;
|
|
113
107
|
startExpressionStartCol = startExpressionLocation.col;
|
|
114
|
-
closeExpressionLocation = this.sliceFragment(lastChild.
|
|
108
|
+
closeExpressionLocation = this.sliceFragment(lastChild.start, endOffset);
|
|
115
109
|
}
|
|
116
110
|
const blockBehavior = detectBlockBehavior(startExpressionRaw);
|
|
117
111
|
nodes.push(...this.visitPsBlock({
|
|
@@ -316,5 +310,16 @@ class AstroParser extends Parser {
|
|
|
316
310
|
detectElementType(nodeName) {
|
|
317
311
|
return super.detectElementType(nodeName, /^[A-Z]/);
|
|
318
312
|
}
|
|
313
|
+
/**
|
|
314
|
+
* A variable used as a tag must be capitalized in Astro, as `detectElementType` takes it. Unlike the
|
|
315
|
+
* default of the base class, a `.` alone does not make a name a component: `<ui.Item>` is an
|
|
316
|
+
* HTML element there.
|
|
317
|
+
*
|
|
318
|
+
* @param name - A tag name as written, case kept
|
|
319
|
+
* @returns Whether the name is a component's
|
|
320
|
+
*/
|
|
321
|
+
isComponentName(name) {
|
|
322
|
+
return /^[A-Z]/.test(name) && !isPotentialCustomElementName(name);
|
|
323
|
+
}
|
|
319
324
|
}
|
|
320
325
|
export const parser = new AstroParser();
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ParseTemplateResult } from 'astro-eslint-parser';
|
|
2
|
+
/**
|
|
3
|
+
* The root `astro-eslint-parser` produces for a template.
|
|
4
|
+
*/
|
|
5
|
+
export type AstroRoot = ParseTemplateResult['result']['ast'];
|
|
6
|
+
/**
|
|
7
|
+
* A node of the Astro template reduced to what {@link import('./parser.js')} needs:
|
|
8
|
+
* its kind, its source range (UTF-16 offsets, `end` exclusive) and the nodes it contains.
|
|
9
|
+
*
|
|
10
|
+
* Attributes are deliberately absent: the parser re-tokenizes the start tag from the
|
|
11
|
+
* source text, so no attribute information is read from the Astro AST.
|
|
12
|
+
*/
|
|
13
|
+
export type Node = (Range & {
|
|
14
|
+
readonly type: 'frontmatter';
|
|
15
|
+
}) | (Range & {
|
|
16
|
+
readonly type: 'doctype';
|
|
17
|
+
readonly value: string;
|
|
18
|
+
}) | (Range & {
|
|
19
|
+
readonly type: 'text';
|
|
20
|
+
}) | (Range & {
|
|
21
|
+
readonly type: 'comment';
|
|
22
|
+
}) | (Range & {
|
|
23
|
+
readonly type: 'element';
|
|
24
|
+
readonly children: readonly Node[];
|
|
25
|
+
}) | (Range & {
|
|
26
|
+
readonly type: 'expression';
|
|
27
|
+
readonly children: readonly Node[];
|
|
28
|
+
});
|
|
29
|
+
interface Range {
|
|
30
|
+
readonly start: number;
|
|
31
|
+
readonly end: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The nodes of the Astro AST as a flat list of top-level {@link Node}s: the frontmatter
|
|
35
|
+
* (when there is one) followed by the body.
|
|
36
|
+
*
|
|
37
|
+
* ## Why a normalization step exists
|
|
38
|
+
*
|
|
39
|
+
* `astro-eslint-parser` v3 reports a JSX-style AST (`JSXElement`, `JSXText`, ...) where the
|
|
40
|
+
* former compiler reported `element` / `component` / `text` nodes. Three of its shapes
|
|
41
|
+
* differ in a way the parser must not see:
|
|
42
|
+
*
|
|
43
|
+
* - The range of an `AstroScript` child is that of the whole `<script>` element, not of its
|
|
44
|
+
* body, so the body range is taken from the surrounding start and end tags.
|
|
45
|
+
* - A JSX node inside an expression (`{items.map(i => <li>{i}</li>)}`) is not a child of the
|
|
46
|
+
* expression container but a descendant of its ESTree expression. The outermost ones are
|
|
47
|
+
* collected, and the code between them is reported as `text` nodes, which is the layout the
|
|
48
|
+
* parser relies on to split an expression into its opening and closing parts.
|
|
49
|
+
* - An absent frontmatter is reported as an empty one.
|
|
50
|
+
*/
|
|
51
|
+
export declare function toNodes(root: AstroRoot): Node[];
|
|
52
|
+
export {};
|
package/lib/to-nodes.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The nodes of the Astro AST as a flat list of top-level {@link Node}s: the frontmatter
|
|
3
|
+
* (when there is one) followed by the body.
|
|
4
|
+
*
|
|
5
|
+
* ## Why a normalization step exists
|
|
6
|
+
*
|
|
7
|
+
* `astro-eslint-parser` v3 reports a JSX-style AST (`JSXElement`, `JSXText`, ...) where the
|
|
8
|
+
* former compiler reported `element` / `component` / `text` nodes. Three of its shapes
|
|
9
|
+
* differ in a way the parser must not see:
|
|
10
|
+
*
|
|
11
|
+
* - The range of an `AstroScript` child is that of the whole `<script>` element, not of its
|
|
12
|
+
* body, so the body range is taken from the surrounding start and end tags.
|
|
13
|
+
* - A JSX node inside an expression (`{items.map(i => <li>{i}</li>)}`) is not a child of the
|
|
14
|
+
* expression container but a descendant of its ESTree expression. The outermost ones are
|
|
15
|
+
* collected, and the code between them is reported as `text` nodes, which is the layout the
|
|
16
|
+
* parser relies on to split an expression into its opening and closing parts.
|
|
17
|
+
* - An absent frontmatter is reported as an empty one.
|
|
18
|
+
*/
|
|
19
|
+
export function toNodes(root) {
|
|
20
|
+
const nodes = [];
|
|
21
|
+
const { frontmatter } = root;
|
|
22
|
+
if (frontmatter && frontmatter.end > frontmatter.start) {
|
|
23
|
+
nodes.push({ type: 'frontmatter', start: frontmatter.start, end: frontmatter.end });
|
|
24
|
+
}
|
|
25
|
+
for (const node of root.body) {
|
|
26
|
+
nodes.push(...convert(node));
|
|
27
|
+
}
|
|
28
|
+
return nodes;
|
|
29
|
+
}
|
|
30
|
+
function convert(node) {
|
|
31
|
+
const { start, end } = node;
|
|
32
|
+
switch (node.type) {
|
|
33
|
+
case 'AstroDoctype': {
|
|
34
|
+
return [{ type: 'doctype', start, end, value: node.value }];
|
|
35
|
+
}
|
|
36
|
+
case 'AstroComment': {
|
|
37
|
+
return [{ type: 'comment', start, end }];
|
|
38
|
+
}
|
|
39
|
+
case 'JSXText': {
|
|
40
|
+
return [{ type: 'text', start, end }];
|
|
41
|
+
}
|
|
42
|
+
case 'JSXElement': {
|
|
43
|
+
return [
|
|
44
|
+
{
|
|
45
|
+
type: 'element',
|
|
46
|
+
start,
|
|
47
|
+
end,
|
|
48
|
+
children: node.children.flatMap(child => {
|
|
49
|
+
if (child.type !== 'AstroScript') {
|
|
50
|
+
return convert(child);
|
|
51
|
+
}
|
|
52
|
+
// An empty body (`<script />`) is still reported as a script: it has no text.
|
|
53
|
+
const bodyStart = node.openingElement.end;
|
|
54
|
+
const bodyEnd = node.closingElement?.start ?? end;
|
|
55
|
+
return bodyEnd > bodyStart ? [{ type: 'text', start: bodyStart, end: bodyEnd }] : [];
|
|
56
|
+
}),
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
}
|
|
60
|
+
case 'JSXFragment': {
|
|
61
|
+
return [{ type: 'element', start, end, children: node.children.flatMap(child => convert(child)) }];
|
|
62
|
+
}
|
|
63
|
+
case 'JSXExpressionContainer': {
|
|
64
|
+
return [{ type: 'expression', start, end, children: expressionChildren(node) }];
|
|
65
|
+
}
|
|
66
|
+
default: {
|
|
67
|
+
// Exhaustive: a node type that `astro-eslint-parser` adds fails the build here
|
|
68
|
+
// instead of silently vanishing from the document.
|
|
69
|
+
node;
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The children of an expression container: the outermost JSX nodes of its expression, with
|
|
76
|
+
* the (non-empty) code between them as `text`. The braces are not part of any child.
|
|
77
|
+
*
|
|
78
|
+
* Empty gaps produce no `text`, as the former compiler did. The parser cuts the opening part of
|
|
79
|
+
* the expression at the end of the first child and the closing part at the start of the last
|
|
80
|
+
* one, so an expression that ends with an element has that element's markup in its closing
|
|
81
|
+
* part (`<input type={type} />}`). That layout is pinned by the `#739` spec in `parser.spec.ts`.
|
|
82
|
+
*/
|
|
83
|
+
function expressionChildren(container) {
|
|
84
|
+
const jsxNodes = collectJsx(container.expression).toSorted((a, b) => a.start - b.start);
|
|
85
|
+
if (jsxNodes.length === 0) {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
const children = [];
|
|
89
|
+
// Skip the opening `{`
|
|
90
|
+
let cursor = container.start + 1;
|
|
91
|
+
for (const jsx of jsxNodes) {
|
|
92
|
+
if (jsx.start > cursor) {
|
|
93
|
+
children.push({ type: 'text', start: cursor, end: jsx.start });
|
|
94
|
+
}
|
|
95
|
+
children.push(...convert(jsx));
|
|
96
|
+
cursor = jsx.end;
|
|
97
|
+
}
|
|
98
|
+
// Stop before the closing `}`
|
|
99
|
+
const innerEnd = container.end - 1;
|
|
100
|
+
if (innerEnd > cursor) {
|
|
101
|
+
children.push({ type: 'text', start: cursor, end: innerEnd });
|
|
102
|
+
}
|
|
103
|
+
return children;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Finds the outermost `JSXElement` / `JSXFragment` nodes in an ESTree subtree. A node found
|
|
107
|
+
* is not descended into: its own descendants are converted when the node itself is.
|
|
108
|
+
*/
|
|
109
|
+
function collectJsx(value) {
|
|
110
|
+
if (typeof value !== 'object' || value === null) {
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
if (Array.isArray(value)) {
|
|
114
|
+
return value.flatMap(item => collectJsx(item));
|
|
115
|
+
}
|
|
116
|
+
if ('type' in value && (value.type === 'JSXElement' || value.type === 'JSXFragment')) {
|
|
117
|
+
return [value];
|
|
118
|
+
}
|
|
119
|
+
return Object.entries(value).flatMap(([key, child]) => (key === 'parent' ? [] : collectJsx(child)));
|
|
120
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markuplint/astro-parser",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.3.0",
|
|
4
4
|
"description": "astro parser for markuplint",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -15,12 +15,12 @@
|
|
|
15
15
|
"type": "module",
|
|
16
16
|
"exports": {
|
|
17
17
|
".": {
|
|
18
|
-
"
|
|
19
|
-
"
|
|
18
|
+
"types": "./lib/index.d.ts",
|
|
19
|
+
"default": "./lib/index.js"
|
|
20
20
|
},
|
|
21
21
|
"./component-scanner": {
|
|
22
|
-
"
|
|
23
|
-
"
|
|
22
|
+
"types": "./lib/component-scanner.d.ts",
|
|
23
|
+
"default": "./lib/component-scanner.js"
|
|
24
24
|
}
|
|
25
25
|
},
|
|
26
26
|
"publishConfig": {
|
|
@@ -32,12 +32,9 @@
|
|
|
32
32
|
"clean": "tsc --build --clean tsconfig.build.json"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@markuplint/ml-ast": "5.
|
|
36
|
-
"@markuplint/parser-utils": "5.
|
|
37
|
-
"astro-eslint-parser": "^
|
|
35
|
+
"@markuplint/ml-ast": "5.3.0",
|
|
36
|
+
"@markuplint/parser-utils": "5.3.0",
|
|
37
|
+
"astro-eslint-parser": "^3.2.0"
|
|
38
38
|
},
|
|
39
|
-
"
|
|
40
|
-
"@astrojs/compiler": "3.0.1"
|
|
41
|
-
},
|
|
42
|
-
"gitHead": "d8b2feab77c9c862513615bea6b80d2817431878"
|
|
39
|
+
"gitHead": "f587cf0206c8426e57369a48d6d5fb22306eafde"
|
|
43
40
|
}
|