@nuasite/cms-marker 0.0.72 → 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 -2765
@@ -0,0 +1,383 @@
1
+ import type { ComponentNode, ElementNode, Node as AstroNode, TextNode } from '@astrojs/compiler/types'
2
+
3
+ import { buildDefinitionPath, parseExpressionPath } from './ast-extractors'
4
+ import { normalizeText } from './snippet-utils'
5
+ import type {
6
+ ComponentPropMatch,
7
+ ExpressionPropMatch,
8
+ FindElementResult,
9
+ ImportInfo,
10
+ SpreadPropMatch,
11
+ TemplateMatch,
12
+ VariableDefinition,
13
+ } from './types'
14
+
15
+ // ============================================================================
16
+ // Text Content Extraction
17
+ // ============================================================================
18
+
19
+ /**
20
+ * Get text content from an AST node recursively.
21
+ * Treats <br> elements as whitespace to match rendered HTML behavior.
22
+ */
23
+ export function getTextContent(node: AstroNode): string {
24
+ if (node.type === 'text') {
25
+ return (node as TextNode).value
26
+ }
27
+ // Treat <br> elements as whitespace (they create line breaks in rendered HTML)
28
+ if (node.type === 'element' && (node as ElementNode).name.toLowerCase() === 'br') {
29
+ return ' '
30
+ }
31
+ if ('children' in node && Array.isArray(node.children)) {
32
+ return node.children.map(getTextContent).join('')
33
+ }
34
+ return ''
35
+ }
36
+
37
+ /**
38
+ * Check for expression children and extract variable names
39
+ */
40
+ export function hasExpressionChild(node: AstroNode): { found: boolean; varNames: string[] } {
41
+ const varNames: string[] = []
42
+ if (node.type === 'expression') {
43
+ // Try to extract variable name from expression
44
+ // The expression node children contain the text representation
45
+ const exprText = getTextContent(node)
46
+ // Extract variable paths like {foo}, {foo.bar}, {items[0]}, {config.nav.title}, {links[0].text}
47
+ const fullPath = parseExpressionPath(exprText)
48
+ if (fullPath) {
49
+ varNames.push(fullPath)
50
+ }
51
+ return { found: true, varNames }
52
+ }
53
+ if ('children' in node && Array.isArray(node.children)) {
54
+ for (const child of node.children) {
55
+ const result = hasExpressionChild(child)
56
+ if (result.found) {
57
+ varNames.push(...result.varNames)
58
+ }
59
+ }
60
+ }
61
+ return { found: varNames.length > 0, varNames }
62
+ }
63
+
64
+ // ============================================================================
65
+ // Element Finding
66
+ // ============================================================================
67
+
68
+ /**
69
+ * Walk the Astro AST to find elements matching a tag with specific text content.
70
+ * Returns the best match (local variables or static content) AND all prop/import candidates
71
+ * that need cross-file verification for multiple same-tag elements.
72
+ * @param propAliases - Map of local variable names to prop names from Astro.props (for cross-file tracking)
73
+ * @param imports - Import information from frontmatter (for cross-file tracking)
74
+ */
75
+ export function findElementWithText(
76
+ ast: AstroNode,
77
+ tag: string,
78
+ searchText: string,
79
+ variableDefinitions: VariableDefinition[],
80
+ propAliases: Map<string, string> = new Map(),
81
+ imports: ImportInfo[] = [],
82
+ ): FindElementResult {
83
+ const normalizedSearch = normalizeText(searchText)
84
+ const tagLower = tag.toLowerCase()
85
+ let bestMatch: TemplateMatch | null = null
86
+ let bestScore = 0
87
+ const propCandidates: TemplateMatch[] = []
88
+ const importCandidates: TemplateMatch[] = []
89
+
90
+ /**
91
+ * Extract the base variable name from an expression path.
92
+ * e.g., 'items[0]' -> 'items', 'config.nav.title' -> 'config'
93
+ */
94
+ function getBaseVarName(exprPath: string): string {
95
+ const match = exprPath.match(/^(\w+)/)
96
+ return match?.[1] ?? exprPath
97
+ }
98
+
99
+ function visit(node: AstroNode) {
100
+ // Check if this is an element or component matching our tag
101
+ if ((node.type === 'element' || node.type === 'component') && node.name.toLowerCase() === tagLower) {
102
+ const elemNode = node as ElementNode | ComponentNode
103
+ const textContent = getTextContent(elemNode)
104
+ const normalizedContent = normalizeText(textContent)
105
+ const line = elemNode.position?.start.line ?? 0
106
+
107
+ // Check for expression (variable reference)
108
+ const exprInfo = hasExpressionChild(elemNode)
109
+ if (exprInfo.found && exprInfo.varNames.length > 0) {
110
+ // Look for matching variable definition
111
+ for (const exprPath of exprInfo.varNames) {
112
+ let foundInLocal = false
113
+
114
+ for (const def of variableDefinitions) {
115
+ // Build the full definition path for comparison
116
+ const defPath = buildDefinitionPath(def)
117
+ // Check if the expression path matches the definition path
118
+ if (defPath === exprPath) {
119
+ foundInLocal = true
120
+ const normalizedDef = normalizeText(def.value)
121
+ if (normalizedDef === normalizedSearch) {
122
+ // Found a variable match - this is highest priority
123
+ if (bestScore < 100) {
124
+ bestScore = 100
125
+ bestMatch = {
126
+ line,
127
+ type: 'variable',
128
+ variableName: defPath,
129
+ definitionLine: def.line,
130
+ }
131
+ }
132
+ return
133
+ }
134
+ }
135
+ }
136
+
137
+ // If not found in local definitions, check if it's from props or imports
138
+ if (!foundInLocal) {
139
+ const baseVar = getBaseVarName(exprPath)
140
+
141
+ // Check props first
142
+ const actualPropName = propAliases.get(baseVar)
143
+ if (actualPropName) {
144
+ // This expression uses a prop - collect as candidate for cross-file verification
145
+ // (don't set bestMatch yet - we need to verify each candidate)
146
+ propCandidates.push({
147
+ line,
148
+ type: 'variable',
149
+ usesProp: true,
150
+ propName: actualPropName, // Use the actual prop name, not the local alias
151
+ expressionPath: exprPath,
152
+ })
153
+ } else {
154
+ // Check if it's from an import
155
+ const importInfo = imports.find((imp) => imp.localName === baseVar)
156
+ if (importInfo) {
157
+ // This expression uses an import - collect as candidate for cross-file verification
158
+ importCandidates.push({
159
+ line,
160
+ type: 'variable',
161
+ usesImport: true,
162
+ importInfo,
163
+ expressionPath: exprPath,
164
+ })
165
+ }
166
+ }
167
+ }
168
+ }
169
+ }
170
+
171
+ // Check for direct text match (static content)
172
+ // Only match if there's meaningful text content (not just variable names/expressions)
173
+ if (normalizedContent && normalizedContent.length >= 2 && normalizedSearch.length > 0) {
174
+ // For short search text (<= 10 chars), require exact match
175
+ if (normalizedSearch.length <= 10) {
176
+ if (normalizedContent.includes(normalizedSearch)) {
177
+ const score = 80
178
+ if (score > bestScore) {
179
+ bestScore = score
180
+ const actualLine = findTextLine(elemNode, normalizedSearch)
181
+ bestMatch = {
182
+ line: actualLine ?? line,
183
+ type: 'static',
184
+ }
185
+ }
186
+ }
187
+ } // For longer search text, check if content contains a significant portion
188
+ else if (normalizedSearch.length > 10) {
189
+ const textPreview = normalizedSearch.slice(0, Math.min(30, normalizedSearch.length))
190
+ if (normalizedContent.includes(textPreview)) {
191
+ const matchLength = Math.min(normalizedSearch.length, normalizedContent.length)
192
+ const score = 50 + (matchLength / normalizedSearch.length) * 40
193
+ if (score > bestScore) {
194
+ bestScore = score
195
+ const actualLine = findTextLine(elemNode, textPreview)
196
+ bestMatch = {
197
+ line: actualLine ?? line,
198
+ type: 'static',
199
+ }
200
+ }
201
+ } // Try matching first few words for very long text
202
+ else if (normalizedSearch.length > 20) {
203
+ const firstWords = normalizedSearch.split(' ').slice(0, 3).join(' ')
204
+ if (firstWords && normalizedContent.includes(firstWords)) {
205
+ const score = 40
206
+ if (score > bestScore) {
207
+ bestScore = score
208
+ const actualLine = findTextLine(elemNode, firstWords)
209
+ bestMatch = {
210
+ line: actualLine ?? line,
211
+ type: 'static',
212
+ }
213
+ }
214
+ }
215
+ }
216
+ }
217
+ }
218
+ }
219
+
220
+ // Recursively visit children
221
+ if ('children' in node && Array.isArray(node.children)) {
222
+ for (const child of node.children) {
223
+ visit(child)
224
+ }
225
+ }
226
+ }
227
+
228
+ function findTextLine(node: AstroNode, searchText: string): number | null {
229
+ if (node.type === 'text') {
230
+ const textNode = node as TextNode
231
+ if (normalizeText(textNode.value).includes(searchText)) {
232
+ return textNode.position?.start.line ?? null
233
+ }
234
+ }
235
+ if ('children' in node && Array.isArray(node.children)) {
236
+ for (const child of node.children) {
237
+ const line = findTextLine(child, searchText)
238
+ if (line !== null) return line
239
+ }
240
+ }
241
+ return null
242
+ }
243
+
244
+ visit(ast)
245
+ return { bestMatch, propCandidates, importCandidates }
246
+ }
247
+
248
+ // ============================================================================
249
+ // Component Prop Finding
250
+ // ============================================================================
251
+
252
+ /**
253
+ * Walk the Astro AST to find component props with specific text value
254
+ */
255
+ export function findComponentProp(
256
+ ast: AstroNode,
257
+ searchText: string,
258
+ ): ComponentPropMatch | null {
259
+ const normalizedSearch = normalizeText(searchText)
260
+
261
+ function visit(node: AstroNode): ComponentPropMatch | null {
262
+ // Check component nodes (PascalCase names)
263
+ if (node.type === 'component') {
264
+ const compNode = node as ComponentNode
265
+ for (const attr of compNode.attributes) {
266
+ if (attr.type === 'attribute' && attr.kind === 'quoted') {
267
+ const normalizedValue = normalizeText(attr.value)
268
+ if (normalizedValue === normalizedSearch) {
269
+ return {
270
+ line: attr.position?.start.line ?? compNode.position?.start.line ?? 0,
271
+ propName: attr.name,
272
+ propValue: attr.value,
273
+ }
274
+ }
275
+ }
276
+ }
277
+ }
278
+
279
+ // Recursively visit children
280
+ if ('children' in node && Array.isArray(node.children)) {
281
+ for (const child of node.children) {
282
+ const result = visit(child)
283
+ if (result) return result
284
+ }
285
+ }
286
+
287
+ return null
288
+ }
289
+
290
+ return visit(ast)
291
+ }
292
+
293
+ /**
294
+ * Walk the Astro AST to find component usages with expression props.
295
+ * Looks for patterns like: <Nav items={navItems} />
296
+ * @param ast - The Astro AST
297
+ * @param componentName - The component name to search for (e.g., 'Nav')
298
+ * @param propName - The prop name to find (e.g., 'items')
299
+ */
300
+ export function findExpressionProp(
301
+ ast: AstroNode,
302
+ componentName: string,
303
+ propName: string,
304
+ ): ExpressionPropMatch | null {
305
+ function visit(node: AstroNode): ExpressionPropMatch | null {
306
+ // Check component nodes matching the name
307
+ if (node.type === 'component') {
308
+ const compNode = node as ComponentNode
309
+ if (compNode.name === componentName) {
310
+ for (const attr of compNode.attributes) {
311
+ // Check for expression attributes: items={navItems}
312
+ if (attr.type === 'attribute' && attr.name === propName && attr.kind === 'expression') {
313
+ // The value contains the expression text
314
+ const exprText = attr.value?.trim() || ''
315
+ if (exprText) {
316
+ return {
317
+ componentName,
318
+ propName,
319
+ expressionText: exprText,
320
+ line: attr.position?.start.line ?? compNode.position?.start.line ?? 0,
321
+ }
322
+ }
323
+ }
324
+ }
325
+ }
326
+ }
327
+
328
+ // Recursively visit children
329
+ if ('children' in node && Array.isArray(node.children)) {
330
+ for (const child of node.children) {
331
+ const result = visit(child)
332
+ if (result) return result
333
+ }
334
+ }
335
+
336
+ return null
337
+ }
338
+
339
+ return visit(ast)
340
+ }
341
+
342
+ /**
343
+ * Walk the Astro AST to find component usages with spread props.
344
+ * Looks for patterns like: <Card {...cardProps} />
345
+ * @param ast - The Astro AST
346
+ * @param componentName - The component name to search for (e.g., 'Card')
347
+ */
348
+ export function findSpreadProp(
349
+ ast: AstroNode,
350
+ componentName: string,
351
+ ): SpreadPropMatch | null {
352
+ function visit(node: AstroNode): SpreadPropMatch | null {
353
+ // Check component nodes matching the name
354
+ if (node.type === 'component') {
355
+ const compNode = node as ComponentNode
356
+ if (compNode.name === componentName) {
357
+ for (const attr of compNode.attributes) {
358
+ // Check for spread attributes: {...cardProps}
359
+ // In Astro AST: type='attribute', kind='spread', name=variable name
360
+ if (attr.type === 'attribute' && attr.kind === 'spread' && attr.name) {
361
+ return {
362
+ componentName,
363
+ spreadVarName: attr.name,
364
+ line: attr.position?.start.line ?? compNode.position?.start.line ?? 0,
365
+ }
366
+ }
367
+ }
368
+ }
369
+ }
370
+
371
+ // Recursively visit children
372
+ if ('children' in node && Array.isArray(node.children)) {
373
+ for (const child of node.children) {
374
+ const result = visit(child)
375
+ if (result) return result
376
+ }
377
+ }
378
+
379
+ return null
380
+ }
381
+
382
+ return visit(ast)
383
+ }
@@ -0,0 +1,189 @@
1
+ import type { ElementNode, Node as AstroNode } from '@astrojs/compiler/types'
2
+ import fs from 'node:fs/promises'
3
+ import path from 'node:path'
4
+
5
+ import { getProjectRoot } from '../config'
6
+ import { getCachedParsedFile } from './ast-parser'
7
+ import { isSearchIndexInitialized } from './cache'
8
+ import { findInImageIndex } from './search-index'
9
+ import { extractImageSnippet } from './snippet-utils'
10
+ import type { ImageMatch, SourceLocation } from './types'
11
+
12
+ // ============================================================================
13
+ // Image Element Finding
14
+ // ============================================================================
15
+
16
+ /**
17
+ * Walk the Astro AST to find img elements with specific src
18
+ */
19
+ export function findImageElement(
20
+ ast: AstroNode,
21
+ imageSrc: string,
22
+ lines: string[],
23
+ ): ImageMatch | null {
24
+ function visit(node: AstroNode): ImageMatch | null {
25
+ if (node.type === 'element') {
26
+ const elemNode = node as ElementNode
27
+ if (elemNode.name.toLowerCase() === 'img') {
28
+ for (const attr of elemNode.attributes) {
29
+ if (attr.type === 'attribute' && attr.name === 'src' && attr.value === imageSrc) {
30
+ const srcLine = attr.position?.start.line ?? elemNode.position?.start.line ?? 0
31
+ const snippet = extractImageSnippet(lines, srcLine - 1)
32
+ return {
33
+ line: srcLine,
34
+ src: imageSrc,
35
+ snippet,
36
+ }
37
+ }
38
+ }
39
+ }
40
+ }
41
+
42
+ // Recursively visit children
43
+ if ('children' in node && Array.isArray(node.children)) {
44
+ for (const child of node.children) {
45
+ const result = visit(child)
46
+ if (result) return result
47
+ }
48
+ }
49
+
50
+ return null
51
+ }
52
+
53
+ return visit(ast)
54
+ }
55
+
56
+ // ============================================================================
57
+ // Image Source Location Finding
58
+ // ============================================================================
59
+
60
+ /**
61
+ * Find source file and line number for an image by its src attribute.
62
+ * Uses pre-built search index for fast lookups.
63
+ */
64
+ export async function findImageSourceLocation(
65
+ imageSrc: string,
66
+ ): Promise<SourceLocation | undefined> {
67
+ // Use index if available (much faster)
68
+ if (isSearchIndexInitialized()) {
69
+ return findInImageIndex(imageSrc)
70
+ }
71
+
72
+ // Fallback to slow search if index not initialized
73
+ const srcDir = path.join(getProjectRoot(), 'src')
74
+
75
+ try {
76
+ const searchDirs = [
77
+ path.join(srcDir, 'pages'),
78
+ path.join(srcDir, 'components'),
79
+ path.join(srcDir, 'layouts'),
80
+ ]
81
+
82
+ for (const dir of searchDirs) {
83
+ try {
84
+ const result = await searchDirectoryForImage(dir, imageSrc)
85
+ if (result) {
86
+ return result
87
+ }
88
+ } catch {
89
+ // Directory doesn't exist, continue
90
+ }
91
+ }
92
+ } catch {
93
+ // Search failed
94
+ }
95
+
96
+ return undefined
97
+ }
98
+
99
+ // ============================================================================
100
+ // Directory Search for Images
101
+ // ============================================================================
102
+
103
+ /**
104
+ * Recursively search directory for image with matching src
105
+ */
106
+ export async function searchDirectoryForImage(
107
+ dir: string,
108
+ imageSrc: string,
109
+ ): Promise<SourceLocation | undefined> {
110
+ try {
111
+ const entries = await fs.readdir(dir, { withFileTypes: true })
112
+
113
+ for (const entry of entries) {
114
+ const fullPath = path.join(dir, entry.name)
115
+
116
+ if (entry.isDirectory()) {
117
+ const result = await searchDirectoryForImage(fullPath, imageSrc)
118
+ if (result) return result
119
+ } else if (entry.isFile() && (entry.name.endsWith('.astro') || entry.name.endsWith('.tsx') || entry.name.endsWith('.jsx'))) {
120
+ const result = await searchFileForImage(fullPath, imageSrc)
121
+ if (result) return result
122
+ }
123
+ }
124
+ } catch {
125
+ // Error reading directory
126
+ }
127
+
128
+ return undefined
129
+ }
130
+
131
+ /**
132
+ * Search a single file for an image with matching src.
133
+ * Uses caching for better performance.
134
+ */
135
+ async function searchFileForImage(
136
+ filePath: string,
137
+ imageSrc: string,
138
+ ): Promise<SourceLocation | undefined> {
139
+ try {
140
+ // Use cached parsed file
141
+ const cached = await getCachedParsedFile(filePath)
142
+ if (!cached) return undefined
143
+
144
+ const { lines, ast } = cached
145
+
146
+ // Use AST parsing for Astro files
147
+ if (filePath.endsWith('.astro')) {
148
+ const imageMatch = findImageElement(ast, imageSrc, lines)
149
+
150
+ if (imageMatch) {
151
+ return {
152
+ file: path.relative(getProjectRoot(), filePath),
153
+ line: imageMatch.line,
154
+ snippet: imageMatch.snippet,
155
+ type: 'static',
156
+ }
157
+ }
158
+ }
159
+
160
+ // Regex fallback for TSX/JSX files or if AST parsing failed
161
+ const srcPatterns = [
162
+ `src="${imageSrc}"`,
163
+ `src='${imageSrc}'`,
164
+ ]
165
+
166
+ for (let i = 0; i < lines.length; i++) {
167
+ const line = lines[i]
168
+ if (!line) continue
169
+
170
+ for (const pattern of srcPatterns) {
171
+ if (line.includes(pattern)) {
172
+ // Found the image, extract the full <img> tag as snippet
173
+ const snippet = extractImageSnippet(lines, i)
174
+
175
+ return {
176
+ file: path.relative(getProjectRoot(), filePath),
177
+ line: i + 1,
178
+ snippet,
179
+ type: 'static',
180
+ }
181
+ }
182
+ }
183
+ }
184
+ } catch {
185
+ // Error reading file
186
+ }
187
+
188
+ return undefined
189
+ }
@@ -0,0 +1,26 @@
1
+ // ============================================================================
2
+ // Public API - Barrel File
3
+ // ============================================================================
4
+ // This file re-exports the public API for backward compatibility.
5
+ // All imports from './source-finder' will continue to work unchanged.
6
+
7
+ // Types (public)
8
+ export type { CollectionInfo, MarkdownContent, SourceLocation, VariableReference } from './types'
9
+
10
+ // Cache management
11
+ export { clearSourceFinderCache } from './cache'
12
+
13
+ // Search index
14
+ export { initializeSearchIndex } from './search-index'
15
+
16
+ // Source location finding
17
+ export { findSourceLocation } from './source-lookup'
18
+
19
+ // Image finding
20
+ export { findImageSourceLocation } from './image-finder'
21
+
22
+ // Collection/markdown finding
23
+ export { findCollectionSource, findMarkdownSourceLocation, parseMarkdownContent } from './collection-finder'
24
+
25
+ // Snippet utilities (used by html-processor)
26
+ export { enhanceManifestWithSourceSnippets, extractCompleteTagSnippet, extractInnerHtmlFromSnippet, extractSourceInnerHtml } from './snippet-utils'