@nuasite/cms-marker 0.0.82 → 0.0.84
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/dist/types/build-processor.d.ts.map +1 -1
- package/dist/types/html-processor.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/source-finder/ast-extractors.d.ts.map +1 -1
- package/dist/types/source-finder/cross-file-tracker.d.ts +10 -0
- package/dist/types/source-finder/cross-file-tracker.d.ts.map +1 -1
- package/dist/types/source-finder/index.d.ts +2 -1
- package/dist/types/source-finder/index.d.ts.map +1 -1
- package/dist/types/source-finder/search-index.d.ts.map +1 -1
- package/dist/types/source-finder/snippet-utils.d.ts +43 -2
- package/dist/types/source-finder/snippet-utils.d.ts.map +1 -1
- package/dist/types/source-finder/source-lookup.d.ts.map +1 -1
- package/dist/types/source-finder/types.d.ts +4 -0
- package/dist/types/source-finder/types.d.ts.map +1 -1
- package/dist/types/tailwind-colors.d.ts +5 -3
- package/dist/types/tailwind-colors.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/dist/types/types.d.ts +16 -271
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/utils.d.ts +2 -4
- package/dist/types/utils.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/build-processor.ts +54 -5
- package/src/dev-middleware.ts +2 -2
- package/src/html-processor.ts +153 -714
- package/src/index.ts +1 -14
- package/src/source-finder/ast-extractors.ts +17 -7
- package/src/source-finder/cross-file-tracker.ts +405 -2
- package/src/source-finder/index.ts +12 -1
- package/src/source-finder/search-index.ts +52 -0
- package/src/source-finder/snippet-utils.ts +308 -16
- package/src/source-finder/source-lookup.ts +5 -1
- package/src/source-finder/types.ts +4 -0
- package/src/tailwind-colors.ts +49 -48
- package/src/types.ts +16 -284
- 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
|
-
|
|
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 ||
|
|
47
|
-
|
|
48
|
-
|
|
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 ||
|
|
118
|
-
|
|
119
|
-
|
|
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 {
|
|
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,
|