@nuasite/cms-marker 0.0.71 → 0.0.73

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 (48) hide show
  1. package/dist/types/build-processor.d.ts.map +1 -1
  2. package/dist/types/dev-middleware.d.ts.map +1 -1
  3. package/dist/types/source-finder/ast-extractors.d.ts +35 -0
  4. package/dist/types/source-finder/ast-extractors.d.ts.map +1 -0
  5. package/dist/types/source-finder/ast-parser.d.ts +16 -0
  6. package/dist/types/source-finder/ast-parser.d.ts.map +1 -0
  7. package/dist/types/source-finder/cache.d.ts +18 -0
  8. package/dist/types/source-finder/cache.d.ts.map +1 -0
  9. package/dist/types/source-finder/collection-finder.d.ts +24 -0
  10. package/dist/types/source-finder/collection-finder.d.ts.map +1 -0
  11. package/dist/types/source-finder/cross-file-tracker.d.ts +29 -0
  12. package/dist/types/source-finder/cross-file-tracker.d.ts.map +1 -0
  13. package/dist/types/source-finder/element-finder.d.ts +42 -0
  14. package/dist/types/source-finder/element-finder.d.ts.map +1 -0
  15. package/dist/types/source-finder/image-finder.d.ts +16 -0
  16. package/dist/types/source-finder/image-finder.d.ts.map +1 -0
  17. package/dist/types/source-finder/index.d.ts +8 -0
  18. package/dist/types/source-finder/index.d.ts.map +1 -0
  19. package/dist/types/source-finder/search-index.d.ts +27 -0
  20. package/dist/types/source-finder/search-index.d.ts.map +1 -0
  21. package/dist/types/source-finder/snippet-utils.d.ts +49 -0
  22. package/dist/types/source-finder/snippet-utils.d.ts.map +1 -0
  23. package/dist/types/source-finder/source-lookup.d.ts +16 -0
  24. package/dist/types/source-finder/source-lookup.d.ts.map +1 -0
  25. package/dist/types/source-finder/types.d.ts +163 -0
  26. package/dist/types/source-finder/types.d.ts.map +1 -0
  27. package/dist/types/source-finder/variable-extraction.d.ts +37 -0
  28. package/dist/types/source-finder/variable-extraction.d.ts.map +1 -0
  29. package/dist/types/tsconfig.tsbuildinfo +1 -1
  30. package/package.json +1 -1
  31. package/src/build-processor.ts +33 -1
  32. package/src/dev-middleware.ts +33 -1
  33. package/src/source-finder/ast-extractors.ts +175 -0
  34. package/src/source-finder/ast-parser.ts +127 -0
  35. package/src/source-finder/cache.ts +75 -0
  36. package/src/source-finder/collection-finder.ts +321 -0
  37. package/src/source-finder/cross-file-tracker.ts +337 -0
  38. package/src/source-finder/element-finder.ts +383 -0
  39. package/src/source-finder/image-finder.ts +189 -0
  40. package/src/source-finder/index.ts +26 -0
  41. package/src/source-finder/search-index.ts +418 -0
  42. package/src/source-finder/snippet-utils.ts +268 -0
  43. package/src/source-finder/source-lookup.ts +197 -0
  44. package/src/source-finder/types.ts +206 -0
  45. package/src/source-finder/variable-extraction.ts +355 -0
  46. package/dist/types/source-finder.d.ts +0 -117
  47. package/dist/types/source-finder.d.ts.map +0 -1
  48. package/src/source-finder.ts +0 -1784
