@nuasite/cms-marker 0.0.82 → 0.0.83

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.
Files changed (37) hide show
  1. package/dist/types/build-processor.d.ts.map +1 -1
  2. package/dist/types/html-processor.d.ts.map +1 -1
  3. package/dist/types/index.d.ts +1 -1
  4. package/dist/types/index.d.ts.map +1 -1
  5. package/dist/types/source-finder/ast-extractors.d.ts.map +1 -1
  6. package/dist/types/source-finder/cross-file-tracker.d.ts +10 -0
  7. package/dist/types/source-finder/cross-file-tracker.d.ts.map +1 -1
  8. package/dist/types/source-finder/index.d.ts +2 -1
  9. package/dist/types/source-finder/index.d.ts.map +1 -1
  10. package/dist/types/source-finder/search-index.d.ts.map +1 -1
  11. package/dist/types/source-finder/snippet-utils.d.ts +43 -2
  12. package/dist/types/source-finder/snippet-utils.d.ts.map +1 -1
  13. package/dist/types/source-finder/source-lookup.d.ts.map +1 -1
  14. package/dist/types/source-finder/types.d.ts +4 -0
  15. package/dist/types/source-finder/types.d.ts.map +1 -1
  16. package/dist/types/tailwind-colors.d.ts +5 -3
  17. package/dist/types/tailwind-colors.d.ts.map +1 -1
  18. package/dist/types/tsconfig.tsbuildinfo +1 -1
  19. package/dist/types/types.d.ts +16 -271
  20. package/dist/types/types.d.ts.map +1 -1
  21. package/dist/types/utils.d.ts +2 -4
  22. package/dist/types/utils.d.ts.map +1 -1
  23. package/package.json +1 -1
  24. package/src/build-processor.ts +54 -5
  25. package/src/dev-middleware.ts +2 -2
  26. package/src/html-processor.ts +153 -714
  27. package/src/index.ts +1 -14
  28. package/src/source-finder/ast-extractors.ts +17 -7
  29. package/src/source-finder/cross-file-tracker.ts +405 -2
  30. package/src/source-finder/index.ts +12 -1
  31. package/src/source-finder/search-index.ts +52 -0
  32. package/src/source-finder/snippet-utils.ts +270 -14
  33. package/src/source-finder/source-lookup.ts +5 -1
  34. package/src/source-finder/types.ts +4 -0
  35. package/src/tailwind-colors.ts +49 -48
  36. package/src/types.ts +16 -284
  37. package/src/utils.ts +1 -6
