@nuasite/cms 0.1.0

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 (269) hide show
  1. package/README.md +237 -0
  2. package/dist/src/build-processor.d.ts +20 -0
  3. package/dist/src/build-processor.d.ts.map +1 -0
  4. package/dist/src/collection-scanner.d.ts +6 -0
  5. package/dist/src/collection-scanner.d.ts.map +1 -0
  6. package/dist/src/component-registry.d.ts +63 -0
  7. package/dist/src/component-registry.d.ts.map +1 -0
  8. package/dist/src/config.d.ts +24 -0
  9. package/dist/src/config.d.ts.map +1 -0
  10. package/dist/src/dev-middleware.d.ts +20 -0
  11. package/dist/src/dev-middleware.d.ts.map +1 -0
  12. package/dist/src/editor/ai.d.ts +60 -0
  13. package/dist/src/editor/ai.d.ts.map +1 -0
  14. package/dist/src/editor/api.d.ts +140 -0
  15. package/dist/src/editor/api.d.ts.map +1 -0
  16. package/dist/src/editor/color-utils.d.ts +106 -0
  17. package/dist/src/editor/color-utils.d.ts.map +1 -0
  18. package/dist/src/editor/components/ai-chat.d.ts +11 -0
  19. package/dist/src/editor/components/ai-chat.d.ts.map +1 -0
  20. package/dist/src/editor/components/ai-tooltip.d.ts +12 -0
  21. package/dist/src/editor/components/ai-tooltip.d.ts.map +1 -0
  22. package/dist/src/editor/components/attribute-editor.d.ts +5 -0
  23. package/dist/src/editor/components/attribute-editor.d.ts.map +1 -0
  24. package/dist/src/editor/components/block-editor.d.ts +12 -0
  25. package/dist/src/editor/components/block-editor.d.ts.map +1 -0
  26. package/dist/src/editor/components/collections-browser.d.ts +2 -0
  27. package/dist/src/editor/components/collections-browser.d.ts.map +1 -0
  28. package/dist/src/editor/components/color-toolbar.d.ts +12 -0
  29. package/dist/src/editor/components/color-toolbar.d.ts.map +1 -0
  30. package/dist/src/editor/components/confirm-dialog.d.ts +2 -0
  31. package/dist/src/editor/components/confirm-dialog.d.ts.map +1 -0
  32. package/dist/src/editor/components/create-page-modal.d.ts +2 -0
  33. package/dist/src/editor/components/create-page-modal.d.ts.map +1 -0
  34. package/dist/src/editor/components/editable-highlights.d.ts +9 -0
  35. package/dist/src/editor/components/editable-highlights.d.ts.map +1 -0
  36. package/dist/src/editor/components/error-boundary.d.ts +32 -0
  37. package/dist/src/editor/components/error-boundary.d.ts.map +1 -0
  38. package/dist/src/editor/components/fields.d.ts +75 -0
  39. package/dist/src/editor/components/fields.d.ts.map +1 -0
  40. package/dist/src/editor/components/frontmatter-fields.d.ts +29 -0
  41. package/dist/src/editor/components/frontmatter-fields.d.ts.map +1 -0
  42. package/dist/src/editor/components/highlight-overlay.d.ts +64 -0
  43. package/dist/src/editor/components/highlight-overlay.d.ts.map +1 -0
  44. package/dist/src/editor/components/image-overlay.d.ts +12 -0
  45. package/dist/src/editor/components/image-overlay.d.ts.map +1 -0
  46. package/dist/src/editor/components/markdown-editor-overlay.d.ts +6 -0
  47. package/dist/src/editor/components/markdown-editor-overlay.d.ts.map +1 -0
  48. package/dist/src/editor/components/markdown-inline-editor.d.ts +10 -0
  49. package/dist/src/editor/components/markdown-inline-editor.d.ts.map +1 -0
  50. package/dist/src/editor/components/media-library.d.ts +2 -0
  51. package/dist/src/editor/components/media-library.d.ts.map +1 -0
  52. package/dist/src/editor/components/outline.d.ts +21 -0
  53. package/dist/src/editor/components/outline.d.ts.map +1 -0
  54. package/dist/src/editor/components/redirect-countdown.d.ts +2 -0
  55. package/dist/src/editor/components/redirect-countdown.d.ts.map +1 -0
  56. package/dist/src/editor/components/seo-editor.d.ts +2 -0
  57. package/dist/src/editor/components/seo-editor.d.ts.map +1 -0
  58. package/dist/src/editor/components/text-style-toolbar.d.ts +8 -0
  59. package/dist/src/editor/components/text-style-toolbar.d.ts.map +1 -0
  60. package/dist/src/editor/components/toast/toast-container.d.ts +7 -0
  61. package/dist/src/editor/components/toast/toast-container.d.ts.map +1 -0
  62. package/dist/src/editor/components/toast/toast.d.ts +7 -0
  63. package/dist/src/editor/components/toast/toast.d.ts.map +1 -0
  64. package/dist/src/editor/components/toast/types.d.ts +7 -0
  65. package/dist/src/editor/components/toast/types.d.ts.map +1 -0
  66. package/dist/src/editor/components/toolbar.d.ts +21 -0
  67. package/dist/src/editor/components/toolbar.d.ts.map +1 -0
  68. package/dist/src/editor/config.d.ts +4 -0
  69. package/dist/src/editor/config.d.ts.map +1 -0
  70. package/dist/src/editor/constants.d.ts +101 -0
  71. package/dist/src/editor/constants.d.ts.map +1 -0
  72. package/dist/src/editor/context.d.ts +14 -0
  73. package/dist/src/editor/context.d.ts.map +1 -0
  74. package/dist/src/editor/dom.d.ts +77 -0
  75. package/dist/src/editor/dom.d.ts.map +1 -0
  76. package/dist/src/editor/editor.d.ts +64 -0
  77. package/dist/src/editor/editor.d.ts.map +1 -0
  78. package/dist/src/editor/history.d.ts +20 -0
  79. package/dist/src/editor/history.d.ts.map +1 -0
  80. package/dist/src/editor/hooks/index.d.ts +14 -0
  81. package/dist/src/editor/hooks/index.d.ts.map +1 -0
  82. package/dist/src/editor/hooks/useAIHandlers.d.ts +22 -0
  83. package/dist/src/editor/hooks/useAIHandlers.d.ts.map +1 -0
  84. package/dist/src/editor/hooks/useBlockEditorHandlers.d.ts +18 -0
  85. package/dist/src/editor/hooks/useBlockEditorHandlers.d.ts.map +1 -0
  86. package/dist/src/editor/hooks/useElementDetection.d.ts +26 -0
  87. package/dist/src/editor/hooks/useElementDetection.d.ts.map +1 -0
  88. package/dist/src/editor/hooks/useImageHoverDetection.d.ts +12 -0
  89. package/dist/src/editor/hooks/useImageHoverDetection.d.ts.map +1 -0
  90. package/dist/src/editor/hooks/useTextSelection.d.ts +23 -0
  91. package/dist/src/editor/hooks/useTextSelection.d.ts.map +1 -0
  92. package/dist/src/editor/hooks/useTooltipState.d.ts +19 -0
  93. package/dist/src/editor/hooks/useTooltipState.d.ts.map +1 -0
  94. package/dist/src/editor/hooks/utils.d.ts +32 -0
  95. package/dist/src/editor/hooks/utils.d.ts.map +1 -0
  96. package/dist/src/editor/index.d.ts +12 -0
  97. package/dist/src/editor/index.d.ts.map +1 -0
  98. package/dist/src/editor/lib/cn.d.ts +3 -0
  99. package/dist/src/editor/lib/cn.d.ts.map +1 -0
  100. package/dist/src/editor/manifest.d.ts +19 -0
  101. package/dist/src/editor/manifest.d.ts.map +1 -0
  102. package/dist/src/editor/markdown-api.d.ts +36 -0
  103. package/dist/src/editor/markdown-api.d.ts.map +1 -0
  104. package/dist/src/editor/signals.d.ts +242 -0
  105. package/dist/src/editor/signals.d.ts.map +1 -0
  106. package/dist/src/editor/storage.d.ts +27 -0
  107. package/dist/src/editor/storage.d.ts.map +1 -0
  108. package/dist/src/editor/text-styling.d.ts +350 -0
  109. package/dist/src/editor/text-styling.d.ts.map +1 -0
  110. package/dist/src/editor/themes.d.ts +38 -0
  111. package/dist/src/editor/themes.d.ts.map +1 -0
  112. package/dist/src/editor/types.d.ts +454 -0
  113. package/dist/src/editor/types.d.ts.map +1 -0
  114. package/dist/src/error-collector.d.ts +56 -0
  115. package/dist/src/error-collector.d.ts.map +1 -0
  116. package/dist/src/handlers/component-ops.d.ts +34 -0
  117. package/dist/src/handlers/component-ops.d.ts.map +1 -0
  118. package/dist/src/handlers/markdown-ops.d.ts +41 -0
  119. package/dist/src/handlers/markdown-ops.d.ts.map +1 -0
  120. package/dist/src/handlers/request-utils.d.ts +20 -0
  121. package/dist/src/handlers/request-utils.d.ts.map +1 -0
  122. package/dist/src/handlers/source-writer.d.ts +51 -0
  123. package/dist/src/handlers/source-writer.d.ts.map +1 -0
  124. package/dist/src/html-processor.d.ts +63 -0
  125. package/dist/src/html-processor.d.ts.map +1 -0
  126. package/dist/src/index.d.ts +41 -0
  127. package/dist/src/index.d.ts.map +1 -0
  128. package/dist/src/manifest-writer.d.ts +111 -0
  129. package/dist/src/manifest-writer.d.ts.map +1 -0
  130. package/dist/src/media/contember.d.ts +15 -0
  131. package/dist/src/media/contember.d.ts.map +1 -0
  132. package/dist/src/media/local.d.ts +9 -0
  133. package/dist/src/media/local.d.ts.map +1 -0
  134. package/dist/src/media/s3.d.ts +12 -0
  135. package/dist/src/media/s3.d.ts.map +1 -0
  136. package/dist/src/media/types.d.ts +40 -0
  137. package/dist/src/media/types.d.ts.map +1 -0
  138. package/dist/src/preview-generator.d.ts +19 -0
  139. package/dist/src/preview-generator.d.ts.map +1 -0
  140. package/dist/src/seo-processor.d.ts +23 -0
  141. package/dist/src/seo-processor.d.ts.map +1 -0
  142. package/dist/src/source-finder/ast-extractors.d.ts +35 -0
  143. package/dist/src/source-finder/ast-extractors.d.ts.map +1 -0
  144. package/dist/src/source-finder/ast-parser.d.ts +16 -0
  145. package/dist/src/source-finder/ast-parser.d.ts.map +1 -0
  146. package/dist/src/source-finder/cache.d.ts +18 -0
  147. package/dist/src/source-finder/cache.d.ts.map +1 -0
  148. package/dist/src/source-finder/collection-finder.d.ts +29 -0
  149. package/dist/src/source-finder/collection-finder.d.ts.map +1 -0
  150. package/dist/src/source-finder/cross-file-tracker.d.ts +39 -0
  151. package/dist/src/source-finder/cross-file-tracker.d.ts.map +1 -0
  152. package/dist/src/source-finder/element-finder.d.ts +42 -0
  153. package/dist/src/source-finder/element-finder.d.ts.map +1 -0
  154. package/dist/src/source-finder/image-finder.d.ts +24 -0
  155. package/dist/src/source-finder/image-finder.d.ts.map +1 -0
  156. package/dist/src/source-finder/index.d.ts +9 -0
  157. package/dist/src/source-finder/index.d.ts.map +1 -0
  158. package/dist/src/source-finder/search-index.d.ts +27 -0
  159. package/dist/src/source-finder/search-index.d.ts.map +1 -0
  160. package/dist/src/source-finder/snippet-utils.d.ts +90 -0
  161. package/dist/src/source-finder/snippet-utils.d.ts.map +1 -0
  162. package/dist/src/source-finder/source-lookup.d.ts +16 -0
  163. package/dist/src/source-finder/source-lookup.d.ts.map +1 -0
  164. package/dist/src/source-finder/types.d.ts +167 -0
  165. package/dist/src/source-finder/types.d.ts.map +1 -0
  166. package/dist/src/source-finder/variable-extraction.d.ts +37 -0
  167. package/dist/src/source-finder/variable-extraction.d.ts.map +1 -0
  168. package/dist/src/tailwind-colors.d.ts +54 -0
  169. package/dist/src/tailwind-colors.d.ts.map +1 -0
  170. package/dist/src/tsconfig.tsbuildinfo +1 -0
  171. package/dist/src/types.d.ts +367 -0
  172. package/dist/src/types.d.ts.map +1 -0
  173. package/dist/src/utils.d.ts +61 -0
  174. package/dist/src/utils.d.ts.map +1 -0
  175. package/dist/src/vite-plugin.d.ts +14 -0
  176. package/dist/src/vite-plugin.d.ts.map +1 -0
  177. package/dist/types/tsconfig.tsbuildinfo +1 -0
  178. package/package.json +80 -0
  179. package/src/build-processor.ts +784 -0
  180. package/src/collection-scanner.ts +304 -0
  181. package/src/component-registry.ts +393 -0
  182. package/src/config.ts +74 -0
  183. package/src/dev-middleware.ts +525 -0
  184. package/src/dist/src/tsconfig.tsbuildinfo +1 -0
  185. package/src/editor/ai.ts +185 -0
  186. package/src/editor/api.ts +513 -0
  187. package/src/editor/color-utils.ts +556 -0
  188. package/src/editor/components/ai-chat.tsx +632 -0
  189. package/src/editor/components/ai-tooltip.tsx +179 -0
  190. package/src/editor/components/attribute-editor.tsx +596 -0
  191. package/src/editor/components/block-editor.tsx +546 -0
  192. package/src/editor/components/collections-browser.tsx +248 -0
  193. package/src/editor/components/color-toolbar.tsx +314 -0
  194. package/src/editor/components/confirm-dialog.tsx +69 -0
  195. package/src/editor/components/create-page-modal.tsx +163 -0
  196. package/src/editor/components/editable-highlights.tsx +260 -0
  197. package/src/editor/components/error-boundary.tsx +87 -0
  198. package/src/editor/components/fields.tsx +387 -0
  199. package/src/editor/components/frontmatter-fields.tsx +469 -0
  200. package/src/editor/components/highlight-overlay.ts +229 -0
  201. package/src/editor/components/image-overlay.tsx +230 -0
  202. package/src/editor/components/markdown-editor-overlay.tsx +505 -0
  203. package/src/editor/components/markdown-inline-editor.tsx +780 -0
  204. package/src/editor/components/media-library.tsx +297 -0
  205. package/src/editor/components/outline.tsx +402 -0
  206. package/src/editor/components/redirect-countdown.tsx +45 -0
  207. package/src/editor/components/seo-editor.tsx +498 -0
  208. package/src/editor/components/text-style-toolbar.tsx +362 -0
  209. package/src/editor/components/toast/toast-container.tsx +15 -0
  210. package/src/editor/components/toast/toast.tsx +49 -0
  211. package/src/editor/components/toast/types.ts +7 -0
  212. package/src/editor/components/toolbar.tsx +366 -0
  213. package/src/editor/config.ts +12 -0
  214. package/src/editor/constants.ts +106 -0
  215. package/src/editor/context.tsx +38 -0
  216. package/src/editor/dom.ts +357 -0
  217. package/src/editor/editor.ts +1510 -0
  218. package/src/editor/env.d.ts +4 -0
  219. package/src/editor/history.ts +355 -0
  220. package/src/editor/hooks/index.ts +19 -0
  221. package/src/editor/hooks/useAIHandlers.ts +345 -0
  222. package/src/editor/hooks/useBlockEditorHandlers.ts +206 -0
  223. package/src/editor/hooks/useElementDetection.ts +284 -0
  224. package/src/editor/hooks/useImageHoverDetection.ts +102 -0
  225. package/src/editor/hooks/useTextSelection.ts +187 -0
  226. package/src/editor/hooks/useTooltipState.ts +126 -0
  227. package/src/editor/hooks/utils.ts +101 -0
  228. package/src/editor/index.tsx +481 -0
  229. package/src/editor/lib/cn.ts +4 -0
  230. package/src/editor/manifest.ts +25 -0
  231. package/src/editor/markdown-api.ts +209 -0
  232. package/src/editor/signals.ts +1351 -0
  233. package/src/editor/storage.ts +266 -0
  234. package/src/editor/styles.css +465 -0
  235. package/src/editor/text-styling.ts +773 -0
  236. package/src/editor/themes.ts +210 -0
  237. package/src/editor/types.ts +591 -0
  238. package/src/error-collector.ts +106 -0
  239. package/src/handlers/component-ops.ts +463 -0
  240. package/src/handlers/markdown-ops.ts +202 -0
  241. package/src/handlers/request-utils.ts +151 -0
  242. package/src/handlers/source-writer.ts +649 -0
  243. package/src/html-processor.ts +1108 -0
  244. package/src/index.ts +284 -0
  245. package/src/manifest-writer.ts +371 -0
  246. package/src/media/contember.ts +84 -0
  247. package/src/media/local.ts +114 -0
  248. package/src/media/s3.ts +133 -0
  249. package/src/media/types.ts +33 -0
  250. package/src/preview-generator.ts +293 -0
  251. package/src/seo-processor.ts +567 -0
  252. package/src/source-finder/ast-extractors.ts +185 -0
  253. package/src/source-finder/ast-parser.ts +150 -0
  254. package/src/source-finder/cache.ts +76 -0
  255. package/src/source-finder/collection-finder.ts +335 -0
  256. package/src/source-finder/cross-file-tracker.ts +741 -0
  257. package/src/source-finder/element-finder.ts +387 -0
  258. package/src/source-finder/image-finder.ts +283 -0
  259. package/src/source-finder/index.ts +37 -0
  260. package/src/source-finder/search-index.ts +525 -0
  261. package/src/source-finder/snippet-utils.ts +668 -0
  262. package/src/source-finder/source-lookup.ts +200 -0
  263. package/src/source-finder/types.ts +210 -0
  264. package/src/source-finder/variable-extraction.ts +406 -0
  265. package/src/tailwind-colors.ts +874 -0
  266. package/src/tsconfig.json +25 -0
  267. package/src/types.ts +406 -0
  268. package/src/utils.ts +186 -0
  269. package/src/vite-plugin.ts +42 -0