@@ -0,0 +1,197 @@
1
+ import fs from 'node:fs/promises'
2
+ import path from 'node:path'
3
+
4
+ import { getProjectRoot } from '../config'
5
+ import { getCachedParsedFile } from './ast-parser'
6
+ import { isSearchIndexInitialized } from './cache'
7
+ import { searchForExpressionProp, searchForImportedValue, searchForPropInParents } from './cross-file-tracker'
8
+ import { findElementWithText } from './element-finder'
9
+ import { findInTextIndex } from './search-index'
10
+ import { extractCompleteTagSnippet, extractInnerHtmlFromSnippet } from './snippet-utils'
11
+ import type { SourceLocation } from './types'
12
+
13
+ // ============================================================================
14
+ // Main Source Location Finding
15
+ // ============================================================================
16
+
17
+ /**
18
+ * Find source file and line number for text content.
19
+ * Uses pre-built search index for fast lookups.
20
+ */
21
+ export async function findSourceLocation(
22
+ textContent: string,
23
+ tag: string,
24
+ ): Promise<SourceLocation | undefined> {
25
+ // Use index if available (much faster)
26
+ if (isSearchIndexInitialized()) {
27
+ return findInTextIndex(textContent, tag)
28
+ }
29
+
30
+ // Fallback to slow search if index not initialized
31
+ const srcDir = path.join(getProjectRoot(), 'src')
32
+
33
+ try {
34
+ const searchDirs = [
35
+ path.join(srcDir, 'components'),
36
+ path.join(srcDir, 'pages'),
37
+ path.join(srcDir, 'layouts'),
38
+ ]
39
+
40
+ for (const dir of searchDirs) {
41
+ try {
42
+ const result = await searchDirectory(dir, textContent, tag)
43
+ if (result) {
44
+ return result
45
+ }
46
+ } catch {
47
+ // Directory doesn't exist, continue
48
+ }
49
+ }
50
+
51
+ // If not found directly, try searching for prop values in parent components
52
+ for (const dir of searchDirs) {
53
+ try {
54
+ const result = await searchForPropInParents(dir, textContent)
55
+ if (result) {
56
+ return result
57
+ }
58
+ } catch {
59
+ // Directory doesn't exist, continue
60
+ }
61
+ }
62
+ } catch {
63
+ // Search failed
64
+ }
65
+
66
+ return undefined
67
+ }
68
+
69
+ // ============================================================================
70
+ // Directory Search
71
+ // ============================================================================
72
+
73
+ /**
74
+ * Recursively search directory for matching content
75
+ */
76
+ export async function searchDirectory(
77
+ dir: string,
78
+ textContent: string,
79
+ tag: string,
80
+ ): Promise<SourceLocation | undefined> {
81
+ try {
82
+ const entries = await fs.readdir(dir, { withFileTypes: true })
83
+
84
+ for (const entry of entries) {
85
+ const fullPath = path.join(dir, entry.name)
86
+
87
+ if (entry.isDirectory()) {
88
+ const result = await searchDirectory(fullPath, textContent, tag)
89
+ if (result) return result
90
+ } else if (entry.isFile() && entry.name.endsWith('.astro')) {
91
+ const result = await searchAstroFile(fullPath, textContent, tag)
92
+ if (result) return result
93
+ }
94
+ }
95
+ } catch {
96
+ // Error reading directory
97
+ }
98
+
99
+ return undefined
100
+ }
101
+
102
+ // ============================================================================
103
+ // Astro File Search
104
+ // ============================================================================
105
+
106
+ /**
107
+ * Search a single Astro file for matching content using AST parsing.
108
+ * Uses caching for better performance.
109
+ */
110
+ export async function searchAstroFile(
111
+ filePath: string,
112
+ textContent: string,
113
+ tag: string,
114
+ ): Promise<SourceLocation | undefined> {
115
+ try {
116
+ // Use cached parsed file
117
+ const cached = await getCachedParsedFile(filePath)
118
+ if (!cached) return undefined
119
+
120
+ const { lines, ast, variableDefinitions, propAliases, imports } = cached
121
+
122
+ // Find matching element in template AST
123
+ const { bestMatch, propCandidates, importCandidates } = findElementWithText(
124
+ ast,
125
+ tag,
126
+ textContent,
127
+ variableDefinitions,
128
+ propAliases,
129
+ imports,
130
+ )
131
+
132
+ // First, check if we have a direct match (local variable or static content)
133
+ if (bestMatch && !bestMatch.usesProp && !bestMatch.usesImport) {
134
+ // Determine the editable line (definition for variables, usage for static)
135
+ const editableLine = bestMatch.type === 'variable' && bestMatch.definitionLine
136
+ ? bestMatch.definitionLine
137
+ : bestMatch.line
138
+
139
+ // Get the source snippet - innerHTML for static content, definition line for variables
140
+ let snippet: string
141
+ if (bestMatch.type === 'static') {
142
+ // For static content, extract only the innerHTML (not the wrapper element)
143
+ const completeSnippet = extractCompleteTagSnippet(lines, editableLine - 1, tag)
144
+ snippet = extractInnerHtmlFromSnippet(completeSnippet, tag) ?? completeSnippet
145
+ } else {
146
+ // For variables/props, just the definition line with indentation
147
+ snippet = lines[editableLine - 1] || ''
148
+ }
149
+
150
+ return {
151
+ file: path.relative(getProjectRoot(), filePath),
152
+ line: editableLine,
153
+ snippet,
154
+ type: bestMatch.type,
155
+ variableName: bestMatch.variableName,
156
+ definitionLine: bestMatch.type === 'variable' ? bestMatch.definitionLine : undefined,
157
+ }
158
+ }
159
+
160
+ // Try all prop candidates - verify each one to find the correct match
161
+ // (handles multiple same-tag elements with different prop values)
162
+ for (const propCandidate of propCandidates) {
163
+ if (propCandidate.propName && propCandidate.expressionPath) {
164
+ const componentFileName = path.basename(filePath)
165
+ const exprPropResult = await searchForExpressionProp(
166
+ componentFileName,
167
+ propCandidate.propName,
168
+ propCandidate.expressionPath,
169
+ textContent,
170
+ )
171
+ if (exprPropResult) {
172
+ return exprPropResult
173
+ }
174
+ }
175
+ }
176
+
177
+ // Try all import candidates - verify each one to find the correct match
178
+ // (handles multiple same-tag elements with different imported values)
179
+ for (const importCandidate of importCandidates) {
180
+ if (importCandidate.importInfo && importCandidate.expressionPath) {
181
+ const importResult = await searchForImportedValue(
182
+ filePath,
183
+ importCandidate.importInfo,
184
+ importCandidate.expressionPath,
185
+ textContent,
186
+ )
187
+ if (importResult) {
188
+ return importResult
189
+ }
190
+ }
191
+ }
192
+ } catch {
193
+ // Error reading/parsing file
194
+ }
195
+
196
+ return undefined
197
+ }
@@ -0,0 +1,206 @@
1
+ import type { Node as AstroNode } from '@astrojs/compiler/types'
2
+
3
+ // ============================================================================
4
+ // Import and Variable Information
5
+ // ============================================================================
6
+
7
+ /** Import information from frontmatter */
8
+ export interface ImportInfo {
9
+ /** Local name of the imported binding */
10
+ localName: string
11
+ /** Original exported name (or 'default' for default imports) */
12
+ importedName: string
13
+ /** The import source path (e.g., './config', '../data/nav') */
14
+ source: string
15
+ }
16
+
17
+ export interface VariableDefinition {
18
+ name: string
19
+ value: string
20
+ line: number
21
+ /** For object properties, the parent variable name */
22
+ parentName?: string
23
+ }
24
+
25
+ // ============================================================================
26
+ // Cached File Types
27
+ // ============================================================================
28
+
29
+ export interface CachedParsedFile {
30
+ content: string
31
+ lines: string[]
32
+ ast: AstroNode
33
+ frontmatterContent: string | null
34
+ frontmatterStartLine: number
35
+ variableDefinitions: VariableDefinition[]
36
+ /** Mapping of local variable names to prop names from Astro.props destructuring
37
+ * e.g., { navItems: 'items' } for `const { items: navItems } = Astro.props` */
38
+ propAliases: Map<string, string>
39
+ /** Import information from frontmatter */
40
+ imports: ImportInfo[]
41
+ }
42
+
43
+ // ============================================================================
44
+ // Search Index Types
45
+ // ============================================================================
46
+
47
+ /** Pre-built search index for fast lookups */
48
+ export interface SearchIndexEntry {
49
+ file: string
50
+ line: number
51
+ snippet: string
52
+ type: 'static' | 'variable' | 'prop' | 'computed'
53
+ variableName?: string
54
+ definitionLine?: number
55
+ normalizedText: string
56
+ tag: string
57
+ }
58
+
59
+ export interface ImageIndexEntry {
60
+ file: string
61
+ line: number
62
+ snippet: string
63
+ src: string
64
+ }
65
+
66
+ // ============================================================================
67
+ // Source Location Types (Public API)
68
+ // ============================================================================
69
+
70
+ export interface SourceLocation {
71
+ file: string
72
+ line: number
73
+ snippet?: string
74
+ type?: 'static' | 'variable' | 'prop' | 'computed' | 'collection'
75
+ variableName?: string
76
+ definitionLine?: number
77
+ /** Collection name for collection entries */
78
+ collectionName?: string
79
+ /** Entry slug for collection entries */
80
+ collectionSlug?: string
81
+ }
82
+
83
+ export interface VariableReference {
84
+ name: string
85
+ pattern: string
86
+ definitionLine: number
87
+ }
88
+
89
+ export interface CollectionInfo {
90
+ name: string
91
+ slug: string
92
+ file: string
93
+ }
94
+
95
+ export interface MarkdownContent {
96
+ /** Frontmatter fields as key-value pairs with line numbers */
97
+ frontmatter: Record<string, { value: string; line: number }>
98
+ /** The full markdown body content */
99
+ body: string
100
+ /** Line number where body starts */
101
+ bodyStartLine: number
102
+ /** File path relative to cwd */
103
+ file: string
104
+ /** Collection name */
105
+ collectionName: string
106
+ /** Collection slug */
107
+ collectionSlug: string
108
+ }
109
+
110
+ // ============================================================================
111
+ // AST Parsing Types
112
+ // ============================================================================
113
+
114
+ export interface ParsedAstroFile {
115
+ ast: AstroNode
116
+ frontmatterContent: string | null
117
+ frontmatterStartLine: number
118
+ }
119
+
120
+ /** Minimal Babel AST node type for our usage */
121
+ export interface BabelNode {
122
+ type: string
123
+ [key: string]: unknown
124
+ }
125
+
126
+ /** Minimal Babel File type */
127
+ export interface BabelFile {
128
+ type: 'File'
129
+ program: BabelNode & { body: BabelNode[] }
130
+ }
131
+
132
+ // ============================================================================
133
+ // Match Result Types
134
+ // ============================================================================
135
+
136
+ export interface TemplateMatch {
137
+ line: number
138
+ type: 'static' | 'variable' | 'computed'
139
+ variableName?: string
140
+ /** For variables, the definition line in frontmatter */
141
+ definitionLine?: number
142
+ /** If true, the expression uses a variable from props that needs cross-file tracking */
143
+ usesProp?: boolean
144
+ /** The prop name if usesProp is true */
145
+ propName?: string
146
+ /** The full expression path if usesProp is true (e.g., 'items[0]') */
147
+ expressionPath?: string
148
+ /** If true, the expression uses a variable from an import */
149
+ usesImport?: boolean
150
+ /** The import info if usesImport is true */
151
+ importInfo?: ImportInfo
152
+ }
153
+
154
+ /** Result type for findElementWithText - returns best match and all prop/import candidates */
155
+ export interface FindElementResult {
156
+ /** The best match found (local variables or static content) */
157
+ bestMatch: TemplateMatch | null
158
+ /** All prop-based matches for the tag (need cross-file verification) */
159
+ propCandidates: TemplateMatch[]
160
+ /** All import-based matches for the tag (need cross-file verification) */
161
+ importCandidates: TemplateMatch[]
162
+ }
163
+
164
+ export interface ComponentPropMatch {
165
+ line: number
166
+ propName: string
167
+ propValue: string
168
+ }
169
+
170
+ export interface ExpressionPropMatch {
171
+ componentName: string
172
+ propName: string
173
+ /** The expression text (e.g., 'navItems' from items={navItems}) */
174
+ expressionText: string
175
+ line: number
176
+ }
177
+
178
+ export interface SpreadPropMatch {
179
+ componentName: string
180
+ /** The variable name being spread (e.g., 'cardProps' from {...cardProps}) */
181
+ spreadVarName: string
182
+ line: number
183
+ }
184
+
185
+ export interface ImageMatch {
186
+ line: number
187
+ src: string
188
+ snippet: string
189
+ }
190
+
191
+ // ============================================================================
192
+ // Line Transformer for AST Extraction
193
+ // ============================================================================
194
+
195
+ /**
196
+ * Transforms Babel line numbers to actual file line numbers.
197
+ * Babel parses content starting at line 1, but frontmatter content
198
+ * may start at a different line in the actual file.
199
+ */
200
+ export type LineTransformer = (babelLine: number) => number
201
+
202
+ /** Identity transformer - use for standalone files where Babel line = file line */
203
+ export const identityLine: LineTransformer = (line) => line
204
+
205
+ /** Create a transformer for frontmatter content that starts at a specific line */
206
+ export const createFrontmatterLineTransformer = (startLine: number): LineTransformer => (babelLine) => (babelLine - 1) + startLine