eslint-plugin-templ 0.0.1 → 0.0.3
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/README.md +2 -2
- package/dist/configs/recommended.d.ts +57 -0
- package/dist/configs/recommended.d.ts.map +1 -0
- package/dist/configs/recommended.js +49 -0
- package/dist/configs/recommended.js.map +1 -0
- package/dist/html-source-code.d.ts +65 -39
- package/dist/html-source-code.d.ts.map +1 -1
- package/dist/html-source-code.js +112 -60
- package/dist/html-source-code.js.map +1 -1
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/parse-for-eslint.d.ts.map +1 -1
- package/dist/parse-for-eslint.js +163 -25
- package/dist/parse-for-eslint.js.map +1 -1
- package/dist/run-templ-ast-to-json-binary.js +5 -3
- package/dist/run-templ-ast-to-json-binary.js.map +1 -1
- package/dist/templ-ast-to-eslint-ast.d.ts +16 -1
- package/dist/templ-ast-to-eslint-ast.d.ts.map +1 -1
- package/dist/templ-ast-to-eslint-ast.js +515 -117
- package/dist/templ-ast-to-eslint-ast.js.map +1 -1
- package/dist/templ-ast.d.ts +818 -230
- package/dist/templ-ast.d.ts.map +1 -1
- package/dist/templ-ast.js +198 -73
- package/dist/templ-ast.js.map +1 -1
- package/dist/templ-language.d.ts +16 -3
- package/dist/templ-language.d.ts.map +1 -1
- package/dist/templ-language.js +9 -3
- package/dist/templ-language.js.map +1 -1
- package/package.json +27 -23
|
@@ -2,18 +2,39 @@
|
|
|
2
2
|
* Conversion utilities for transforming templ AST to ESLint-compatible HTML AST
|
|
3
3
|
*/
|
|
4
4
|
import { NodeTypes } from "es-html-parser";
|
|
5
|
+
/**
|
|
6
|
+
* Converts a templ AST to an ESLint-compatible HTML AST (DocumentNode).
|
|
7
|
+
*
|
|
8
|
+
* This function is a pure converter — it consumes only the templ AST and does
|
|
9
|
+
* not accept or use source text. All information needed for the conversion must
|
|
10
|
+
* be present in the AST. If something is missing, the fix belongs in the templ
|
|
11
|
+
* parser (github.com/AdamVig/templ), not here.
|
|
12
|
+
*/
|
|
5
13
|
export function convertToESLintAST(templAST) {
|
|
6
14
|
const children = [];
|
|
7
15
|
for (const node of templAST) {
|
|
8
|
-
// Only process HTMLTemplate nodes which have Children
|
|
9
|
-
// Skip FileGoExpression, CSSTemplate, and ScriptTemplate as they don't contain HTML
|
|
10
|
-
if (
|
|
11
|
-
|
|
16
|
+
// Only process HTMLTemplate nodes which have Children.
|
|
17
|
+
// Skip FileGoExpression, CSSTemplate, and ScriptTemplate as they don't contain HTML.
|
|
18
|
+
if (node.type !== "HTMLTemplate") {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
for (const child of node.Children) {
|
|
22
|
+
try {
|
|
12
23
|
const converted = convertChild(child);
|
|
13
24
|
if (converted) {
|
|
14
25
|
children.push(converted);
|
|
26
|
+
// Only Element nodes have TrailingSpace
|
|
27
|
+
if (child.type === "Element" && child.TrailingSpace) {
|
|
28
|
+
const trailingSpace = createTrailingSpaceNode(child);
|
|
29
|
+
if (trailingSpace) {
|
|
30
|
+
children.push(trailingSpace);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
15
33
|
}
|
|
16
34
|
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
throw new TemplASTConversionError(`Failed to convert Templ AST node to ESLint AST${error instanceof Error ? `: ${error.message}` : ""}`, error instanceof TemplASTConversionError ? error.node : child);
|
|
37
|
+
}
|
|
17
38
|
}
|
|
18
39
|
}
|
|
19
40
|
// Calculate overall document range
|
|
@@ -33,94 +54,358 @@ export function convertToESLintAST(templAST) {
|
|
|
33
54
|
};
|
|
34
55
|
}
|
|
35
56
|
function convertChild(child) {
|
|
36
|
-
|
|
37
|
-
|
|
57
|
+
try {
|
|
58
|
+
switch (child.type) {
|
|
59
|
+
case "ScriptElement":
|
|
60
|
+
return convertScriptElement(child);
|
|
61
|
+
case "RawElement":
|
|
62
|
+
return convertRawElement(child);
|
|
63
|
+
case "Element":
|
|
64
|
+
return convertElement(child);
|
|
65
|
+
case "Whitespace":
|
|
66
|
+
case "Text":
|
|
67
|
+
return {
|
|
68
|
+
type: NodeTypes.Text,
|
|
69
|
+
value: child.Value,
|
|
70
|
+
range: convertRange(child.Range),
|
|
71
|
+
loc: convertLoc(child.Range),
|
|
72
|
+
parts: [],
|
|
73
|
+
};
|
|
74
|
+
case "DocType":
|
|
75
|
+
return convertDocType(child);
|
|
76
|
+
case "StringExpression":
|
|
77
|
+
return {
|
|
78
|
+
type: NodeTypes.Text,
|
|
79
|
+
value: child.Expression.Value,
|
|
80
|
+
range: convertRange(child.Expression.Range),
|
|
81
|
+
loc: convertLoc(child.Expression.Range),
|
|
82
|
+
parts: [],
|
|
83
|
+
};
|
|
84
|
+
case "Fallthrough":
|
|
85
|
+
return null;
|
|
86
|
+
default:
|
|
87
|
+
// Ignore Go expressions, call expressions, control flow, etc.
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
38
90
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
91
|
+
catch (error) {
|
|
92
|
+
throw new TemplASTConversionError(`Failed to convert ${child.type} node${error instanceof Error ? `: ${error.message}` : ""}`, error instanceof TemplASTConversionError ? error.node : child);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Creates a text node for an element's trailing space.
|
|
97
|
+
* Elements have a TrailingSpace field that contains whitespace after the closing tag.
|
|
98
|
+
* This whitespace should be represented as a text node sibling.
|
|
99
|
+
*/
|
|
100
|
+
function createTrailingSpaceNode(node) {
|
|
101
|
+
if (!node.TrailingSpace) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
const elementEnd = getElementEndPosition(node);
|
|
105
|
+
// The trailing space starts right after the serialized element ends.
|
|
106
|
+
const startIndex = elementEnd.Index;
|
|
107
|
+
const endIndex = startIndex + node.TrailingSpace.length;
|
|
108
|
+
// Calculate the end position accounting for newlines in the trailing space
|
|
109
|
+
const lines = node.TrailingSpace.split("\n");
|
|
110
|
+
const endLine = elementEnd.Line + lines.length;
|
|
111
|
+
const endCol = lines.length > 1
|
|
112
|
+
? lines[lines.length - 1].length
|
|
113
|
+
: elementEnd.Col + node.TrailingSpace.length;
|
|
114
|
+
return {
|
|
115
|
+
type: NodeTypes.Text,
|
|
116
|
+
value: node.TrailingSpace,
|
|
117
|
+
range: [startIndex, endIndex],
|
|
118
|
+
loc: {
|
|
119
|
+
start: convertPosition(elementEnd),
|
|
120
|
+
end: {
|
|
121
|
+
line: endLine,
|
|
122
|
+
column: endCol,
|
|
49
123
|
},
|
|
50
|
-
|
|
51
|
-
|
|
124
|
+
},
|
|
125
|
+
parts: [],
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function getElementEndPosition(node) {
|
|
129
|
+
return node.CloseTagRange ? node.CloseTagRange.To : node.OpenTagRange.To;
|
|
130
|
+
}
|
|
131
|
+
function getElementRange(node) {
|
|
132
|
+
return {
|
|
133
|
+
From: node.OpenTagRange.From,
|
|
134
|
+
To: getElementEndPosition(node),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function getOpenTagEndRange(node) {
|
|
138
|
+
const end = node.OpenTagRange.To;
|
|
139
|
+
const startOffset = node.SelfClosing ? -2 : -1;
|
|
140
|
+
return {
|
|
141
|
+
From: {
|
|
142
|
+
Index: end.Index + startOffset,
|
|
143
|
+
Line: end.Line,
|
|
144
|
+
Col: end.Col + startOffset,
|
|
145
|
+
},
|
|
146
|
+
To: end,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function convertDocType(node) {
|
|
150
|
+
// templ fmt always writes uppercase <!DOCTYPE, so we hardcode it here.
|
|
151
|
+
// The html/lowercase rule is disabled in the recommended config to match.
|
|
152
|
+
return {
|
|
153
|
+
type: NodeTypes.Doctype,
|
|
154
|
+
range: convertRange(node.Range),
|
|
155
|
+
loc: convertLoc(node.Range),
|
|
156
|
+
open: {
|
|
157
|
+
type: NodeTypes.DoctypeOpen,
|
|
158
|
+
value: "<!DOCTYPE",
|
|
159
|
+
range: convertRange(node.OpenRange),
|
|
160
|
+
loc: convertLoc(node.OpenRange),
|
|
161
|
+
},
|
|
162
|
+
attributes: [
|
|
163
|
+
{
|
|
164
|
+
type: NodeTypes.DoctypeAttribute,
|
|
165
|
+
range: convertRange(node.ValueRange),
|
|
166
|
+
loc: convertLoc(node.ValueRange),
|
|
167
|
+
value: {
|
|
168
|
+
type: NodeTypes.DoctypeAttributeValue,
|
|
169
|
+
value: node.Value,
|
|
170
|
+
range: convertRange(node.ValueRange),
|
|
171
|
+
loc: convertLoc(node.ValueRange),
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
close: {
|
|
176
|
+
type: NodeTypes.DoctypeClose,
|
|
177
|
+
value: ">",
|
|
178
|
+
range: convertRange(node.CloseRange),
|
|
179
|
+
loc: convertLoc(node.CloseRange),
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function convertElement(node) {
|
|
184
|
+
const elementRange = getElementRange(node);
|
|
185
|
+
const range = convertRange(elementRange);
|
|
186
|
+
const loc = convertLoc(elementRange);
|
|
187
|
+
const openStart = {
|
|
188
|
+
type: NodeTypes.OpenTagStart,
|
|
189
|
+
value: `<${node.Name}`,
|
|
190
|
+
range: convertRange({
|
|
191
|
+
From: node.OpenTagRange.From,
|
|
192
|
+
To: node.NameRange.To,
|
|
193
|
+
}),
|
|
194
|
+
loc: convertLoc({
|
|
195
|
+
From: node.OpenTagRange.From,
|
|
196
|
+
To: node.NameRange.To,
|
|
197
|
+
}),
|
|
198
|
+
};
|
|
199
|
+
const openTagEndRange = getOpenTagEndRange(node);
|
|
200
|
+
const openEnd = {
|
|
201
|
+
type: NodeTypes.OpenTagEnd,
|
|
202
|
+
value: node.SelfClosing ? "/>" : ">",
|
|
203
|
+
range: convertRange(openTagEndRange),
|
|
204
|
+
loc: convertLoc(openTagEndRange),
|
|
205
|
+
};
|
|
206
|
+
let closeRange;
|
|
207
|
+
let closeLoc;
|
|
208
|
+
if (node.CloseTagRange) {
|
|
209
|
+
closeRange = convertRange(node.CloseTagRange);
|
|
210
|
+
closeLoc = convertLoc(node.CloseTagRange);
|
|
52
211
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
212
|
+
else {
|
|
213
|
+
const elementEnd = getElementEndPosition(node);
|
|
214
|
+
closeRange = [elementEnd.Index, elementEnd.Index];
|
|
215
|
+
const closePosition = convertPosition(elementEnd);
|
|
216
|
+
closeLoc = {
|
|
217
|
+
start: closePosition,
|
|
218
|
+
end: closePosition,
|
|
60
219
|
};
|
|
61
220
|
}
|
|
62
|
-
|
|
63
|
-
|
|
221
|
+
const close = {
|
|
222
|
+
type: NodeTypes.CloseTag,
|
|
223
|
+
value: `</${node.Name}>`,
|
|
224
|
+
range: closeRange,
|
|
225
|
+
loc: closeLoc,
|
|
226
|
+
};
|
|
227
|
+
let attributes;
|
|
228
|
+
try {
|
|
229
|
+
attributes = convertAttributes(node.Attributes);
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
throw new TemplASTConversionError(`Failed to convert attributes${error instanceof Error ? `: ${error.message}` : ""}`, error instanceof TemplASTConversionError ? error.node : node);
|
|
233
|
+
}
|
|
234
|
+
const children = [];
|
|
235
|
+
try {
|
|
236
|
+
if (node.Children) {
|
|
237
|
+
for (const child of node.Children) {
|
|
238
|
+
const converted = convertChild(child);
|
|
239
|
+
if (converted && converted.type !== NodeTypes.Doctype) {
|
|
240
|
+
children.push(converted);
|
|
241
|
+
if (child.type === "Element" && child.TrailingSpace) {
|
|
242
|
+
const trailingSpace = createTrailingSpaceNode(child);
|
|
243
|
+
if (trailingSpace) {
|
|
244
|
+
children.push(trailingSpace);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
throw new TemplASTConversionError(`Failed to convert element children${error instanceof Error ? `: ${error.message}` : ""}`, error instanceof TemplASTConversionError ? error.node : node);
|
|
253
|
+
}
|
|
254
|
+
// Determine the appropriate node type based on element name
|
|
255
|
+
const lowerName = node.Name.toLowerCase();
|
|
256
|
+
const nodeType = lowerName === "script"
|
|
257
|
+
? NodeTypes.ScriptTag
|
|
258
|
+
: lowerName === "style"
|
|
259
|
+
? NodeTypes.StyleTag
|
|
260
|
+
: NodeTypes.Tag;
|
|
261
|
+
// ScriptTagNode and StyleTagNode are separate types in es-html-parser
|
|
262
|
+
// but html-eslint rules accept them interchangeably with TagNode.
|
|
263
|
+
return {
|
|
264
|
+
type: nodeType,
|
|
265
|
+
name: node.Name,
|
|
266
|
+
selfClosing: node.SelfClosing,
|
|
267
|
+
openStart,
|
|
268
|
+
openEnd,
|
|
269
|
+
close,
|
|
270
|
+
attributes,
|
|
271
|
+
children,
|
|
272
|
+
range,
|
|
273
|
+
loc,
|
|
274
|
+
};
|
|
64
275
|
}
|
|
65
|
-
function
|
|
276
|
+
function getContentRange(openTagRange, closeTagRange) {
|
|
277
|
+
return {
|
|
278
|
+
From: openTagRange.To,
|
|
279
|
+
To: closeTagRange.From,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
function convertScriptElement(node) {
|
|
283
|
+
const range = convertRange(node.Range);
|
|
284
|
+
const loc = convertLoc(node.Range);
|
|
285
|
+
const scriptOpenStartRange = getLiteralRange(node.OpenTagRange.From, "<script");
|
|
286
|
+
const contentRange = getContentRange(node.OpenTagRange, node.CloseTagRange);
|
|
287
|
+
// Extract text content from script element
|
|
288
|
+
const scriptContents = node.Contents ?? [];
|
|
289
|
+
const textContent = scriptContents
|
|
290
|
+
.map((content) => content.Value ?? "")
|
|
291
|
+
.join("");
|
|
292
|
+
const openStart = {
|
|
293
|
+
type: NodeTypes.OpenScriptTagStart,
|
|
294
|
+
value: `<script`,
|
|
295
|
+
range: convertRange(scriptOpenStartRange),
|
|
296
|
+
loc: convertLoc(scriptOpenStartRange),
|
|
297
|
+
};
|
|
298
|
+
const openTagEndRange = getOpenTagEndRange(node);
|
|
299
|
+
const openEnd = {
|
|
300
|
+
type: NodeTypes.OpenScriptTagEnd,
|
|
301
|
+
value: ">",
|
|
302
|
+
range: convertRange(openTagEndRange),
|
|
303
|
+
loc: convertLoc(openTagEndRange),
|
|
304
|
+
};
|
|
305
|
+
const close = {
|
|
306
|
+
type: NodeTypes.CloseScriptTag,
|
|
307
|
+
value: `</script>`,
|
|
308
|
+
range: convertRange(node.CloseTagRange),
|
|
309
|
+
loc: convertLoc(node.CloseTagRange),
|
|
310
|
+
};
|
|
311
|
+
const attributes = convertAttributes(node.Attributes);
|
|
312
|
+
const value = textContent
|
|
313
|
+
? {
|
|
314
|
+
type: NodeTypes.ScriptTagContent,
|
|
315
|
+
value: textContent,
|
|
316
|
+
range: convertRange(contentRange),
|
|
317
|
+
loc: convertLoc(contentRange),
|
|
318
|
+
parts: [],
|
|
319
|
+
}
|
|
320
|
+
: undefined;
|
|
321
|
+
return {
|
|
322
|
+
type: NodeTypes.ScriptTag,
|
|
323
|
+
openStart,
|
|
324
|
+
openEnd,
|
|
325
|
+
close,
|
|
326
|
+
attributes,
|
|
327
|
+
...(value && { value }),
|
|
328
|
+
range,
|
|
329
|
+
loc,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
function convertRawElement(node) {
|
|
66
333
|
const range = convertRange(node.Range);
|
|
67
334
|
const loc = convertLoc(node.Range);
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
335
|
+
const lowerName = node.Name.toLowerCase();
|
|
336
|
+
const contentRange = getContentRange(node.OpenTagRange, node.CloseTagRange);
|
|
337
|
+
const openTagEndRange = getOpenTagEndRange(node);
|
|
338
|
+
if (lowerName === "style") {
|
|
339
|
+
const openStart = {
|
|
340
|
+
type: NodeTypes.OpenStyleTagStart,
|
|
341
|
+
value: `<${node.Name}`,
|
|
342
|
+
range: convertRange({
|
|
343
|
+
From: node.OpenTagRange.From,
|
|
344
|
+
To: node.NameRange.To,
|
|
345
|
+
}),
|
|
346
|
+
loc: convertLoc({
|
|
347
|
+
From: node.OpenTagRange.From,
|
|
348
|
+
To: node.NameRange.To,
|
|
349
|
+
}),
|
|
350
|
+
};
|
|
351
|
+
const openEnd = {
|
|
352
|
+
type: NodeTypes.OpenStyleTagEnd,
|
|
353
|
+
value: ">",
|
|
354
|
+
range: convertRange(openTagEndRange),
|
|
355
|
+
loc: convertLoc(openTagEndRange),
|
|
356
|
+
};
|
|
357
|
+
const close = {
|
|
358
|
+
type: NodeTypes.CloseStyleTag,
|
|
359
|
+
value: `</${node.Name}>`,
|
|
360
|
+
range: convertRange(node.CloseTagRange),
|
|
361
|
+
loc: convertLoc(node.CloseTagRange),
|
|
362
|
+
};
|
|
363
|
+
const attributes = convertAttributes(node.Attributes);
|
|
364
|
+
const value = node.Contents
|
|
365
|
+
? {
|
|
366
|
+
type: NodeTypes.StyleTagContent,
|
|
367
|
+
value: node.Contents,
|
|
368
|
+
range: convertRange(contentRange),
|
|
369
|
+
loc: convertLoc(contentRange),
|
|
370
|
+
parts: [],
|
|
371
|
+
}
|
|
372
|
+
: undefined;
|
|
373
|
+
return {
|
|
374
|
+
type: NodeTypes.StyleTag,
|
|
375
|
+
openStart,
|
|
376
|
+
openEnd,
|
|
377
|
+
close,
|
|
378
|
+
attributes,
|
|
379
|
+
...(value && { value }),
|
|
380
|
+
range,
|
|
381
|
+
loc,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
72
384
|
const openStart = {
|
|
73
385
|
type: NodeTypes.OpenTagStart,
|
|
74
386
|
value: `<${node.Name}`,
|
|
75
|
-
range:
|
|
387
|
+
range: convertRange({
|
|
388
|
+
From: node.OpenTagRange.From,
|
|
389
|
+
To: node.NameRange.To,
|
|
390
|
+
}),
|
|
76
391
|
loc: convertLoc({
|
|
77
|
-
From: node.
|
|
392
|
+
From: node.OpenTagRange.From,
|
|
78
393
|
To: node.NameRange.To,
|
|
79
394
|
}),
|
|
80
395
|
};
|
|
81
|
-
// Estimate the position of the closing > of the opening tag
|
|
82
|
-
const lastAttr = node.Attributes && node.Attributes.length > 0
|
|
83
|
-
? node.Attributes[node.Attributes.length - 1]
|
|
84
|
-
: null;
|
|
85
|
-
const openEndStart = lastAttr
|
|
86
|
-
? lastAttr.Key.NameRange.To.Index +
|
|
87
|
-
2 +
|
|
88
|
-
(lastAttr.Value?.length ?? lastAttr.Expression?.Value.length ?? 0) +
|
|
89
|
-
1
|
|
90
|
-
: node.NameRange.To.Index;
|
|
91
396
|
const openEnd = {
|
|
92
397
|
type: NodeTypes.OpenTagEnd,
|
|
93
398
|
value: ">",
|
|
94
|
-
range:
|
|
95
|
-
loc:
|
|
96
|
-
start: {
|
|
97
|
-
line: node.NameRange.To.Line + 1,
|
|
98
|
-
column: node.NameRange.To.Col,
|
|
99
|
-
},
|
|
100
|
-
end: {
|
|
101
|
-
line: node.NameRange.To.Line + 1,
|
|
102
|
-
column: node.NameRange.To.Col + 1,
|
|
103
|
-
},
|
|
104
|
-
},
|
|
399
|
+
range: convertRange(openTagEndRange),
|
|
400
|
+
loc: convertLoc(openTagEndRange),
|
|
105
401
|
};
|
|
106
|
-
// Estimate close tag position (at the end of the element range)
|
|
107
|
-
const closeTagStart = node.Range.To.Index - (node.Name.length + 3); // -3 for </>
|
|
108
402
|
const close = {
|
|
109
403
|
type: NodeTypes.CloseTag,
|
|
110
404
|
value: `</${node.Name}>`,
|
|
111
|
-
range:
|
|
112
|
-
loc:
|
|
113
|
-
start: {
|
|
114
|
-
line: node.Range.To.Line + 1,
|
|
115
|
-
column: Math.max(0, node.Range.To.Col - (node.Name.length + 3)),
|
|
116
|
-
},
|
|
117
|
-
end: convertPosition(node.Range.To),
|
|
118
|
-
},
|
|
405
|
+
range: convertRange(node.CloseTagRange),
|
|
406
|
+
loc: convertLoc(node.CloseTagRange),
|
|
119
407
|
};
|
|
120
|
-
const attributes = node.Attributes
|
|
121
|
-
? node.Attributes.map(convertAttribute)
|
|
122
|
-
: [];
|
|
123
|
-
const children = node.Children.map(convertChild).filter((child) => child !== null);
|
|
408
|
+
const attributes = convertAttributes(node.Attributes);
|
|
124
409
|
return {
|
|
125
410
|
type: NodeTypes.Tag,
|
|
126
411
|
name: node.Name,
|
|
@@ -129,12 +414,51 @@ function convertElement(node) {
|
|
|
129
414
|
openEnd,
|
|
130
415
|
close,
|
|
131
416
|
attributes,
|
|
132
|
-
children,
|
|
417
|
+
children: [],
|
|
133
418
|
range,
|
|
134
419
|
loc,
|
|
135
420
|
};
|
|
136
421
|
}
|
|
422
|
+
function convertAttributes(attrs) {
|
|
423
|
+
if (!attrs) {
|
|
424
|
+
return [];
|
|
425
|
+
}
|
|
426
|
+
const converted = [];
|
|
427
|
+
for (const attr of attrs) {
|
|
428
|
+
switch (attr.type) {
|
|
429
|
+
case "ConditionalAttribute":
|
|
430
|
+
converted.push(...convertAttributes(attr.Then));
|
|
431
|
+
if (attr.Else) {
|
|
432
|
+
converted.push(...convertAttributes(attr.Else));
|
|
433
|
+
}
|
|
434
|
+
break;
|
|
435
|
+
case "SpreadAttributes":
|
|
436
|
+
// Spread attributes cannot be represented in the ESLint HTML AST.
|
|
437
|
+
break;
|
|
438
|
+
case "BoolConstantAttribute":
|
|
439
|
+
case "BoolExpressionAttribute":
|
|
440
|
+
case "ConstantAttribute":
|
|
441
|
+
case "ExpressionAttribute":
|
|
442
|
+
if (attr.Key.type !== "ConstantAttributeKey") {
|
|
443
|
+
throw new Error("Expression-based attribute keys are not supported in ESLint conversion");
|
|
444
|
+
}
|
|
445
|
+
try {
|
|
446
|
+
converted.push(convertAttribute(attr));
|
|
447
|
+
}
|
|
448
|
+
catch (error) {
|
|
449
|
+
throw new TemplASTConversionError(`Failed to convert attribute${error instanceof Error ? `: ${error.message}` : ""}`, error instanceof TemplASTConversionError ? error.node : attr);
|
|
450
|
+
}
|
|
451
|
+
break;
|
|
452
|
+
default:
|
|
453
|
+
throw new Error("Unsupported attribute structure");
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return converted;
|
|
457
|
+
}
|
|
137
458
|
function convertAttribute(attr) {
|
|
459
|
+
if (attr.Key.type !== "ConstantAttributeKey") {
|
|
460
|
+
throw new Error("Expression-based attribute keys are not supported in ESLint conversion");
|
|
461
|
+
}
|
|
138
462
|
const keyRange = convertRange(attr.Key.NameRange);
|
|
139
463
|
const keyLoc = convertLoc(attr.Key.NameRange);
|
|
140
464
|
const key = {
|
|
@@ -144,45 +468,97 @@ function convertAttribute(attr) {
|
|
|
144
468
|
loc: keyLoc,
|
|
145
469
|
parts: [],
|
|
146
470
|
};
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
:
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
471
|
+
let value;
|
|
472
|
+
let startWrapper;
|
|
473
|
+
let endWrapper;
|
|
474
|
+
switch (attr.type) {
|
|
475
|
+
case "ConstantAttribute":
|
|
476
|
+
value = {
|
|
477
|
+
type: NodeTypes.AttributeValue,
|
|
478
|
+
value: attr.Value,
|
|
479
|
+
range: convertRange(attr.ValueRange),
|
|
480
|
+
loc: convertLoc(attr.ValueRange),
|
|
481
|
+
parts: [],
|
|
482
|
+
};
|
|
483
|
+
({ startWrapper, endWrapper } = createAttributeValueWrappers(attr));
|
|
484
|
+
break;
|
|
485
|
+
case "ExpressionAttribute":
|
|
486
|
+
value = createDynamicAttributeValue(attr.AttributeStartRange, attr.Expression);
|
|
487
|
+
break;
|
|
488
|
+
case "BoolExpressionAttribute":
|
|
489
|
+
value = createDynamicAttributeValue(getBoolExpressionAttributeValueRange(attr), attr.Expression);
|
|
490
|
+
break;
|
|
491
|
+
case "BoolConstantAttribute":
|
|
492
|
+
// Boolean attributes have no value
|
|
493
|
+
break;
|
|
494
|
+
}
|
|
495
|
+
const hasValue = value !== undefined;
|
|
496
|
+
const attrRange = hasValue
|
|
497
|
+
? convertRange(attr.Range)
|
|
498
|
+
: keyRange;
|
|
174
499
|
return {
|
|
175
500
|
type: NodeTypes.Attribute,
|
|
176
501
|
key,
|
|
177
|
-
value,
|
|
502
|
+
...(value && { value }), // Only include value if it exists
|
|
503
|
+
...(startWrapper && { startWrapper }),
|
|
504
|
+
...(endWrapper && { endWrapper }),
|
|
178
505
|
range: attrRange,
|
|
179
|
-
loc:
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
506
|
+
loc: hasValue ? convertLoc(attr.Range) : keyLoc,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
function createAttributeValueWrappers(attr) {
|
|
510
|
+
const attrRange = convertRange(attr.Range);
|
|
511
|
+
const valueRange = convertRange(attr.ValueRange);
|
|
512
|
+
if (valueRange[0] - 1 < attrRange[0] || valueRange[1] + 1 > attrRange[1]) {
|
|
513
|
+
return {};
|
|
514
|
+
}
|
|
515
|
+
const wrapperValue = attr.SingleQuote ? "'" : '"';
|
|
516
|
+
const startRange = {
|
|
517
|
+
From: offsetPosition(attr.ValueRange.From, -1),
|
|
518
|
+
To: attr.ValueRange.From,
|
|
519
|
+
};
|
|
520
|
+
const endRange = {
|
|
521
|
+
From: attr.ValueRange.To,
|
|
522
|
+
To: offsetPosition(attr.ValueRange.To, 1),
|
|
523
|
+
};
|
|
524
|
+
return {
|
|
525
|
+
startWrapper: {
|
|
526
|
+
type: NodeTypes.AttributeValueWrapperStart,
|
|
527
|
+
value: wrapperValue,
|
|
528
|
+
range: convertRange(startRange),
|
|
529
|
+
loc: convertLoc(startRange),
|
|
185
530
|
},
|
|
531
|
+
endWrapper: {
|
|
532
|
+
type: NodeTypes.AttributeValueWrapperEnd,
|
|
533
|
+
value: wrapperValue,
|
|
534
|
+
range: convertRange(endRange),
|
|
535
|
+
loc: convertLoc(endRange),
|
|
536
|
+
},
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
function createDynamicAttributeValue(range, expression) {
|
|
540
|
+
const templatePart = {
|
|
541
|
+
type: NodeTypes.Template,
|
|
542
|
+
partOf: NodeTypes.AttributeValue,
|
|
543
|
+
value: expression.Value,
|
|
544
|
+
range: convertRange(expression.Range),
|
|
545
|
+
loc: convertLoc(expression.Range),
|
|
546
|
+
};
|
|
547
|
+
return {
|
|
548
|
+
type: NodeTypes.AttributeValue,
|
|
549
|
+
value: expression.Value,
|
|
550
|
+
range: convertRange(range),
|
|
551
|
+
loc: convertLoc(range),
|
|
552
|
+
parts: [templatePart],
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
function getBoolExpressionAttributeValueRange(attr) {
|
|
556
|
+
if (attr.Key.type !== "ConstantAttributeKey") {
|
|
557
|
+
throw new Error("Expression-based attribute keys are not supported in ESLint conversion");
|
|
558
|
+
}
|
|
559
|
+
return {
|
|
560
|
+
From: attr.Key.NameRange.To,
|
|
561
|
+
To: attr.Range.To,
|
|
186
562
|
};
|
|
187
563
|
}
|
|
188
564
|
function convertLoc(range) {
|
|
@@ -191,6 +567,13 @@ function convertLoc(range) {
|
|
|
191
567
|
end: convertPosition(range.To),
|
|
192
568
|
};
|
|
193
569
|
}
|
|
570
|
+
function offsetPosition(position, offset) {
|
|
571
|
+
return {
|
|
572
|
+
...position,
|
|
573
|
+
Index: position.Index + offset,
|
|
574
|
+
Col: position.Col + offset,
|
|
575
|
+
};
|
|
576
|
+
}
|
|
194
577
|
function convertRange(range) {
|
|
195
578
|
return [range.From.Index, range.To.Index];
|
|
196
579
|
}
|
|
@@ -200,19 +583,34 @@ function convertPosition(pos) {
|
|
|
200
583
|
column: pos.Col,
|
|
201
584
|
};
|
|
202
585
|
}
|
|
203
|
-
function
|
|
204
|
-
return
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
586
|
+
function getLiteralRange(from, value) {
|
|
587
|
+
return {
|
|
588
|
+
From: from,
|
|
589
|
+
To: {
|
|
590
|
+
Index: from.Index + value.length,
|
|
591
|
+
Line: from.Line,
|
|
592
|
+
Col: from.Col + value.length,
|
|
593
|
+
},
|
|
594
|
+
};
|
|
211
595
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
596
|
+
export class TemplASTConversionError extends SyntaxError {
|
|
597
|
+
name = "TemplASTConversionError";
|
|
598
|
+
line;
|
|
599
|
+
column;
|
|
600
|
+
node;
|
|
601
|
+
constructor(message, node, options) {
|
|
602
|
+
super(message, options);
|
|
603
|
+
this.node = node;
|
|
604
|
+
// Extract position from node if it has a Range property, otherwise default to line 1, column 0
|
|
605
|
+
if ("Range" in node && node.Range) {
|
|
606
|
+
const position = convertPosition(node.Range.From);
|
|
607
|
+
this.line = position.line;
|
|
608
|
+
this.column = position.column;
|
|
609
|
+
}
|
|
610
|
+
else {
|
|
611
|
+
this.line = 1;
|
|
612
|
+
this.column = 0;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
217
615
|
}
|
|
218
616
|
//# sourceMappingURL=templ-ast-to-eslint-ast.js.map
|