eslint-plugin-templ 0.0.1 → 0.0.2

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