package/src/index.ts CHANGED
@@ -128,46 +128,33 @@ export type { CollectionInfo, MarkdownContent, SourceLocation, VariableReference
128
128
  // Re-export types for consumers
129
129
  export { findCollectionSource, parseMarkdownContent } from './source-finder'
130
130
  export type {
131
- AriaAttributes,
131
+ Attribute,
132
132
  AvailableColors,
133
133
  AvailableTextStyles,
134
- ButtonAttributes,
135
134
  CanonicalUrl,
136
135
  CmsManifest,
137
136
  CmsMarkerOptions,
138
137
  CollectionDefinition,
139
138
  CollectionEntry,
140
- ColorClasses,
141
139
  ComponentDefinition,
142
140
  ComponentInstance,
143
141
  ComponentProp,
144
142
  ContentConstraints,
145
- DataAttributes,
146
143
  FieldDefinition,
147
144
  FieldType,
148
- FormAttributes,
149
- GradientClasses,
150
- IframeAttributes,
151
145
  ImageMetadata,
152
- InputAttributes,
153
146
  JsonLdEntry,
154
- LinkAttributes,
155
147
  ManifestEntry,
156
148
  ManifestMetadata,
157
- MediaAttributes,
158
- OpacityClasses,
159
149
  OpenGraphData,
160
150
  PageEntry,
161
151
  PageSeoData,
162
- SelectAttributes,
163
152
  SeoKeywords,
164
153
  SeoMetaTag,
165
154
  SeoOptions,
166
155
  SeoSourceInfo,
167
156
  SeoTitle,
168
- SourceContext,
169
157
  TailwindColor,
170
- TextareaAttributes,
171
158
  TextStyleValue,
172
159
  TwitterCardData,
173
160
  } from './types'
@@ -25,6 +25,16 @@ export function getStringValue(node: BabelNode): string | null {
25
25
  // Object and Array Extraction
26
26
  // ============================================================================
27
27
 
28
+ /**
29
+ * Extract property name from an object key node.
30
+ * Handles both `{ name: value }` (Identifier) and `{ "name": value }` (StringLiteral).
31
+ */
32
+ function getKeyName(key: BabelNode): string | null {
33
+ if (key.type === 'Identifier') return key.name as string
34
+ if (key.type === 'StringLiteral') return key.value as string
35
+ return null
36
+ }
37
+
28
38
  /**
29
39
  * Recursively extract properties from an object expression
30
40
  * @param objNode - The ObjectExpression node
@@ -43,9 +53,9 @@ export function extractObjectProperties(
43
53
  if (prop.type !== 'ObjectProperty') continue
44
54
  const key = prop.key as BabelNode | undefined
45
55
  const value = prop.value as BabelNode | undefined
46
- if (!key || key.type !== 'Identifier' || !value) continue
47
-
48
- const propName = key.name as string
56
+ if (!key || !value) continue
57
+ const propName = getKeyName(key)
58
+ if (!propName) continue
49
59
  const fullPath = `${parentPath}.${propName}`
50
60
  const propLoc = prop.loc as { start: { line: number } } | undefined
51
61
  const propLine = lineTransformer(propLoc?.start.line ?? 1)
@@ -107,16 +117,16 @@ export function extractArrayElements(
107
117
  })
108
118
  }
109
119
 
110
- // Handle array of objects: [{ text: 'Home' }]
120
+ // Handle array of objects: [{ text: 'Home' }] or [{ "text": 'Home' }]
111
121
  if (elem.type === 'ObjectExpression') {
112
122
  const objProperties = elem.properties as BabelNode[] | undefined
113
123
  for (const prop of objProperties ?? []) {
114
124
  if (prop.type !== 'ObjectProperty') continue
115
125
  const key = prop.key as BabelNode | undefined
116
126
  const value = prop.value as BabelNode | undefined
117
- if (!key || key.type !== 'Identifier' || !value) continue
118
-
119
- const propName = key.name as string
127
+ if (!key || !value) continue
128
+ const propName = getKeyName(key)
129
+ if (!propName) continue
120
130
  const propLoc = prop.loc as { start: { line: number } } | undefined
121
131
  const propLine = propLoc ? lineTransformer(propLoc.start.line) : elemLine
122
132
 
@@ -2,11 +2,11 @@ import fs from 'node:fs/promises'
2
2
  import path from 'node:path'
3
3
 
4
4
  import { getProjectRoot } from '../config'
5
- import { buildDefinitionPath } from './ast-extractors'
5
+ import { buildDefinitionPath, parseExpressionPath } from './ast-extractors'
6
6
  import { getCachedParsedFile } from './ast-parser'
7
7
  import { findComponentProp, findExpressionProp, findSpreadProp } from './element-finder'
8
8
  import { normalizeText } from './snippet-utils'
9
- import type { ImportInfo, SourceLocation } from './types'
9
+ import type { ImportInfo, SourceLocation, VariableDefinition } from './types'
10
10
  import { getExportedDefinitions, resolveImportPath } from './variable-extraction'
11
11
 
12
12
  // ============================================================================
@@ -335,3 +335,406 @@ export async function searchForPropInParents(dir: string, textContent: string):
335
335
 
336
336
  return undefined
337
337
  }
338
+
339
+ // ============================================================================
340
+ // Attribute Source Location Finding
341
+ // ============================================================================
342
+
343
+ /**
344
+ * Find the actual source location for a dynamic attribute value.
345
+ * Uses the resolved VALUE to search for where it's defined (handles loop variables, etc.)
346
+ *
347
+ * @param expression - The source expression (e.g., "component.githubUrl")
348
+ * @param resolvedValue - The actual resolved value from the rendered HTML
349
+ * @param sourceFilePath - The source file path where the attribute is used (relative to project root)
350
+ * @returns Source location with file, line, and snippet for the actual value definition
351
+ */
352
+ export async function findAttributeSourceLocation(
353
+ expression: string,
354
+ resolvedValue: string,
355
+ sourceFilePath: string,
356
+ ): Promise<SourceLocation | undefined> {
357
+ // Parse the expression to get property name (e.g., "githubUrl" from "component.githubUrl")
358
+ const exprPath = parseExpressionPath(expression)
359
+ if (!exprPath) return undefined
360
+
361
+ // Get the property name (last part of the expression)
362
+ const propName = exprPath.includes('.') ? exprPath.split('.').pop()! : exprPath
363
+
364
+ const filePath = path.isAbsolute(sourceFilePath)
365
+ ? sourceFilePath
366
+ : path.join(getProjectRoot(), sourceFilePath)
367
+
368
+ const cached = await getCachedParsedFile(filePath)
369
+ if (!cached) return undefined
370
+
371
+ // 1. Search local variable definitions by VALUE (handles loop variables)
372
+ // Look for definitions where: the property name matches AND the value matches
373
+ for (const def of cached.variableDefinitions) {
374
+ if (def.name === propName && def.value === resolvedValue) {
375
+ return {
376
+ file: path.relative(getProjectRoot(), filePath),
377
+ line: def.line,
378
+ snippet: cached.lines[def.line - 1] || '',
379
+ type: 'variable',
380
+ variableName: buildDefinitionPath(def),
381
+ definitionLine: def.line,
382
+ }
383
+ }
384
+ }
385
+
386
+ // 2. Search by exact expression path match
387
+ const baseVar = exprPath.match(/^(\w+)/)?.[1]
388
+ if (baseVar) {
389
+ for (const def of cached.variableDefinitions) {
390
+ const defPath = buildDefinitionPath(def)
391
+ if (defPath === exprPath && def.value === resolvedValue) {
392
+ return {
393
+ file: path.relative(getProjectRoot(), filePath),
394
+ line: def.line,
395
+ snippet: cached.lines[def.line - 1] || '',
396
+ type: 'variable',
397
+ variableName: defPath,
398
+ definitionLine: def.line,
399
+ }
400
+ }
401
+ }
402
+
403
+ // 3. Check if the base variable comes from props
404
+ const actualPropName = cached.propAliases.get(baseVar)
405
+ if (actualPropName) {
406
+ const componentFileName = path.basename(filePath)
407
+ const result = await searchForExpressionPropAttributeByValue(
408
+ componentFileName,
409
+ propName,
410
+ resolvedValue,
411
+ )
412
+ if (result) return result
413
+ }
414
+
415
+ // 4. Check if the base variable comes from an import
416
+ const importInfo = cached.imports.find((imp) => imp.localName === baseVar)
417
+ if (importInfo) {
418
+ const result = await searchForImportedAttributeByValue(
419
+ filePath,
420
+ importInfo,
421
+ propName,
422
+ resolvedValue,
423
+ )
424
+ if (result) return result
425
+ }
426
+ }
427
+
428
+ // 5. Fallback: search all variable definitions by value only
429
+ for (const def of cached.variableDefinitions) {
430
+ if (def.value === resolvedValue) {
431
+ return {
432
+ file: path.relative(getProjectRoot(), filePath),
433
+ line: def.line,
434
+ snippet: cached.lines[def.line - 1] || '',
435
+ type: 'variable',
436
+ variableName: buildDefinitionPath(def),
437
+ definitionLine: def.line,
438
+ }
439
+ }
440
+ }
441
+
442
+ return undefined
443
+ }
444
+
445
+ /**
446
+ * Search for attribute value in parent components by matching the resolved value.
447
+ */
448
+ async function searchForExpressionPropAttributeByValue(
449
+ componentFileName: string,
450
+ propName: string,
451
+ resolvedValue: string,
452
+ depth: number = 0,
453
+ ): Promise<SourceLocation | undefined> {
454
+ if (depth > 5) return undefined
455
+
456
+ const srcDir = path.join(getProjectRoot(), 'src')
457
+ const searchDirs = [
458
+ path.join(srcDir, 'pages'),
459
+ path.join(srcDir, 'components'),
460
+ path.join(srcDir, 'layouts'),
461
+ ]
462
+
463
+ for (const dir of searchDirs) {
464
+ try {
465
+ const result = await searchDirForAttributeByValue(dir, propName, resolvedValue, depth)
466
+ if (result) return result
467
+ } catch {
468
+ // Directory doesn't exist
469
+ }
470
+ }
471
+
472
+ return undefined
473
+ }
474
+
475
+ async function searchDirForAttributeByValue(
476
+ dir: string,
477
+ propName: string,
478
+ resolvedValue: string,
479
+ depth: number,
480
+ ): Promise<SourceLocation | undefined> {
481
+ try {
482
+ const entries = await fs.readdir(dir, { withFileTypes: true })
483
+
484
+ for (const entry of entries) {
485
+ const fullPath = path.join(dir, entry.name)
486
+
487
+ if (entry.isDirectory()) {
488
+ const result = await searchDirForAttributeByValue(fullPath, propName, resolvedValue, depth)
489
+ if (result) return result
490
+ } else if (entry.isFile() && entry.name.endsWith('.astro')) {
491
+ const cached = await getCachedParsedFile(fullPath)
492
+ if (!cached) continue
493
+
494
+ // Search for variable definitions matching propName and value
495
+ for (const def of cached.variableDefinitions) {
496
+ if (def.name === propName && def.value === resolvedValue) {
497
+ return {
498
+ file: path.relative(getProjectRoot(), fullPath),
499
+ line: def.line,
500
+ snippet: cached.lines[def.line - 1] || '',
501
+ type: 'variable',
502
+ variableName: buildDefinitionPath(def),
503
+ definitionLine: def.line,
504
+ }
505
+ }
506
+ }
507
+ }
508
+ }
509
+ } catch {
510
+ // Error reading directory
511
+ }
512
+
513
+ return undefined
514
+ }
515
+
516
+ /**
517
+ * Search for attribute value in imported files by matching the resolved value.
518
+ */
519
+ async function searchForImportedAttributeByValue(
520
+ fromFile: string,
521
+ importInfo: ImportInfo,
522
+ propName: string,
523
+ resolvedValue: string,
524
+ ): Promise<SourceLocation | undefined> {
525
+ const importedFilePath = await resolveImportPath(importInfo.source, fromFile)
526
+ if (!importedFilePath) return undefined
527
+
528
+ const exportedDefs = await getExportedDefinitions(importedFilePath)
529
+ if (exportedDefs.length === 0) return undefined
530
+
531
+ // Search for definitions matching propName and value
532
+ for (const def of exportedDefs) {
533
+ if (def.name === propName && def.value === resolvedValue) {
534
+ const importedFileContent = await fs.readFile(importedFilePath, 'utf-8')
535
+ const importedLines = importedFileContent.split('\n')
536
+
537
+ return {
538
+ file: path.relative(getProjectRoot(), importedFilePath),
539
+ line: def.line,
540
+ snippet: importedLines[def.line - 1] || '',
541
+ type: 'variable',
542
+ variableName: buildDefinitionPath(def),
543
+ definitionLine: def.line,
544
+ }
545
+ }
546
+ }
547
+
548
+ // Also try matching by value only as fallback
549
+ for (const def of exportedDefs) {
550
+ if (def.value === resolvedValue) {
551
+ const importedFileContent = await fs.readFile(importedFilePath, 'utf-8')
552
+ const importedLines = importedFileContent.split('\n')
553
+
554
+ return {
555
+ file: path.relative(getProjectRoot(), importedFilePath),
556
+ line: def.line,
557
+ snippet: importedLines[def.line - 1] || '',
558
+ type: 'variable',
559
+ variableName: buildDefinitionPath(def),
560
+ definitionLine: def.line,
561
+ }
562
+ }
563
+ }
564
+
565
+ return undefined
566
+ }
567
+
568
+ /**
569
+ * Search for attribute value in parent components via expression props.
570
+ * @deprecated Use searchForExpressionPropAttributeByValue instead
571
+ */
572
+ async function searchForExpressionPropAttribute(
573
+ componentFileName: string,
574
+ propName: string,
575
+ expressionPath: string,
576
+ depth: number = 0,
577
+ ): Promise<SourceLocation | undefined> {
578
+ if (depth > 5) return undefined
579
+
580
+ const srcDir = path.join(getProjectRoot(), 'src')
581
+ const searchDirs = [
582
+ path.join(srcDir, 'pages'),
583
+ path.join(srcDir, 'components'),
584
+ path.join(srcDir, 'layouts'),
585
+ ]
586
+
587
+ const componentName = path.basename(componentFileName, '.astro')
588
+
589
+ for (const dir of searchDirs) {
590
+ try {
591
+ const result = await searchDirForAttributeProp(
592
+ dir,
593
+ componentName,
594
+ propName,
595
+ expressionPath,
596
+ depth,
597
+ )
598
+ if (result) return result
599
+ } catch {
600
+ // Directory doesn't exist
601
+ }
602
+ }
603
+
604
+ return undefined
605
+ }
606
+
607
+ async function searchDirForAttributeProp(
608
+ dir: string,
609
+ componentName: string,
610
+ propName: string,
611
+ expressionPath: string,
612
+ depth: number,
613
+ ): Promise<SourceLocation | undefined> {
614
+ try {
615
+ const entries = await fs.readdir(dir, { withFileTypes: true })
616
+
617
+ for (const entry of entries) {
618
+ const fullPath = path.join(dir, entry.name)
619
+
620
+ if (entry.isDirectory()) {
621
+ const result = await searchDirForAttributeProp(
622
+ fullPath,
623
+ componentName,
624
+ propName,
625
+ expressionPath,
626
+ depth,
627
+ )
628
+ if (result) return result
629
+ } else if (entry.isFile() && entry.name.endsWith('.astro')) {
630
+ const cached = await getCachedParsedFile(fullPath)
631
+ if (!cached) continue
632
+
633
+ // Find expression prop usage: <Component prop={variable} />
634
+ const exprPropMatch = findExpressionProp(cached.ast, componentName, propName)
635
+
636
+ if (exprPropMatch) {
637
+ const exprText = exprPropMatch.expressionText
638
+ // Build the path in the parent's context
639
+ const parentPath = expressionPath.replace(/^[^.[]+/, exprText)
640
+
641
+ // Check local variable definitions
642
+ for (const def of cached.variableDefinitions) {
643
+ const defPath = buildDefinitionPath(def)
644
+ if (defPath === parentPath) {
645
+ return {
646
+ file: path.relative(getProjectRoot(), fullPath),
647
+ line: def.line,
648
+ snippet: cached.lines[def.line - 1] || '',
649
+ type: 'variable',
650
+ variableName: defPath,
651
+ definitionLine: def.line,
652
+ }
653
+ }
654
+ }
655
+
656
+ // Check if exprText is from props (multi-level drilling)
657
+ const baseVar = exprText.match(/^(\w+)/)?.[1]
658
+ if (baseVar && cached.propAliases.has(baseVar)) {
659
+ const actualPropName = cached.propAliases.get(baseVar)!
660
+ const result = await searchForExpressionPropAttribute(
661
+ entry.name,
662
+ actualPropName,
663
+ parentPath,
664
+ depth + 1,
665
+ )
666
+ if (result) return result
667
+ }
668
+ }
669
+
670
+ // Try spread prop usage
671
+ const spreadMatch = findSpreadProp(cached.ast, componentName)
672
+ if (spreadMatch) {
673
+ const spreadPropPath = `${spreadMatch.spreadVarName}.${propName}`
674
+ for (const def of cached.variableDefinitions) {
675
+ const defPath = buildDefinitionPath(def)
676
+ if (defPath === spreadPropPath) {
677
+ return {
678
+ file: path.relative(getProjectRoot(), fullPath),
679
+ line: def.line,
680
+ snippet: cached.lines[def.line - 1] || '',
681
+ type: 'variable',
682
+ variableName: defPath,
683
+ definitionLine: def.line,
684
+ }
685
+ }
686
+ }
687
+ }
688
+ }
689
+ }
690
+ } catch {
691
+ // Error reading directory
692
+ }
693
+
694
+ return undefined
695
+ }
696
+
697
+ /**
698
+ * Search for attribute value in an imported file.
699
+ */
700
+ async function searchForImportedAttribute(
701
+ fromFile: string,
702
+ importInfo: ImportInfo,
703
+ expressionPath: string,
704
+ ): Promise<SourceLocation | undefined> {
705
+ const importedFilePath = await resolveImportPath(importInfo.source, fromFile)
706
+ if (!importedFilePath) return undefined
707
+
708
+ const exportedDefs = await getExportedDefinitions(importedFilePath)
709
+ if (exportedDefs.length === 0) return undefined
710
+
711
+ // Build the target path in the imported file
712
+ let targetPath: string
713
+ if (importInfo.importedName === 'default' || importInfo.importedName === importInfo.localName) {
714
+ targetPath = expressionPath
715
+ } else {
716
+ targetPath = expressionPath.replace(
717
+ new RegExp(`^${importInfo.localName}`),
718
+ importInfo.importedName,
719
+ )
720
+ }
721
+
722
+ for (const def of exportedDefs) {
723
+ const defPath = buildDefinitionPath(def)
724
+ if (defPath === targetPath) {
725
+ const importedFileContent = await fs.readFile(importedFilePath, 'utf-8')
726
+ const importedLines = importedFileContent.split('\n')
727
+
728
+ return {
729
+ file: path.relative(getProjectRoot(), importedFilePath),
730
+ line: def.line,
731
+ snippet: importedLines[def.line - 1] || '',
732
+ type: 'variable',
733
+ variableName: defPath,
734
+ definitionLine: def.line,
735
+ }
736
+ }
737
+ }
738
+
739
+ return undefined
740
+ }
@@ -16,6 +16,9 @@ export { initializeSearchIndex } from './search-index'
16
16
  // Source location finding
17
17
  export { findSourceLocation } from './source-lookup'
18
18
 
19
+ // Attribute source finding
20
+ export { findAttributeSourceLocation } from './cross-file-tracker'
21
+
19
22
  // Image finding
20
23
  export { findImageSourceLocation } from './image-finder'
21
24
 
@@ -23,4 +26,12 @@ export { findImageSourceLocation } from './image-finder'
23
26
  export { findCollectionSource, findMarkdownSourceLocation, parseMarkdownContent } from './collection-finder'
24
27
 
25
28
  // Snippet utilities (used by html-processor)
26
- export { enhanceManifestWithSourceSnippets, extractCompleteTagSnippet, extractInnerHtmlFromSnippet, extractSourceSnippet } from './snippet-utils'
29
+ export {
30
+ enhanceManifestWithSourceSnippets,
31
+ extractCompleteTagSnippet,
32
+ extractInnerHtmlFromSnippet,
33
+ extractOpeningTagWithLine,
34
+ extractSourceSnippet,
35
+ updateAttributeSources,
36
+ updateColorClassSources,
37
+ } from './snippet-utils'
@@ -193,6 +193,53 @@ function extractCompleteTagSnippet(lines: string[], startLine: number, tag: stri
193
193
  return snippetLines.join('\n')
194
194
  }
195
195
 
196
+ /**
197
+ * Extract the opening tag from source lines with its start line number.
198
+ * Local version for indexing (to avoid circular dependency)
199
+ */
200
+ function extractOpeningTagWithLine(
201
+ lines: string[],
202
+ startLine: number,
203
+ tag: string,
204
+ ): { snippet: string; startLine: number } | undefined {
205
+ const openTagPattern = new RegExp(`<${tag}(?:[\\s>]|$)`, 'gi')
206
+
207
+ let actualStartLine = startLine
208
+ const startLineContent = lines[startLine] || ''
209
+ if (!openTagPattern.test(startLineContent)) {
210
+ for (let i = startLine - 1; i >= Math.max(0, startLine - 20); i--) {
211
+ const line = lines[i]
212
+ if (!line) continue
213
+ openTagPattern.lastIndex = 0
214
+ if (openTagPattern.test(line)) {
215
+ actualStartLine = i
216
+ break
217
+ }
218
+ }
219
+ }
220
+
221
+ const snippetLines: string[] = []
222
+ for (let i = actualStartLine; i < Math.min(actualStartLine + 10, lines.length); i++) {
223
+ const line = lines[i]
224
+ if (!line) continue
225
+
226
+ snippetLines.push(line)
227
+ const combined = snippetLines.join('\n')
228
+
229
+ const openTagMatch = combined.match(new RegExp(`<${tag}[^>]*>`, 'i'))
230
+ if (openTagMatch) {
231
+ return { snippet: openTagMatch[0], startLine: actualStartLine }
232
+ }
233
+
234
+ const selfClosingMatch = combined.match(new RegExp(`<${tag}[^>]*/\\s*>`, 'i'))
235
+ if (selfClosingMatch) {
236
+ return { snippet: selfClosingMatch[0], startLine: actualStartLine }
237
+ }
238
+ }
239
+
240
+ return undefined
241
+ }
242
+
196
243
  /**
197
244
  * Index all searchable text content from a parsed file
198
245
  */
@@ -241,11 +288,13 @@ export function indexFileContent(cached: CachedParsedFile, relFile: string): voi
241
288
  // Index static text content
242
289
  const completeSnippet = extractCompleteTagSnippet(cached.lines, line - 1, tag)
243
290
  const snippet = extractInnerHtmlFromSnippet(completeSnippet, tag) ?? completeSnippet
291
+ const openingTagInfo = extractOpeningTagWithLine(cached.lines, line - 1, tag)
244
292
 
245
293
  addToTextSearchIndex({
246
294
  file: relFile,
247
295
  line,
248
296
  snippet,
297
+ openingTagSnippet: openingTagInfo?.snippet,
249
298
  type: 'static',
250
299
  normalizedText,
251
300
  tag,
@@ -358,6 +407,7 @@ export function findInTextIndex(textContent: string, tag: string): SourceLocatio
358
407
  file: entry.file,
359
408
  line: entry.line,
360
409
  snippet: entry.snippet,
410
+ openingTagSnippet: entry.openingTagSnippet,
361
411
  type: entry.type,
362
412
  variableName: entry.variableName,
363
413
  definitionLine: entry.definitionLine,
@@ -374,6 +424,7 @@ export function findInTextIndex(textContent: string, tag: string): SourceLocatio
374
424
  file: entry.file,
375
425
  line: entry.line,
376
426
  snippet: entry.snippet,
427
+ openingTagSnippet: entry.openingTagSnippet,
377
428
  type: entry.type,
378
429
  variableName: entry.variableName,
379
430
  definitionLine: entry.definitionLine,
@@ -389,6 +440,7 @@ export function findInTextIndex(textContent: string, tag: string): SourceLocatio
389
440
  file: entry.file,
390
441
  line: entry.line,
391
442
  snippet: entry.snippet,
443
+ openingTagSnippet: entry.openingTagSnippet,
392
444
  type: entry.type,
393
445
  variableName: entry.variableName,
394
446
  definitionLine: entry.definitionLine,