@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,335 @@
1
+ import fs from 'node:fs/promises'
2
+ import path from 'node:path'
3
+ import { isMap, isPair, isScalar, LineCounter, parseDocument } from 'yaml'
4
+
5
+ import { getProjectRoot } from '../config'
6
+ import { getMarkdownFileCache } from './cache'
7
+ import { normalizeText } from './snippet-utils'
8
+ import type { CollectionInfo, MarkdownContent, SourceLocation } from './types'
9
+
10
+ // ============================================================================
11
+ // Markdown File Cache
12
+ // ============================================================================
13
+
14
+ /**
15
+ * Get cached markdown file content
16
+ */
17
+ async function getCachedMarkdownFile(filePath: string): Promise<{ content: string; lines: string[] } | null> {
18
+ const cache = getMarkdownFileCache()
19
+ const cached = cache.get(filePath)
20
+ if (cached) return cached
21
+
22
+ try {
23
+ const content = await fs.readFile(filePath, 'utf-8')
24
+ const lines = content.split('\n')
25
+ const entry = { content, lines }
26
+ cache.set(filePath, entry)
27
+ return entry
28
+ } catch {
29
+ return null
30
+ }
31
+ }
32
+
33
+ // ============================================================================
34
+ // Collection Source Finding
35
+ // ============================================================================
36
+
37
+ /**
38
+ * Find markdown collection file for a given page path.
39
+ *
40
+ * Uses slug-based reverse lookup: scans all collection directories for a
41
+ * matching entry regardless of the URL prefix. This supports localized or
42
+ * renamed routes (e.g. `/aktuality/my-article` with content in `src/content/news/`).
43
+ *
44
+ * @param pagePath - The URL path of the page (e.g., '/services/3d-tisk')
45
+ * @param contentDir - The content directory (default: 'src/content')
46
+ * @returns Collection info if found, undefined otherwise
47
+ */
48
+ export async function findCollectionSource(
49
+ pagePath: string,
50
+ contentDir: string = 'src/content',
51
+ ): Promise<CollectionInfo | undefined> {
52
+ // Remove leading/trailing slashes
53
+ const cleanPath = pagePath.replace(/^\/+|\/+$/g, '')
54
+ const pathParts = cleanPath.split('/')
55
+
56
+ if (pathParts.length < 1 || (pathParts.length === 1 && !pathParts[0])) {
57
+ return undefined
58
+ }
59
+
60
+ const contentPath = path.join(getProjectRoot(), contentDir)
61
+
62
+ try {
63
+ await fs.access(contentPath)
64
+ } catch {
65
+ return undefined
66
+ }
67
+
68
+ // List all collection directories (skip _ and . prefixed)
69
+ let collectionDirs: string[]
70
+ try {
71
+ const entries = await fs.readdir(contentPath, { withFileTypes: true })
72
+ collectionDirs = entries
73
+ .filter(e => e.isDirectory() && !e.name.startsWith('_') && !e.name.startsWith('.'))
74
+ .map(e => e.name)
75
+ } catch {
76
+ return undefined
77
+ }
78
+
79
+ // Try progressively longer tail segments as slug candidates
80
+ // For /a/b/c try: "c", then "b/c"
81
+ for (let i = pathParts.length - 1; i >= 1; i--) {
82
+ const slug = pathParts.slice(i).join('/')
83
+ const matches: { name: string; file: string }[] = []
84
+
85
+ for (const dir of collectionDirs) {
86
+ const collectionPath = path.join(contentPath, dir)
87
+ const mdFile = await findMarkdownFile(collectionPath, slug)
88
+ if (mdFile) {
89
+ matches.push({ name: dir, file: mdFile })
90
+ }
91
+ }
92
+
93
+ if (matches.length === 1 && matches[0]) {
94
+ return {
95
+ name: matches[0].name,
96
+ slug,
97
+ file: path.relative(getProjectRoot(), matches[0].file),
98
+ }
99
+ }
100
+
101
+ if (matches.length > 1 && matches[0]) {
102
+ // Disambiguate: prefer collection whose name matches the URL prefix
103
+ const urlPrefix = pathParts[0]
104
+ const prefixMatch = matches.find(m => m.name === urlPrefix)
105
+ const chosen = prefixMatch || matches[0]
106
+
107
+ return {
108
+ name: chosen.name,
109
+ slug,
110
+ file: path.relative(getProjectRoot(), chosen.file),
111
+ }
112
+ }
113
+ }
114
+
115
+ return undefined
116
+ }
117
+
118
+ /**
119
+ * Find a markdown file in a collection directory by slug
120
+ */
121
+ async function findMarkdownFile(collectionPath: string, slug: string): Promise<string | undefined> {
122
+ // Try direct match: slug.md or slug.mdx
123
+ const directPaths = [
124
+ path.join(collectionPath, `${slug}.md`),
125
+ path.join(collectionPath, `${slug}.mdx`),
126
+ ]
127
+
128
+ for (const p of directPaths) {
129
+ try {
130
+ await fs.access(p)
131
+ return p
132
+ } catch {
133
+ // File doesn't exist, continue
134
+ }
135
+ }
136
+
137
+ // Try nested path for slugs with slashes
138
+ const slugParts = slug.split('/')
139
+ if (slugParts.length > 1) {
140
+ const nestedPath = path.join(collectionPath, ...slugParts.slice(0, -1))
141
+ const fileName = slugParts[slugParts.length - 1]
142
+ const nestedPaths = [
143
+ path.join(nestedPath, `${fileName}.md`),
144
+ path.join(nestedPath, `${fileName}.mdx`),
145
+ ]
146
+ for (const p of nestedPaths) {
147
+ try {
148
+ await fs.access(p)
149
+ return p
150
+ } catch {
151
+ // File doesn't exist, continue
152
+ }
153
+ }
154
+ }
155
+
156
+ // Try index file in slug directory
157
+ const indexPaths = [
158
+ path.join(collectionPath, slug, 'index.md'),
159
+ path.join(collectionPath, slug, 'index.mdx'),
160
+ ]
161
+
162
+ for (const p of indexPaths) {
163
+ try {
164
+ await fs.access(p)
165
+ return p
166
+ } catch {
167
+ // File doesn't exist, continue
168
+ }
169
+ }
170
+
171
+ return undefined
172
+ }
173
+
174
+ // ============================================================================
175
+ // Markdown Source Location Finding
176
+ // ============================================================================
177
+
178
+ /**
179
+ * Find text content in a markdown file and return source location
180
+ * Only matches frontmatter fields, not body content (body is handled separately as a whole)
181
+ * @param textContent - The text content to search for
182
+ * @param collectionInfo - Collection information (name, slug, file path)
183
+ * @returns Source location if found in frontmatter
184
+ */
185
+ export async function findMarkdownSourceLocation(
186
+ textContent: string,
187
+ collectionInfo: CollectionInfo,
188
+ ): Promise<SourceLocation | undefined> {
189
+ try {
190
+ const filePath = path.join(getProjectRoot(), collectionInfo.file)
191
+ const cached = await getCachedMarkdownFile(filePath)
192
+ if (!cached) return undefined
193
+
194
+ const { lines } = cached
195
+ const normalizedSearch = normalizeText(textContent)
196
+
197
+ // Parse frontmatter
198
+ let frontmatterEnd = -1
199
+ let inFrontmatter = false
200
+
201
+ for (let i = 0; i < lines.length; i++) {
202
+ const line = lines[i]?.trim()
203
+ if (line === '---') {
204
+ if (!inFrontmatter) {
205
+ inFrontmatter = true
206
+ } else {
207
+ frontmatterEnd = i
208
+ break
209
+ }
210
+ }
211
+ }
212
+
213
+ // Search in frontmatter only (for title, subtitle, etc.)
214
+ if (frontmatterEnd > 0) {
215
+ for (let i = 1; i < frontmatterEnd; i++) {
216
+ const line = lines[i]
217
+ if (!line) continue
218
+
219
+ // Extract value from YAML key: value (keys can contain hyphens)
220
+ const match = line.match(/^\s*([\w-]+):\s*(.+)$/)
221
+ if (match) {
222
+ const key = match[1]
223
+ let value = match[2]?.trim() || ''
224
+
225
+ // Handle quoted strings
226
+ if (
227
+ (value.startsWith('"') && value.endsWith('"'))
228
+ || (value.startsWith("'") && value.endsWith("'"))
229
+ ) {
230
+ value = value.slice(1, -1)
231
+ }
232
+
233
+ if (normalizeText(value) === normalizedSearch) {
234
+ return {
235
+ file: collectionInfo.file,
236
+ line: i + 1,
237
+ snippet: line,
238
+ type: 'collection',
239
+ variableName: key,
240
+ collectionName: collectionInfo.name,
241
+ collectionSlug: collectionInfo.slug,
242
+ }
243
+ }
244
+ }
245
+ }
246
+ }
247
+
248
+ // Body content is not searched line-by-line anymore
249
+ // Use parseMarkdownContent to get the full body as one entry
250
+ } catch {
251
+ // Error reading file
252
+ }
253
+
254
+ return undefined
255
+ }
256
+
257
+ // ============================================================================
258
+ // Markdown Content Parsing
259
+ // ============================================================================
260
+
261
+ /**
262
+ * Parse markdown file and extract frontmatter fields and full body content.
263
+ * Uses caching for better performance.
264
+ * @param collectionInfo - Collection information (name, slug, file path)
265
+ * @returns Parsed markdown content with frontmatter and body
266
+ */
267
+ export async function parseMarkdownContent(
268
+ collectionInfo: CollectionInfo,
269
+ ): Promise<MarkdownContent | undefined> {
270
+ try {
271
+ const filePath = path.join(getProjectRoot(), collectionInfo.file)
272
+ const cached = await getCachedMarkdownFile(filePath)
273
+ if (!cached) return undefined
274
+
275
+ const { lines } = cached
276
+
277
+ // Parse frontmatter
278
+ let frontmatterStart = -1
279
+ let frontmatterEnd = -1
280
+
281
+ for (let i = 0; i < lines.length; i++) {
282
+ const line = lines[i]?.trim()
283
+ if (line === '---') {
284
+ if (frontmatterStart === -1) {
285
+ frontmatterStart = i
286
+ } else {
287
+ frontmatterEnd = i
288
+ break
289
+ }
290
+ }
291
+ }
292
+
293
+ const frontmatter: Record<string, { value: string; line: number }> = {}
294
+
295
+ // Extract frontmatter fields using yaml parser
296
+ if (frontmatterEnd > 0) {
297
+ const yamlStr = lines.slice(frontmatterStart + 1, frontmatterEnd).join('\n')
298
+ const lineCounter = new LineCounter()
299
+ const doc = parseDocument(yamlStr, { lineCounter })
300
+
301
+ if (isMap(doc.contents)) {
302
+ for (const pair of doc.contents.items) {
303
+ if (isPair(pair) && isScalar(pair.key)) {
304
+ const key = String(pair.key.value)
305
+ const value = isScalar(pair.value) ? String(pair.value.value) : ''
306
+ const keyRange = (pair.key as any).range
307
+ const yamlLine = keyRange ? lineCounter.linePos(keyRange[0]).line : 0
308
+ const fileLine = yamlLine + frontmatterStart + 1
309
+ if (key && value) {
310
+ frontmatter[key] = { value, line: fileLine }
311
+ }
312
+ }
313
+ }
314
+ }
315
+ }
316
+
317
+ // Extract body (everything after frontmatter)
318
+ const bodyStartLine = frontmatterEnd > 0 ? frontmatterEnd + 1 : 0
319
+ const bodyLines = lines.slice(bodyStartLine)
320
+ const body = bodyLines.join('\n').trim()
321
+
322
+ return {
323
+ frontmatter,
324
+ body,
325
+ bodyStartLine: bodyStartLine + 1, // 1-indexed
326
+ file: collectionInfo.file,
327
+ collectionName: collectionInfo.name,
328
+ collectionSlug: collectionInfo.slug,
329
+ }
330
+ } catch {
331
+ // Error reading file
332
+ }
333
+
334
+ return undefined
335
+ }