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.
@@ -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,358 @@ 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
+ case "Fallthrough":
85
+ return null;
86
+ default:
87
+ // Ignore Go expressions, call expressions, control flow, etc.
88
+ return null;
89
+ }
38
90
  }
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 },
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
- parts: [],
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
- 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: [],
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
- // For now, ignore Go expressions and call expressions
63
- return null;
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 convertElement(node) {
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 openStartRange = convertRange({
69
- From: node.Range.From,
70
- To: node.NameRange.To,
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: openStartRange,
387
+ range: convertRange({
388
+ From: node.OpenTagRange.From,
389
+ To: node.NameRange.To,
390
+ }),
76
391
  loc: convertLoc({
77
- From: node.Range.From,
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: [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
- },
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: [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
- },
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
- // 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 }
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
- start: keyLoc.start,
181
- end: {
182
- line: value.loc.end.line,
183
- column: value.loc.end.column + 1,
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 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));
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
- function isStringExpression(node) {
213
- return ("Value" in node &&
214
- "Range" in node &&
215
- "TrailingSpace" in node &&
216
- !("Expression" in node));
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