@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,525 @@
1
+ import type { ComponentNode, ElementNode, Node as AstroNode, TextNode } 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 { buildDefinitionPath, parseExpressionPath } from './ast-extractors'
7
+ import { getCachedParsedFile } from './ast-parser'
8
+ import {
9
+ addToImageSearchIndex,
10
+ addToTextSearchIndex,
11
+ getDirectoryCache,
12
+ getImageSearchIndex,
13
+ getTextSearchIndex,
14
+ isSearchIndexInitialized,
15
+ setSearchIndexInitialized,
16
+ } from './cache'
17
+ import { escapeRegex } from '../utils'
18
+ import { extractImageSnippet, extractInnerHtmlFromSnippet, normalizeText } from './snippet-utils'
19
+ import type { CachedParsedFile, SourceLocation } from './types'
20
+
21
+ // ============================================================================
22
+ // File Collection
23
+ // ============================================================================
24
+
25
+ /**
26
+ * Collect all .astro files in a directory recursively
27
+ */
28
+ export async function collectAstroFiles(dir: string): Promise<string[]> {
29
+ const cache = getDirectoryCache()
30
+ const cached = cache.get(dir)
31
+ if (cached) return cached
32
+
33
+ const results: string[] = []
34
+
35
+ try {
36
+ const entries = await fs.readdir(dir, { withFileTypes: true })
37
+
38
+ await Promise.all(entries.map(async (entry) => {
39
+ const fullPath = path.join(dir, entry.name)
40
+ if (entry.isDirectory()) {
41
+ const subFiles = await collectAstroFiles(fullPath)
42
+ results.push(...subFiles)
43
+ } else if (entry.isFile() && (entry.name.endsWith('.astro') || entry.name.endsWith('.tsx') || entry.name.endsWith('.jsx'))) {
44
+ results.push(fullPath)
45
+ }
46
+ }))
47
+ } catch {
48
+ // Directory doesn't exist
49
+ }
50
+
51
+ cache.set(dir, results)
52
+ return results
53
+ }
54
+
55
+ // ============================================================================
56
+ // Index Initialization
57
+ // ============================================================================
58
+
59
+ /**
60
+ * Initialize search index by pre-scanning all source files.
61
+ * This is much faster than searching per-entry.
62
+ */
63
+ export async function initializeSearchIndex(): Promise<void> {
64
+ if (isSearchIndexInitialized()) return
65
+
66
+ const srcDir = path.join(getProjectRoot(), 'src')
67
+ const searchDirs = [
68
+ path.join(srcDir, 'components'),
69
+ path.join(srcDir, 'pages'),
70
+ path.join(srcDir, 'layouts'),
71
+ ]
72
+
73
+ // Collect all Astro files first
74
+ const allFiles: string[] = []
75
+ for (const dir of searchDirs) {
76
+ try {
77
+ const files = await collectAstroFiles(dir)
78
+ allFiles.push(...files)
79
+ } catch {
80
+ // Directory doesn't exist
81
+ }
82
+ }
83
+
84
+ // Parse all files in parallel and build indexes
85
+ await Promise.all(allFiles.map(async (filePath) => {
86
+ try {
87
+ const cached = await getCachedParsedFile(filePath)
88
+ if (!cached) return
89
+
90
+ const relFile = path.relative(getProjectRoot(), filePath)
91
+
92
+ // Index all text content from this file
93
+ indexFileContent(cached, relFile)
94
+
95
+ // Index all images from this file
96
+ indexFileImages(cached, relFile)
97
+ } catch {
98
+ // Skip files that fail to parse
99
+ }
100
+ }))
101
+
102
+ setSearchIndexInitialized(true)
103
+ }
104
+
105
+ // ============================================================================
106
+ // Content Indexing
107
+ // ============================================================================
108
+
109
+ // Helper for indexing - get text content from node
110
+ // Treats <br> elements as whitespace to match rendered HTML behavior
111
+ function getTextContent(node: AstroNode): string {
112
+ if (node.type === 'text') {
113
+ return (node as TextNode).value
114
+ }
115
+ // Treat <br> elements as whitespace (they create line breaks in rendered HTML)
116
+ if (node.type === 'element' && (node as ElementNode).name.toLowerCase() === 'br') {
117
+ return ' '
118
+ }
119
+ // Treat <wbr> elements as empty (word break opportunity, no visible content)
120
+ if (node.type === 'element' && (node as ElementNode).name.toLowerCase() === 'wbr') {
121
+ return ''
122
+ }
123
+ if ('children' in node && Array.isArray(node.children)) {
124
+ return node.children.map(getTextContent).join('')
125
+ }
126
+ return ''
127
+ }
128
+
129
+ // Helper for indexing - check for expression children
130
+ function hasExpressionChild(node: AstroNode): { found: boolean; varNames: string[] } {
131
+ const varNames: string[] = []
132
+ if (node.type === 'expression') {
133
+ const exprText = getTextContent(node)
134
+ const fullPath = parseExpressionPath(exprText)
135
+ if (fullPath) {
136
+ varNames.push(fullPath)
137
+ }
138
+ return { found: true, varNames }
139
+ }
140
+ if ('children' in node && Array.isArray(node.children)) {
141
+ for (const child of node.children) {
142
+ const result = hasExpressionChild(child)
143
+ if (result.found) {
144
+ varNames.push(...result.varNames)
145
+ }
146
+ }
147
+ }
148
+ return { found: varNames.length > 0, varNames }
149
+ }
150
+
151
+ /**
152
+ * Extract complete tag snippet including content and indentation.
153
+ * Local version for indexing (to avoid circular dependency)
154
+ */
155
+ function extractCompleteTagSnippet(lines: string[], startLine: number, tag: string): string {
156
+ const escapedTag = escapeRegex(tag)
157
+ const openTagPattern = new RegExp(`<${escapedTag}(?:[\\s>]|$)`, 'gi')
158
+
159
+ let actualStartLine = startLine
160
+ const startLineContent = lines[startLine] || ''
161
+ if (!openTagPattern.test(startLineContent)) {
162
+ for (let i = startLine - 1; i >= Math.max(0, startLine - 20); i--) {
163
+ const line = lines[i]
164
+ if (!line) continue
165
+ openTagPattern.lastIndex = 0
166
+ if (openTagPattern.test(line)) {
167
+ actualStartLine = i
168
+ break
169
+ }
170
+ }
171
+ }
172
+
173
+ const snippetLines: string[] = []
174
+ let depth = 0
175
+ let foundClosing = false
176
+
177
+ for (let i = actualStartLine; i < Math.min(actualStartLine + 30, lines.length); i++) {
178
+ const line = lines[i]
179
+ if (!line) continue
180
+
181
+ snippetLines.push(line)
182
+
183
+ const openTags = (line.match(new RegExp(`<${escapedTag}(?:[\\s>]|$)`, 'gi')) || []).length
184
+ const selfClosing = (line.match(new RegExp(`<${escapedTag}[^>]*/>`, 'gi')) || []).length
185
+ const closeTags = (line.match(new RegExp(`</${escapedTag}>`, 'gi')) || []).length
186
+
187
+ depth += openTags - selfClosing - closeTags
188
+
189
+ if (selfClosing > 0 || (depth <= 0 && (closeTags > 0 || openTags > 0))) {
190
+ foundClosing = true
191
+ break
192
+ }
193
+ }
194
+
195
+ if (!foundClosing && snippetLines.length > 1) {
196
+ return snippetLines[0]!
197
+ }
198
+
199
+ return snippetLines.join('\n')
200
+ }
201
+
202
+ /**
203
+ * Extract the opening tag from source lines with its start line number.
204
+ * Local version for indexing (to avoid circular dependency)
205
+ */
206
+ function extractOpeningTagWithLine(
207
+ lines: string[],
208
+ startLine: number,
209
+ tag: string,
210
+ ): { snippet: string; startLine: number } | undefined {
211
+ const escapedTag = escapeRegex(tag)
212
+ const openTagPattern = new RegExp(`<${escapedTag}(?:[\\s>]|$)`, 'gi')
213
+
214
+ let actualStartLine = startLine
215
+ const startLineContent = lines[startLine] || ''
216
+ if (!openTagPattern.test(startLineContent)) {
217
+ for (let i = startLine - 1; i >= Math.max(0, startLine - 20); i--) {
218
+ const line = lines[i]
219
+ if (!line) continue
220
+ openTagPattern.lastIndex = 0
221
+ if (openTagPattern.test(line)) {
222
+ actualStartLine = i
223
+ break
224
+ }
225
+ }
226
+ }
227
+
228
+ const snippetLines: string[] = []
229
+ for (let i = actualStartLine; i < Math.min(actualStartLine + 10, lines.length); i++) {
230
+ const line = lines[i]
231
+ if (!line) continue
232
+
233
+ snippetLines.push(line)
234
+ const combined = snippetLines.join('\n')
235
+
236
+ const openTagMatch = combined.match(new RegExp(`<${escapedTag}[^>]*>`, 'i'))
237
+ if (openTagMatch) {
238
+ return { snippet: openTagMatch[0], startLine: actualStartLine }
239
+ }
240
+
241
+ const selfClosingMatch = combined.match(new RegExp(`<${escapedTag}[^>]*/\\s*>`, 'i'))
242
+ if (selfClosingMatch) {
243
+ return { snippet: selfClosingMatch[0], startLine: actualStartLine }
244
+ }
245
+ }
246
+
247
+ return undefined
248
+ }
249
+
250
+ /**
251
+ * Index all searchable text content from a parsed file
252
+ */
253
+ export function indexFileContent(cached: CachedParsedFile, relFile: string): void {
254
+ // Walk AST and collect all text elements
255
+ function visit(node: AstroNode) {
256
+ if ((node.type === 'element' || node.type === 'component')) {
257
+ const elemNode = node as ElementNode | ComponentNode
258
+ const tag = elemNode.name.toLowerCase()
259
+ const textContent = getTextContent(elemNode)
260
+ const normalizedText = normalizeText(textContent)
261
+ const line = elemNode.position?.start.line ?? 0
262
+
263
+ if (normalizedText && normalizedText.length >= 2) {
264
+ // Check for variable references
265
+ const exprInfo = hasExpressionChild(elemNode)
266
+ if (exprInfo.found && exprInfo.varNames.length > 0) {
267
+ for (const exprPath of exprInfo.varNames) {
268
+ for (const def of cached.variableDefinitions) {
269
+ // Build the full definition path for comparison
270
+ // For array indices (numeric names), use bracket notation
271
+ const defPath = buildDefinitionPath(def)
272
+ // Check if the expression path matches the definition path
273
+ // e.g., 'config.nav.title' matches def with parentName='config.nav', name='title'
274
+ // or 'items[0]' matches def with parentName='items', name='0'
275
+ if (defPath === exprPath) {
276
+ const normalizedDef = normalizeText(def.value)
277
+ const completeSnippet = extractCompleteTagSnippet(cached.lines, line - 1, tag)
278
+ const snippet = extractInnerHtmlFromSnippet(completeSnippet, tag) ?? completeSnippet
279
+
280
+ addToTextSearchIndex({
281
+ file: relFile,
282
+ line: def.line,
283
+ snippet: cached.lines[def.line - 1] || '',
284
+ type: 'variable',
285
+ variableName: defPath,
286
+ definitionLine: def.line,
287
+ normalizedText: normalizedDef,
288
+ tag,
289
+ })
290
+ }
291
+ }
292
+ }
293
+ }
294
+
295
+ // Index static text content
296
+ const completeSnippet = extractCompleteTagSnippet(cached.lines, line - 1, tag)
297
+ const snippet = extractInnerHtmlFromSnippet(completeSnippet, tag) ?? completeSnippet
298
+ const openingTagInfo = extractOpeningTagWithLine(cached.lines, line - 1, tag)
299
+
300
+ addToTextSearchIndex({
301
+ file: relFile,
302
+ line,
303
+ snippet,
304
+ openingTagSnippet: openingTagInfo?.snippet,
305
+ type: 'static',
306
+ normalizedText,
307
+ tag,
308
+ })
309
+ }
310
+
311
+ // Also index component props
312
+ if (node.type === 'component') {
313
+ for (const attr of elemNode.attributes) {
314
+ if (attr.type === 'attribute' && attr.kind === 'quoted' && attr.value) {
315
+ const normalizedValue = normalizeText(attr.value)
316
+ if (normalizedValue && normalizedValue.length >= 2) {
317
+ addToTextSearchIndex({
318
+ file: relFile,
319
+ line: attr.position?.start.line ?? line,
320
+ snippet: cached.lines[(attr.position?.start.line ?? line) - 1] || '',
321
+ type: 'prop',
322
+ variableName: attr.name,
323
+ normalizedText: normalizedValue,
324
+ tag,
325
+ })
326
+ }
327
+ }
328
+ }
329
+ }
330
+ }
331
+
332
+ if ('children' in node && Array.isArray(node.children)) {
333
+ for (const child of node.children) {
334
+ visit(child)
335
+ }
336
+ }
337
+ }
338
+
339
+ visit(cached.ast)
340
+ }
341
+
342
+ /**
343
+ * Index all images from a parsed file
344
+ */
345
+ export function indexFileImages(cached: CachedParsedFile, relFile: string): void {
346
+ // For Astro files, use AST
347
+ if (relFile.endsWith('.astro')) {
348
+ function visit(node: AstroNode) {
349
+ if (node.type === 'element') {
350
+ const elemNode = node as ElementNode
351
+ if (elemNode.name.toLowerCase() === 'img') {
352
+ for (const attr of elemNode.attributes) {
353
+ if (attr.type === 'attribute' && attr.name === 'src' && attr.value) {
354
+ const srcLine = attr.position?.start.line ?? elemNode.position?.start.line ?? 0
355
+ const snippet = extractImageSnippet(cached.lines, srcLine - 1)
356
+ addToImageSearchIndex({
357
+ file: relFile,
358
+ line: srcLine,
359
+ snippet,
360
+ src: attr.value,
361
+ })
362
+ }
363
+ }
364
+ }
365
+ }
366
+
367
+ // Also index component nodes with src attributes (e.g., <Image src="..." />)
368
+ // This captures image component usages where the actual src is defined
369
+ if (node.type === 'component') {
370
+ const compNode = node as ComponentNode
371
+ for (const attr of compNode.attributes) {
372
+ if (attr.type === 'attribute' && attr.name === 'src' && attr.value) {
373
+ const srcLine = attr.position?.start.line ?? compNode.position?.start.line ?? 0
374
+ const snippet = extractImageSnippet(cached.lines, srcLine - 1)
375
+ addToImageSearchIndex({
376
+ file: relFile,
377
+ line: srcLine,
378
+ snippet,
379
+ src: attr.value,
380
+ })
381
+ }
382
+ }
383
+ }
384
+
385
+ if ('children' in node && Array.isArray(node.children)) {
386
+ for (const child of node.children) {
387
+ visit(child)
388
+ }
389
+ }
390
+ }
391
+ visit(cached.ast)
392
+ } else {
393
+ // For tsx/jsx, use regex
394
+ const srcPatterns = [/src="([^"]+)"/g, /src='([^']+)'/g]
395
+ for (let i = 0; i < cached.lines.length; i++) {
396
+ const line = cached.lines[i]
397
+ if (!line) continue
398
+
399
+ for (const pattern of srcPatterns) {
400
+ pattern.lastIndex = 0
401
+ let match: RegExpExecArray | null
402
+ while ((match = pattern.exec(line)) !== null) {
403
+ const snippet = extractImageSnippet(cached.lines, i)
404
+ addToImageSearchIndex({
405
+ file: relFile,
406
+ line: i + 1,
407
+ snippet,
408
+ src: match[1]!,
409
+ })
410
+ }
411
+ }
412
+ }
413
+ }
414
+ }
415
+
416
+ // ============================================================================
417
+ // Index Lookup
418
+ // ============================================================================
419
+
420
+ /**
421
+ * Fast text lookup using pre-built index
422
+ */
423
+ export function findInTextIndex(textContent: string, tag: string): SourceLocation | undefined {
424
+ const normalizedSearch = normalizeText(textContent)
425
+ const tagLower = tag.toLowerCase()
426
+ const index = getTextSearchIndex()
427
+
428
+ // First try exact match with same tag
429
+ for (const entry of index) {
430
+ if (entry.tag === tagLower && entry.normalizedText === normalizedSearch) {
431
+ return {
432
+ file: entry.file,
433
+ line: entry.line,
434
+ snippet: entry.snippet,
435
+ openingTagSnippet: entry.openingTagSnippet,
436
+ type: entry.type,
437
+ variableName: entry.variableName,
438
+ definitionLine: entry.definitionLine,
439
+ }
440
+ }
441
+ }
442
+
443
+ // Then try partial match for longer text
444
+ if (normalizedSearch.length > 10) {
445
+ const textPreview = normalizedSearch.slice(0, Math.min(30, normalizedSearch.length))
446
+ for (const entry of index) {
447
+ if (entry.tag === tagLower && entry.normalizedText.includes(textPreview)) {
448
+ return {
449
+ file: entry.file,
450
+ line: entry.line,
451
+ snippet: entry.snippet,
452
+ openingTagSnippet: entry.openingTagSnippet,
453
+ type: entry.type,
454
+ variableName: entry.variableName,
455
+ definitionLine: entry.definitionLine,
456
+ }
457
+ }
458
+ }
459
+ }
460
+
461
+ // Try any tag match
462
+ for (const entry of index) {
463
+ if (entry.normalizedText === normalizedSearch) {
464
+ return {
465
+ file: entry.file,
466
+ line: entry.line,
467
+ snippet: entry.snippet,
468
+ openingTagSnippet: entry.openingTagSnippet,
469
+ type: entry.type,
470
+ variableName: entry.variableName,
471
+ definitionLine: entry.definitionLine,
472
+ }
473
+ }
474
+ }
475
+
476
+ return undefined
477
+ }
478
+
479
+ /**
480
+ * Extract the pathname from a src value (handles both absolute URLs and relative paths)
481
+ */
482
+ function extractPathname(src: string): string {
483
+ try {
484
+ return new URL(src).pathname
485
+ } catch {
486
+ return (src.split('?')[0] ?? src)
487
+ }
488
+ }
489
+
490
+ /**
491
+ * Fast image lookup using pre-built index
492
+ */
493
+ export function findInImageIndex(imageSrc: string): SourceLocation | undefined {
494
+ const index = getImageSearchIndex()
495
+
496
+ // Exact match first
497
+ for (const entry of index) {
498
+ if (entry.src === imageSrc) {
499
+ return {
500
+ file: entry.file,
501
+ line: entry.line,
502
+ snippet: entry.snippet,
503
+ type: 'static',
504
+ }
505
+ }
506
+ }
507
+
508
+ // Fallback: path suffix matching for CDN-transformed URLs
509
+ // e.g., rendered src "/cdn-cgi/image/.../assets/photo.webp" should match
510
+ // authored src "https://cdn.nuasite.com/assets/photo.webp"
511
+ const targetPath = extractPathname(imageSrc)
512
+ for (const entry of index) {
513
+ const entryPath = extractPathname(entry.src)
514
+ if (entryPath.length > 5 && (targetPath.endsWith(entryPath) || entryPath.endsWith(targetPath))) {
515
+ return {
516
+ file: entry.file,
517
+ line: entry.line,
518
+ snippet: entry.snippet,
519
+ type: 'static',
520
+ }
521
+ }
522
+ }
523
+
524
+ return undefined
525
+ }