@@ -0,0 +1,387 @@
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
+ // Treat <wbr> elements as empty (word break opportunity, no visible content)
32
+ if (node.type === 'element' && (node as ElementNode).name.toLowerCase() === 'wbr') {
33
+ return ''
34
+ }
35
+ if ('children' in node && Array.isArray(node.children)) {
36
+ return node.children.map(getTextContent).join('')
37
+ }
38
+ return ''
39
+ }
40
+
41
+ /**
42
+ * Check for expression children and extract variable names
43
+ */
44
+ export function hasExpressionChild(node: AstroNode): { found: boolean; varNames: string[] } {
45
+ const varNames: string[] = []
46
+ if (node.type === 'expression') {
47
+ // Try to extract variable name from expression
48
+ // The expression node children contain the text representation
49
+ const exprText = getTextContent(node)
50
+ // Extract variable paths like {foo}, {foo.bar}, {items[0]}, {config.nav.title}, {links[0].text}
51
+ const fullPath = parseExpressionPath(exprText)
52
+ if (fullPath) {
53
+ varNames.push(fullPath)
54
+ }
55
+ return { found: true, varNames }
56
+ }
57
+ if ('children' in node && Array.isArray(node.children)) {
58
+ for (const child of node.children) {
59
+ const result = hasExpressionChild(child)
60
+ if (result.found) {
61
+ varNames.push(...result.varNames)
62
+ }
63
+ }
64
+ }
65
+ return { found: varNames.length > 0, varNames }
66
+ }
67
+
68
+ // ============================================================================
69
+ // Element Finding
70
+ // ============================================================================
71
+
72
+ /**
73
+ * Walk the Astro AST to find elements matching a tag with specific text content.
74
+ * Returns the best match (local variables or static content) AND all prop/import candidates
75
+ * that need cross-file verification for multiple same-tag elements.
76
+ * @param propAliases - Map of local variable names to prop names from Astro.props (for cross-file tracking)
77
+ * @param imports - Import information from frontmatter (for cross-file tracking)
78
+ */
79
+ export function findElementWithText(
80
+ ast: AstroNode,
81
+ tag: string,
82
+ searchText: string,
83
+ variableDefinitions: VariableDefinition[],
84
+ propAliases: Map<string, string> = new Map(),
85
+ imports: ImportInfo[] = [],
86
+ ): FindElementResult {
87
+ const normalizedSearch = normalizeText(searchText)
88
+ const tagLower = tag.toLowerCase()
89
+ let bestMatch: TemplateMatch | null = null
90
+ let bestScore = 0
91
+ const propCandidates: TemplateMatch[] = []
92
+ const importCandidates: TemplateMatch[] = []
93
+
94
+ /**
95
+ * Extract the base variable name from an expression path.
96
+ * e.g., 'items[0]' -> 'items', 'config.nav.title' -> 'config'
97
+ */
98
+ function getBaseVarName(exprPath: string): string {
99
+ const match = exprPath.match(/^(\w+)/)
100
+ return match?.[1] ?? exprPath
101
+ }
102
+
103
+ function visit(node: AstroNode) {
104
+ // Check if this is an element or component matching our tag
105
+ if ((node.type === 'element' || node.type === 'component') && node.name.toLowerCase() === tagLower) {
106
+ const elemNode = node as ElementNode | ComponentNode
107
+ const textContent = getTextContent(elemNode)
108
+ const normalizedContent = normalizeText(textContent)
109
+ const line = elemNode.position?.start.line ?? 0
110
+
111
+ // Check for expression (variable reference)
112
+ const exprInfo = hasExpressionChild(elemNode)
113
+ if (exprInfo.found && exprInfo.varNames.length > 0) {
114
+ // Look for matching variable definition
115
+ for (const exprPath of exprInfo.varNames) {
116
+ let foundInLocal = false
117
+
118
+ for (const def of variableDefinitions) {
119
+ // Build the full definition path for comparison
120
+ const defPath = buildDefinitionPath(def)
121
+ // Check if the expression path matches the definition path
122
+ if (defPath === exprPath) {
123
+ foundInLocal = true
124
+ const normalizedDef = normalizeText(def.value)
125
+ if (normalizedDef === normalizedSearch) {
126
+ // Found a variable match - this is highest priority
127
+ if (bestScore < 100) {
128
+ bestScore = 100
129
+ bestMatch = {
130
+ line,
131
+ type: 'variable',
132
+ variableName: defPath,
133
+ definitionLine: def.line,
134
+ }
135
+ }
136
+ return
137
+ }
138
+ }
139
+ }
140
+
141
+ // If not found in local definitions, check if it's from props or imports
142
+ if (!foundInLocal) {
143
+ const baseVar = getBaseVarName(exprPath)
144
+
145
+ // Check props first
146
+ const actualPropName = propAliases.get(baseVar)
147
+ if (actualPropName) {
148
+ // This expression uses a prop - collect as candidate for cross-file verification
149
+ // (don't set bestMatch yet - we need to verify each candidate)
150
+ propCandidates.push({
151
+ line,
152
+ type: 'variable',
153
+ usesProp: true,
154
+ propName: actualPropName, // Use the actual prop name, not the local alias
155
+ expressionPath: exprPath,
156
+ })
157
+ } else {
158
+ // Check if it's from an import
159
+ const importInfo = imports.find((imp) => imp.localName === baseVar)
160
+ if (importInfo) {
161
+ // This expression uses an import - collect as candidate for cross-file verification
162
+ importCandidates.push({
163
+ line,
164
+ type: 'variable',
165
+ usesImport: true,
166
+ importInfo,
167
+ expressionPath: exprPath,
168
+ })
169
+ }
170
+ }
171
+ }
172
+ }
173
+ }
174
+
175
+ // Check for direct text match (static content)
176
+ // Only match if there's meaningful text content (not just variable names/expressions)
177
+ if (normalizedContent && normalizedContent.length >= 2 && normalizedSearch.length > 0) {
178
+ // For short search text (<= 10 chars), require exact match
179
+ if (normalizedSearch.length <= 10) {
180
+ if (normalizedContent.includes(normalizedSearch)) {
181
+ const score = 80
182
+ if (score > bestScore) {
183
+ bestScore = score
184
+ const actualLine = findTextLine(elemNode, normalizedSearch)
185
+ bestMatch = {
186
+ line: actualLine ?? line,
187
+ type: 'static',
188
+ }
189
+ }
190
+ }
191
+ } // For longer search text, check if content contains a significant portion
192
+ else if (normalizedSearch.length > 10) {
193
+ const textPreview = normalizedSearch.slice(0, Math.min(30, normalizedSearch.length))
194
+ if (normalizedContent.includes(textPreview)) {
195
+ const matchLength = Math.min(normalizedSearch.length, normalizedContent.length)
196
+ const score = 50 + (matchLength / normalizedSearch.length) * 40
197
+ if (score > bestScore) {
198
+ bestScore = score
199
+ const actualLine = findTextLine(elemNode, textPreview)
200
+ bestMatch = {
201
+ line: actualLine ?? line,
202
+ type: 'static',
203
+ }
204
+ }
205
+ } // Try matching first few words for very long text
206
+ else if (normalizedSearch.length > 20) {
207
+ const firstWords = normalizedSearch.split(' ').slice(0, 3).join(' ')
208
+ if (firstWords && normalizedContent.includes(firstWords)) {
209
+ const score = 40
210
+ if (score > bestScore) {
211
+ bestScore = score
212
+ const actualLine = findTextLine(elemNode, firstWords)
213
+ bestMatch = {
214
+ line: actualLine ?? line,
215
+ type: 'static',
216
+ }
217
+ }
218
+ }
219
+ }
220
+ }
221
+ }
222
+ }
223
+
224
+ // Recursively visit children
225
+ if ('children' in node && Array.isArray(node.children)) {
226
+ for (const child of node.children) {
227
+ visit(child)
228
+ }
229
+ }
230
+ }
231
+
232
+ function findTextLine(node: AstroNode, searchText: string): number | null {
233
+ if (node.type === 'text') {
234
+ const textNode = node as TextNode
235
+ if (normalizeText(textNode.value).includes(searchText)) {
236
+ return textNode.position?.start.line ?? null
237
+ }
238
+ }
239
+ if ('children' in node && Array.isArray(node.children)) {
240
+ for (const child of node.children) {
241
+ const line = findTextLine(child, searchText)
242
+ if (line !== null) return line
243
+ }
244
+ }
245
+ return null
246
+ }
247
+
248
+ visit(ast)
249
+ return { bestMatch, propCandidates, importCandidates }
250
+ }
251
+
252
+ // ============================================================================
253
+ // Component Prop Finding
254
+ // ============================================================================
255
+
256
+ /**
257
+ * Walk the Astro AST to find component props with specific text value
258
+ */
259
+ export function findComponentProp(
260
+ ast: AstroNode,
261
+ searchText: string,
262
+ ): ComponentPropMatch | null {
263
+ const normalizedSearch = normalizeText(searchText)
264
+
265
+ function visit(node: AstroNode): ComponentPropMatch | null {
266
+ // Check component nodes (PascalCase names)
267
+ if (node.type === 'component') {
268
+ const compNode = node as ComponentNode
269
+ for (const attr of compNode.attributes) {
270
+ if (attr.type === 'attribute' && attr.kind === 'quoted') {
271
+ const normalizedValue = normalizeText(attr.value)
272
+ if (normalizedValue === normalizedSearch) {
273
+ return {
274
+ line: attr.position?.start.line ?? compNode.position?.start.line ?? 0,
275
+ propName: attr.name,
276
+ propValue: attr.value,
277
+ }
278
+ }
279
+ }
280
+ }
281
+ }
282
+
283
+ // Recursively visit children
284
+ if ('children' in node && Array.isArray(node.children)) {
285
+ for (const child of node.children) {
286
+ const result = visit(child)
287
+ if (result) return result
288
+ }
289
+ }
290
+
291
+ return null
292
+ }
293
+
294
+ return visit(ast)
295
+ }
296
+
297
+ /**
298
+ * Walk the Astro AST to find component usages with expression props.
299
+ * Looks for patterns like: <Nav items={navItems} />
300
+ * @param ast - The Astro AST
301
+ * @param componentName - The component name to search for (e.g., 'Nav')
302
+ * @param propName - The prop name to find (e.g., 'items')
303
+ */
304
+ export function findExpressionProp(
305
+ ast: AstroNode,
306
+ componentName: string,
307
+ propName: string,
308
+ ): ExpressionPropMatch | null {
309
+ function visit(node: AstroNode): ExpressionPropMatch | null {
310
+ // Check component nodes matching the name
311
+ if (node.type === 'component') {
312
+ const compNode = node as ComponentNode
313
+ if (compNode.name === componentName) {
314
+ for (const attr of compNode.attributes) {
315
+ // Check for expression attributes: items={navItems}
316
+ if (attr.type === 'attribute' && attr.name === propName && attr.kind === 'expression') {
317
+ // The value contains the expression text
318
+ const exprText = attr.value?.trim() || ''
319
+ if (exprText) {
320
+ return {
321
+ componentName,
322
+ propName,
323
+ expressionText: exprText,
324
+ line: attr.position?.start.line ?? compNode.position?.start.line ?? 0,
325
+ }
326
+ }
327
+ }
328
+ }
329
+ }
330
+ }
331
+
332
+ // Recursively visit children
333
+ if ('children' in node && Array.isArray(node.children)) {
334
+ for (const child of node.children) {
335
+ const result = visit(child)
336
+ if (result) return result
337
+ }
338
+ }
339
+
340
+ return null
341
+ }
342
+
343
+ return visit(ast)
344
+ }
345
+
346
+ /**
347
+ * Walk the Astro AST to find component usages with spread props.
348
+ * Looks for patterns like: <Card {...cardProps} />
349
+ * @param ast - The Astro AST
350
+ * @param componentName - The component name to search for (e.g., 'Card')
351
+ */
352
+ export function findSpreadProp(
353
+ ast: AstroNode,
354
+ componentName: string,
355
+ ): SpreadPropMatch | null {
356
+ function visit(node: AstroNode): SpreadPropMatch | null {
357
+ // Check component nodes matching the name
358
+ if (node.type === 'component') {
359
+ const compNode = node as ComponentNode
360
+ if (compNode.name === componentName) {
361
+ for (const attr of compNode.attributes) {
362
+ // Check for spread attributes: {...cardProps}
363
+ // In Astro AST: type='attribute', kind='spread', name=variable name
364
+ if (attr.type === 'attribute' && attr.kind === 'spread' && attr.name) {
365
+ return {
366
+ componentName,
367
+ spreadVarName: attr.name,
368
+ line: attr.position?.start.line ?? compNode.position?.start.line ?? 0,
369
+ }
370
+ }
371
+ }
372
+ }
373
+ }
374
+
375
+ // Recursively visit children
376
+ if ('children' in node && Array.isArray(node.children)) {
377
+ for (const child of node.children) {
378
+ const result = visit(child)
379
+ if (result) return result
380
+ }
381
+ }
382
+
383
+ return null
384
+ }
385
+
386
+ return visit(ast)
387
+ }
@@ -0,0 +1,283 @@
1
+ import type { ComponentNode, 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 or Image component usages 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
+ // Check <img> elements
26
+ if (node.type === 'element') {
27
+ const elemNode = node as ElementNode
28
+ if (elemNode.name.toLowerCase() === 'img') {
29
+ for (const attr of elemNode.attributes) {
30
+ if (attr.type === 'attribute' && attr.name === 'src' && attr.value === imageSrc) {
31
+ const srcLine = attr.position?.start.line ?? elemNode.position?.start.line ?? 0
32
+ const snippet = extractImageSnippet(lines, srcLine - 1)
33
+ return {
34
+ line: srcLine,
35
+ src: imageSrc,
36
+ snippet,
37
+ }
38
+ }
39
+ }
40
+ }
41
+ }
42
+
43
+ // Check component nodes with src attributes (e.g., <Image src="..." />)
44
+ if (node.type === 'component') {
45
+ const compNode = node as ComponentNode
46
+ for (const attr of compNode.attributes) {
47
+ if (attr.type === 'attribute' && attr.name === 'src' && attr.value === imageSrc) {
48
+ const srcLine = attr.position?.start.line ?? compNode.position?.start.line ?? 0
49
+ const snippet = extractImageSnippet(lines, srcLine - 1)
50
+ return {
51
+ line: srcLine,
52
+ src: imageSrc,
53
+ snippet,
54
+ }
55
+ }
56
+ }
57
+ }
58
+
59
+ // Recursively visit children
60
+ if ('children' in node && Array.isArray(node.children)) {
61
+ for (const child of node.children) {
62
+ const result = visit(child)
63
+ if (result) return result
64
+ }
65
+ }
66
+
67
+ return null
68
+ }
69
+
70
+ return visit(ast)
71
+ }
72
+
73
+ /**
74
+ * Walk the Astro AST to find img elements near a given source line.
75
+ * Used as a fallback when the src value can't be matched (expression attributes).
76
+ * Returns the img element closest to the expected line.
77
+ */
78
+ export function findImageElementNearLine(
79
+ ast: AstroNode,
80
+ expectedLine: number,
81
+ lines: string[],
82
+ ): ImageMatch | null {
83
+ let bestMatch: ImageMatch | null = null
84
+ let bestDistance = Infinity
85
+
86
+ function visit(node: AstroNode): void {
87
+ if (node.type === 'element') {
88
+ const elemNode = node as ElementNode
89
+ if (elemNode.name.toLowerCase() === 'img') {
90
+ // Check if this img has a src attribute (any kind)
91
+ const srcAttr = elemNode.attributes.find(
92
+ attr => attr.type === 'attribute' && attr.name === 'src',
93
+ )
94
+ if (srcAttr) {
95
+ const imgLine = srcAttr.position?.start.line ?? elemNode.position?.start.line ?? 0
96
+ const distance = Math.abs(imgLine - expectedLine)
97
+
98
+ if (distance < bestDistance) {
99
+ bestDistance = distance
100
+ const snippet = extractImageSnippet(lines, imgLine - 1)
101
+ bestMatch = {
102
+ line: imgLine,
103
+ src: srcAttr.value,
104
+ snippet,
105
+ }
106
+ }
107
+ }
108
+ }
109
+ }
110
+
111
+ if ('children' in node && Array.isArray(node.children)) {
112
+ for (const child of node.children) {
113
+ visit(child)
114
+ }
115
+ }
116
+ }
117
+
118
+ visit(ast)
119
+
120
+ // Only return match if within a reasonable distance (15 lines)
121
+ return bestMatch && bestDistance <= 15 ? bestMatch : null
122
+ }
123
+
124
+ // ============================================================================
125
+ // Image Source Location Finding
126
+ // ============================================================================
127
+
128
+ /**
129
+ * Parse URLs from a srcset attribute string.
130
+ * srcset format: "url1 480w, url2 768w, ..."
131
+ */
132
+ function parseSrcsetUrls(srcSet: string): string[] {
133
+ return srcSet
134
+ .split(',')
135
+ .map(entry => entry.trim().split(/\s+/)[0])
136
+ .filter((url): url is string => !!url && url.length > 0)
137
+ }
138
+
139
+ /**
140
+ * Find source file and line number for an image by its src attribute.
141
+ * Also checks srcset URLs as fallback when src doesn't match (e.g., when src
142
+ * is a local upload path but srcset contains CDN-transformed original URLs).
143
+ * Uses pre-built search index for fast lookups.
144
+ */
145
+ export async function findImageSourceLocation(
146
+ imageSrc: string,
147
+ imageSrcSet?: string,
148
+ ): Promise<SourceLocation | undefined> {
149
+ // Use index if available (much faster)
150
+ if (isSearchIndexInitialized()) {
151
+ const result = findInImageIndex(imageSrc)
152
+ if (result) return result
153
+
154
+ // Fallback: try URLs extracted from srcset
155
+ if (imageSrcSet) {
156
+ const srcsetUrls = parseSrcsetUrls(imageSrcSet)
157
+ for (const url of srcsetUrls) {
158
+ const srcsetResult = findInImageIndex(url)
159
+ if (srcsetResult) return srcsetResult
160
+ }
161
+ }
162
+
163
+ return undefined
164
+ }
165
+
166
+ // Fallback to slow search if index not initialized
167
+ const srcDir = path.join(getProjectRoot(), 'src')
168
+
169
+ try {
170
+ const searchDirs = [
171
+ path.join(srcDir, 'pages'),
172
+ path.join(srcDir, 'components'),
173
+ path.join(srcDir, 'layouts'),
174
+ ]
175
+
176
+ for (const dir of searchDirs) {
177
+ try {
178
+ const result = await searchDirectoryForImage(dir, imageSrc)
179
+ if (result) {
180
+ return result
181
+ }
182
+ } catch {
183
+ // Directory doesn't exist, continue
184
+ }
185
+ }
186
+ } catch {
187
+ // Search failed
188
+ }
189
+
190
+ return undefined
191
+ }
192
+
193
+ // ============================================================================
194
+ // Directory Search for Images
195
+ // ============================================================================
196
+
197
+ /**
198
+ * Recursively search directory for image with matching src
199
+ */
200
+ export async function searchDirectoryForImage(
201
+ dir: string,
202
+ imageSrc: string,
203
+ ): Promise<SourceLocation | undefined> {
204
+ try {
205
+ const entries = await fs.readdir(dir, { withFileTypes: true })
206
+
207
+ for (const entry of entries) {
208
+ const fullPath = path.join(dir, entry.name)
209
+
210
+ if (entry.isDirectory()) {
211
+ const result = await searchDirectoryForImage(fullPath, imageSrc)
212
+ if (result) return result
213
+ } else if (entry.isFile() && (entry.name.endsWith('.astro') || entry.name.endsWith('.tsx') || entry.name.endsWith('.jsx'))) {
214
+ const result = await searchFileForImage(fullPath, imageSrc)
215
+ if (result) return result
216
+ }
217
+ }
218
+ } catch {
219
+ // Error reading directory
220
+ }
221
+
222
+ return undefined
223
+ }
224
+
225
+ /**
226
+ * Search a single file for an image with matching src.
227
+ * Uses caching for better performance.
228
+ */
229
+ async function searchFileForImage(
230
+ filePath: string,
231
+ imageSrc: string,
232
+ ): Promise<SourceLocation | undefined> {
233
+ try {
234
+ // Use cached parsed file
235
+ const cached = await getCachedParsedFile(filePath)
236
+ if (!cached) return undefined
237
+
238
+ const { lines, ast } = cached
239
+
240
+ // Use AST parsing for Astro files
241
+ if (filePath.endsWith('.astro')) {
242
+ const imageMatch = findImageElement(ast, imageSrc, lines)
243
+
244
+ if (imageMatch) {
245
+ return {
246
+ file: path.relative(getProjectRoot(), filePath),
247
+ line: imageMatch.line,
248
+ snippet: imageMatch.snippet,
249
+ type: 'static',
250
+ }
251
+ }
252
+ }
253
+
254
+ // Regex fallback for TSX/JSX files or if AST parsing failed
255
+ const srcPatterns = [
256
+ `src="${imageSrc}"`,
257
+ `src='${imageSrc}'`,
258
+ ]
259
+
260
+ for (let i = 0; i < lines.length; i++) {
261
+ const line = lines[i]
262
+ if (!line) continue
263
+
264
+ for (const pattern of srcPatterns) {
265
+ if (line.includes(pattern)) {
266
+ // Found the image, extract the full <img> tag as snippet
267
+ const snippet = extractImageSnippet(lines, i)
268
+
269
+ return {
270
+ file: path.relative(getProjectRoot(), filePath),
271
+ line: i + 1,
272
+ snippet,
273
+ type: 'static',
274
+ }
275
+ }
276
+ }
277
+ }
278
+ } catch {
279
+ // Error reading file
280
+ }
281
+
282
+ return undefined
283
+ }
@@ -0,0 +1,37 @@
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
+ // Attribute source finding
20
+ export { findAttributeSourceLocation } from './cross-file-tracker'
21
+
22
+ // Image finding
23
+ export { findImageSourceLocation } from './image-finder'
24
+
25
+ // Collection/markdown finding
26
+ export { findCollectionSource, findMarkdownSourceLocation, parseMarkdownContent } from './collection-finder'
27
+
28
+ // Snippet utilities (used by html-processor)
29
+ export {
30
+ enhanceManifestWithSourceSnippets,
31
+ extractCompleteTagSnippet,
32
+ extractInnerHtmlFromSnippet,
33
+ extractOpeningTagWithLine,
34
+ extractSourceSnippet,
35
+ updateAttributeSources,
36
+ updateColorClassSources,
37
+ } from './snippet-utils'