@nuasite/cms 0.46.0 → 0.46.2
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.
- package/dist/editor.js +1 -1
- package/package.json +7 -7
- package/src/source-finder/cache.ts +12 -0
- package/src/source-finder/collection-finder.ts +231 -79
package/dist/editor.js
CHANGED
|
@@ -386,7 +386,7 @@ function IC(t, e) {
|
|
|
386
386
|
function _C(t, e) {
|
|
387
387
|
return typeof e == "function" ? e(t) : e;
|
|
388
388
|
}
|
|
389
|
-
const u5 = "0.46.
|
|
389
|
+
const u5 = "0.46.2", h5 = u5, ct = {
|
|
390
390
|
/** Highlight overlay for hovered elements */
|
|
391
391
|
HIGHLIGHT: 2147483644,
|
|
392
392
|
/** Hover outline for elements/components */
|
package/package.json
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"directory": "packages/astro-cms"
|
|
15
15
|
},
|
|
16
16
|
"license": "Apache-2.0",
|
|
17
|
-
"version": "0.46.
|
|
17
|
+
"version": "0.46.2",
|
|
18
18
|
"module": "src/index.ts",
|
|
19
19
|
"types": "src/index.ts",
|
|
20
20
|
"type": "module",
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
}
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@nuasite/cms-core": "0.46.
|
|
30
|
-
"@nuasite/cms-types": "0.46.
|
|
29
|
+
"@nuasite/cms-core": "0.46.2",
|
|
30
|
+
"@nuasite/cms-types": "0.46.2",
|
|
31
31
|
"@astrojs/compiler": "^3.0.1",
|
|
32
32
|
"@babel/parser": "^7.29.2",
|
|
33
33
|
"node-html-parser": "^7.1.0",
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
"yaml": "^2.8.3"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@nuasite/cms-sidecar": "0.46.
|
|
39
|
-
"@nuasite/collections-admin": "0.46.
|
|
38
|
+
"@nuasite/cms-sidecar": "0.46.2",
|
|
39
|
+
"@nuasite/collections-admin": "0.46.2",
|
|
40
40
|
"@babel/types": "^7.29.0",
|
|
41
41
|
"@types/react": "^19.2.7",
|
|
42
42
|
"@types/react-dom": "^19.2.3",
|
|
@@ -76,8 +76,8 @@
|
|
|
76
76
|
"typescript": "^6.0.2",
|
|
77
77
|
"vite": "^7.0.0",
|
|
78
78
|
"@aws-sdk/client-s3": "^3.0.0",
|
|
79
|
-
"@nuasite/cms-sidecar": "0.46.
|
|
80
|
-
"@nuasite/collections-admin": "0.46.
|
|
79
|
+
"@nuasite/cms-sidecar": "0.46.2",
|
|
80
|
+
"@nuasite/collections-admin": "0.46.2",
|
|
81
81
|
"react": "^19.0.0",
|
|
82
82
|
"react-dom": "^19.0.0"
|
|
83
83
|
},
|
|
@@ -21,6 +21,9 @@ let searchIndexInitialized = false
|
|
|
21
21
|
/** Pre-built reverse index: normalizedText → SourceLocation[] (collection data files) */
|
|
22
22
|
let collectionTextIndex: Map<string, SourceLocation[]> | null = null
|
|
23
23
|
|
|
24
|
+
/** Per-collection-directory index: declared page URL → file abs path — used by the same-slug URL-disambiguation fallback */
|
|
25
|
+
const declaredUrlIndexCache = new Map<string, Map<string, string>>()
|
|
26
|
+
|
|
24
27
|
/** Lazy reverse index on i18n entries: translationKey → SearchIndexEntry[]. Rebuilt on demand after any mutation. */
|
|
25
28
|
let translationKeyIndex: Map<string, SearchIndexEntry[]> | null = null
|
|
26
29
|
|
|
@@ -93,6 +96,10 @@ export function setCollectionTextIndex(index: Map<string, SourceLocation[]> | nu
|
|
|
93
96
|
collectionTextIndex = index
|
|
94
97
|
}
|
|
95
98
|
|
|
99
|
+
export function getDeclaredUrlIndexCache(): Map<string, Map<string, string>> {
|
|
100
|
+
return declaredUrlIndexCache
|
|
101
|
+
}
|
|
102
|
+
|
|
96
103
|
// ============================================================================
|
|
97
104
|
// Dirty File Tracking (incremental re-indexing)
|
|
98
105
|
// ============================================================================
|
|
@@ -106,6 +113,10 @@ export function markFileDirty(absPath: string): void {
|
|
|
106
113
|
dirtyFiles.add(absPath)
|
|
107
114
|
// Also evict the parsed file cache so it's re-read from disk
|
|
108
115
|
parsedFileCache.delete(absPath)
|
|
116
|
+
// A changed file may add/remove/alter a declared URL anywhere in its
|
|
117
|
+
// collection directory — cheaper to drop the whole cache than track
|
|
118
|
+
// per-directory membership for a rarely-hit index.
|
|
119
|
+
declaredUrlIndexCache.clear()
|
|
109
120
|
}
|
|
110
121
|
|
|
111
122
|
export function getDirtyFiles(): Set<string> {
|
|
@@ -155,4 +166,5 @@ export function clearSourceFinderCache(): void {
|
|
|
155
166
|
searchIndexInitialized = false
|
|
156
167
|
collectionTextIndex = null
|
|
157
168
|
translationKeyIndex = null
|
|
169
|
+
declaredUrlIndexCache.clear()
|
|
158
170
|
}
|
|
@@ -4,7 +4,7 @@ import { isMap, isPair, isScalar, isSeq, LineCounter, parseDocument } from 'yaml
|
|
|
4
4
|
|
|
5
5
|
import { getProjectRoot } from '../config'
|
|
6
6
|
import type { CollectionDefinition } from '../types'
|
|
7
|
-
import { getCollectionTextIndex, getMarkdownFileCache, setCollectionTextIndex } from './cache'
|
|
7
|
+
import { getCollectionTextIndex, getDeclaredUrlIndexCache, getMarkdownFileCache, setCollectionTextIndex } from './cache'
|
|
8
8
|
import { normalizeText } from './snippet-utils'
|
|
9
9
|
import type { CollectionInfo, MarkdownContent, SourceLocation } from './types'
|
|
10
10
|
|
|
@@ -52,20 +52,10 @@ async function doBuildCollectionTextIndex(
|
|
|
52
52
|
} else {
|
|
53
53
|
// Markdown — index scalars from frontmatter only
|
|
54
54
|
const { lines } = cached
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (fmStart === -1) fmStart = i
|
|
60
|
-
else {
|
|
61
|
-
fmEnd = i
|
|
62
|
-
break
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
if (fmEnd > 0) {
|
|
67
|
-
const yamlStr = lines.slice(fmStart + 1, fmEnd).join('\n')
|
|
68
|
-
collectScalarsFromYaml(yamlStr, fmStart + 1, lines, info, index)
|
|
55
|
+
const bounds = findFrontmatterBounds(lines)
|
|
56
|
+
if (bounds) {
|
|
57
|
+
const yamlStr = lines.slice(bounds.start + 1, bounds.end).join('\n')
|
|
58
|
+
collectScalarsFromYaml(yamlStr, bounds.start + 1, lines, info, index)
|
|
69
59
|
}
|
|
70
60
|
}
|
|
71
61
|
} catch {
|
|
@@ -187,6 +177,22 @@ export function lookupCollectionText(
|
|
|
187
177
|
// Markdown File Cache
|
|
188
178
|
// ============================================================================
|
|
189
179
|
|
|
180
|
+
/**
|
|
181
|
+
* Locate the `---`-delimited frontmatter block in a markdown file's lines.
|
|
182
|
+
* Returns the indexes of the opening and closing `---` lines, or undefined if
|
|
183
|
+
* the file has no closed frontmatter block.
|
|
184
|
+
*/
|
|
185
|
+
function findFrontmatterBounds(lines: string[]): { start: number; end: number } | undefined {
|
|
186
|
+
let start = -1
|
|
187
|
+
for (let i = 0; i < lines.length; i++) {
|
|
188
|
+
if (lines[i]?.trim() === '---') {
|
|
189
|
+
if (start === -1) start = i
|
|
190
|
+
else return { start, end: i }
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return undefined
|
|
194
|
+
}
|
|
195
|
+
|
|
190
196
|
/**
|
|
191
197
|
* Get cached markdown file content
|
|
192
198
|
*/
|
|
@@ -210,6 +216,17 @@ async function getCachedMarkdownFile(filePath: string): Promise<{ content: strin
|
|
|
210
216
|
// Collection Source Finding
|
|
211
217
|
// ============================================================================
|
|
212
218
|
|
|
219
|
+
/**
|
|
220
|
+
* Frontmatter fields, in preference order, that may declare an entry's own
|
|
221
|
+
* canonical page URL. Only site-absolute values (starting with `/`) are trusted
|
|
222
|
+
* — external `url: https://…` values and bare slugs are ignored. Deliberately
|
|
223
|
+
* excludes `canonical`/`canonicalUrl`: by SEO convention those declare the URL
|
|
224
|
+
* that should be indexed *instead of* the current page (duplicate-content
|
|
225
|
+
* consolidation), which can point at a different entry entirely — trusting it
|
|
226
|
+
* as self-identity could resolve an edit to the wrong file.
|
|
227
|
+
*/
|
|
228
|
+
const DECLARED_URL_FIELDS = ['urlpath', 'permalink', 'pathname', 'route', 'url']
|
|
229
|
+
|
|
213
230
|
/**
|
|
214
231
|
* Find markdown collection file for a given page path.
|
|
215
232
|
*
|
|
@@ -217,6 +234,14 @@ async function getCachedMarkdownFile(filePath: string): Promise<{ content: strin
|
|
|
217
234
|
* matching entry regardless of the URL prefix. This supports localized or
|
|
218
235
|
* renamed routes (e.g. `/aktuality/my-article` with content in `src/content/news/`).
|
|
219
236
|
*
|
|
237
|
+
* Filename matching alone cannot tell apart two entries that share a slug but
|
|
238
|
+
* live under different URL prefixes (e.g. the same article slug published under
|
|
239
|
+
* two topic prefixes, where one file carries a disambiguating filename suffix).
|
|
240
|
+
* When a filename match declares a canonical URL in its frontmatter that
|
|
241
|
+
* contradicts the requested path, we fall back to matching entries by that
|
|
242
|
+
* declared URL. Projects whose entries declare no URL field keep the exact
|
|
243
|
+
* previous (filename-only) behavior.
|
|
244
|
+
*
|
|
220
245
|
* @param pagePath - The URL path of the page (e.g., '/services/3d-tisk')
|
|
221
246
|
* @param contentDir - The content directory (default: 'src/content')
|
|
222
247
|
* @returns Collection info if found, undefined otherwise
|
|
@@ -233,6 +258,7 @@ export async function findCollectionSource(
|
|
|
233
258
|
return undefined
|
|
234
259
|
}
|
|
235
260
|
|
|
261
|
+
const requestedUrl = normalizeSitePath(`/${cleanPath}`)
|
|
236
262
|
const contentPath = path.join(getProjectRoot(), contentDir)
|
|
237
263
|
|
|
238
264
|
try {
|
|
@@ -245,9 +271,12 @@ export async function findCollectionSource(
|
|
|
245
271
|
let collectionDirs: string[]
|
|
246
272
|
try {
|
|
247
273
|
const entries = await fs.readdir(contentPath, { withFileTypes: true })
|
|
274
|
+
// Sorted so match/resolution order is deterministic across runs and
|
|
275
|
+
// platforms, not dependent on readdir's unspecified enumeration order.
|
|
248
276
|
collectionDirs = entries
|
|
249
277
|
.filter(e => e.isDirectory() && !e.name.startsWith('_') && !e.name.startsWith('.'))
|
|
250
278
|
.map(e => e.name)
|
|
279
|
+
.sort()
|
|
251
280
|
} catch {
|
|
252
281
|
return undefined
|
|
253
282
|
}
|
|
@@ -266,6 +295,20 @@ export async function findCollectionSource(
|
|
|
266
295
|
}
|
|
267
296
|
}
|
|
268
297
|
|
|
298
|
+
if (matches.length === 0) continue
|
|
299
|
+
|
|
300
|
+
// Prefer the entry whose declared canonical URL equals the requested
|
|
301
|
+
// path. Only kicks in when an entry actually declares a URL, so
|
|
302
|
+
// URL-less projects fall through to the filename logic unchanged.
|
|
303
|
+
const byUrl = await resolveByDeclaredUrl(matches, requestedUrl, contentPath)
|
|
304
|
+
if (byUrl) {
|
|
305
|
+
// byUrl.file may differ from the file the filename match found
|
|
306
|
+
// (that's the whole point of this fallback) — its slug must be
|
|
307
|
+
// derived from the actual resolved file, not the URL-tail slug
|
|
308
|
+
// candidate, or downstream collectionSlug lookups break.
|
|
309
|
+
return { name: byUrl.name, slug: slugFromFilePath(byUrl.file), file: path.relative(getProjectRoot(), byUrl.file) }
|
|
310
|
+
}
|
|
311
|
+
|
|
269
312
|
if (matches.length === 1 && matches[0]) {
|
|
270
313
|
return {
|
|
271
314
|
name: matches[0].name,
|
|
@@ -291,6 +334,162 @@ export async function findCollectionSource(
|
|
|
291
334
|
return undefined
|
|
292
335
|
}
|
|
293
336
|
|
|
337
|
+
/** Normalize a site-absolute path: ensure a leading slash, drop query/hash and any trailing slash. */
|
|
338
|
+
function normalizeSitePath(p: string): string {
|
|
339
|
+
let s = p.split('?')[0]?.split('#')[0] ?? p
|
|
340
|
+
if (!s.startsWith('/')) s = `/${s}`
|
|
341
|
+
if (s.length > 1 && s.endsWith('/')) s = s.slice(0, -1)
|
|
342
|
+
return s
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Derive a collection entry's slug from its file path, matching the same
|
|
347
|
+
* convention collection-scanner.ts uses: flat `<slug>.md(x)` files use the
|
|
348
|
+
* basename minus extension; Hugo-style `<slug>/index.md(x)` files use the
|
|
349
|
+
* parent directory name.
|
|
350
|
+
*/
|
|
351
|
+
function slugFromFilePath(fileAbsPath: string): string {
|
|
352
|
+
const base = path.basename(fileAbsPath)
|
|
353
|
+
if (base === 'index.md' || base === 'index.mdx') {
|
|
354
|
+
return path.basename(path.dirname(fileAbsPath))
|
|
355
|
+
}
|
|
356
|
+
return base.replace(/\.mdx?$/, '')
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Read an entry's declared canonical page URL from its frontmatter, if any.
|
|
361
|
+
* Returns the normalized site-absolute path, or undefined when the file has no
|
|
362
|
+
* frontmatter or declares no site-absolute URL field.
|
|
363
|
+
*/
|
|
364
|
+
async function readDeclaredPageUrl(fileAbsPath: string): Promise<string | undefined> {
|
|
365
|
+
const cached = await getCachedMarkdownFile(fileAbsPath)
|
|
366
|
+
if (!cached) return undefined
|
|
367
|
+
|
|
368
|
+
const bounds = findFrontmatterBounds(cached.lines)
|
|
369
|
+
if (!bounds) return undefined
|
|
370
|
+
|
|
371
|
+
let doc
|
|
372
|
+
try {
|
|
373
|
+
doc = parseDocument(cached.lines.slice(bounds.start + 1, bounds.end).join('\n'))
|
|
374
|
+
} catch {
|
|
375
|
+
return undefined
|
|
376
|
+
}
|
|
377
|
+
if (!isMap(doc.contents)) return undefined
|
|
378
|
+
|
|
379
|
+
const found: Record<string, string> = {}
|
|
380
|
+
for (const pair of doc.contents.items) {
|
|
381
|
+
if (!isPair(pair) || !isScalar(pair.key) || !isScalar(pair.value)) continue
|
|
382
|
+
const key = String(pair.key.value).toLowerCase()
|
|
383
|
+
if (!DECLARED_URL_FIELDS.includes(key)) continue
|
|
384
|
+
const val = pair.value.value
|
|
385
|
+
if (typeof val === 'string' && val.startsWith('/')) {
|
|
386
|
+
found[key] ??= normalizeSitePath(val)
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
for (const field of DECLARED_URL_FIELDS) {
|
|
391
|
+
if (found[field]) return found[field]
|
|
392
|
+
}
|
|
393
|
+
return undefined
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Resolve the correct entry for `requestedUrl` using declared canonical URLs.
|
|
398
|
+
*
|
|
399
|
+
* 1. If a filename candidate declares exactly `requestedUrl`, use it.
|
|
400
|
+
* 2. Otherwise, if any candidate declares *some* URL (so the collection is
|
|
401
|
+
* URL-aware) but none matches, the filename match is for a same-slug sibling
|
|
402
|
+
* under a different prefix — scan the candidate collection(s) for the file
|
|
403
|
+
* whose declared URL is `requestedUrl`.
|
|
404
|
+
* 3. If no candidate declares any URL, return undefined so the caller keeps the
|
|
405
|
+
* legacy filename behavior.
|
|
406
|
+
*/
|
|
407
|
+
async function resolveByDeclaredUrl(
|
|
408
|
+
matches: { name: string; file: string }[],
|
|
409
|
+
requestedUrl: string,
|
|
410
|
+
contentPath: string,
|
|
411
|
+
): Promise<{ name: string; file: string } | undefined> {
|
|
412
|
+
let sawDeclaredUrl = false
|
|
413
|
+
for (const m of matches) {
|
|
414
|
+
const declared = await readDeclaredPageUrl(m.file)
|
|
415
|
+
if (declared === undefined) continue
|
|
416
|
+
sawDeclaredUrl = true
|
|
417
|
+
if (declared === requestedUrl) return m
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (!sawDeclaredUrl) return undefined
|
|
421
|
+
|
|
422
|
+
// Contradiction: the right entry is named differently from its slug. Scan
|
|
423
|
+
// the collection(s) that produced filename matches for a declared-URL hit.
|
|
424
|
+
// `matches` (and thus this Set) is built by iterating the sorted
|
|
425
|
+
// `collectionDirs`, so directory order here is deterministic.
|
|
426
|
+
for (const dir of new Set(matches.map(m => m.name))) {
|
|
427
|
+
const hit = await findFileByDeclaredUrl(path.join(contentPath, dir), requestedUrl)
|
|
428
|
+
if (hit) return { name: dir, file: hit }
|
|
429
|
+
}
|
|
430
|
+
return undefined
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Find the file in a collection directory whose declared canonical URL
|
|
435
|
+
* matches, via a per-directory URL→file index that's built once and cached
|
|
436
|
+
* (see `getDeclaredUrlIndexCache`) — only the first request for an ambiguous
|
|
437
|
+
* slug in a given directory pays for the full scan.
|
|
438
|
+
*/
|
|
439
|
+
async function findFileByDeclaredUrl(collectionPathAbs: string, requestedUrl: string): Promise<string | undefined> {
|
|
440
|
+
const cache = getDeclaredUrlIndexCache()
|
|
441
|
+
let index = cache.get(collectionPathAbs)
|
|
442
|
+
if (!index) {
|
|
443
|
+
index = await buildDeclaredUrlIndex(collectionPathAbs)
|
|
444
|
+
cache.set(collectionPathAbs, index)
|
|
445
|
+
}
|
|
446
|
+
return index.get(requestedUrl)
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Scan a collection directory (flat `*.md(x)` files and Hugo-style
|
|
451
|
+
* `<slug>/index.md(x)`) and index every entry by its declared canonical URL.
|
|
452
|
+
* Entries are visited in sorted order so that if two entries declare the same
|
|
453
|
+
* URL (a content bug), the winner is deterministic rather than readdir-order
|
|
454
|
+
* dependent.
|
|
455
|
+
*/
|
|
456
|
+
async function buildDeclaredUrlIndex(collectionPathAbs: string): Promise<Map<string, string>> {
|
|
457
|
+
const index = new Map<string, string>()
|
|
458
|
+
let dirEntries
|
|
459
|
+
try {
|
|
460
|
+
dirEntries = await fs.readdir(collectionPathAbs, { withFileTypes: true })
|
|
461
|
+
} catch {
|
|
462
|
+
return index
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const files = dirEntries
|
|
466
|
+
.filter(e => e.isFile() && /\.mdx?$/.test(e.name))
|
|
467
|
+
.map(e => e.name)
|
|
468
|
+
.sort()
|
|
469
|
+
for (const name of files) {
|
|
470
|
+
const file = path.join(collectionPathAbs, name)
|
|
471
|
+
const declared = await readDeclaredPageUrl(file)
|
|
472
|
+
if (declared && !index.has(declared)) index.set(declared, file)
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const subDirs = dirEntries
|
|
476
|
+
.filter(e => e.isDirectory() && !e.name.startsWith('_') && !e.name.startsWith('.'))
|
|
477
|
+
.map(e => e.name)
|
|
478
|
+
.sort()
|
|
479
|
+
for (const dir of subDirs) {
|
|
480
|
+
for (const idx of ['index.md', 'index.mdx']) {
|
|
481
|
+
const file = path.join(collectionPathAbs, dir, idx)
|
|
482
|
+
const declared = await readDeclaredPageUrl(file)
|
|
483
|
+
if (declared) {
|
|
484
|
+
if (!index.has(declared)) index.set(declared, file)
|
|
485
|
+
break
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return index
|
|
491
|
+
}
|
|
492
|
+
|
|
294
493
|
/**
|
|
295
494
|
* Find a markdown file in a collection directory by slug
|
|
296
495
|
*/
|
|
@@ -370,23 +569,11 @@ export async function findMarkdownSourceLocation(
|
|
|
370
569
|
const { lines } = cached
|
|
371
570
|
const normalizedSearch = normalizeText(textContent)
|
|
372
571
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
let frontmatterEnd = -1
|
|
376
|
-
for (let i = 0; i < lines.length; i++) {
|
|
377
|
-
if (lines[i]?.trim() === '---') {
|
|
378
|
-
if (frontmatterStart === -1) {
|
|
379
|
-
frontmatterStart = i
|
|
380
|
-
} else {
|
|
381
|
-
frontmatterEnd = i
|
|
382
|
-
break
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
if (frontmatterEnd <= 0) return undefined
|
|
572
|
+
const bounds = findFrontmatterBounds(lines)
|
|
573
|
+
if (!bounds) return undefined
|
|
387
574
|
|
|
388
|
-
const yamlStr = lines.slice(
|
|
389
|
-
const lineOffset =
|
|
575
|
+
const yamlStr = lines.slice(bounds.start + 1, bounds.end).join('\n')
|
|
576
|
+
const lineOffset = bounds.start + 1
|
|
390
577
|
return findScalarInYamlAst(yamlStr, lineOffset, normalizedSearch, lines, collectionInfo)
|
|
391
578
|
} catch {
|
|
392
579
|
// Error reading file
|
|
@@ -550,20 +737,10 @@ export async function findFieldInCollectionEntry(
|
|
|
550
737
|
|
|
551
738
|
// For markdown, search inside frontmatter only
|
|
552
739
|
const { lines } = cached
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
if (fmStart === -1) fmStart = i
|
|
558
|
-
else {
|
|
559
|
-
fmEnd = i
|
|
560
|
-
break
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
if (fmEnd <= 0) return undefined
|
|
565
|
-
const yamlStr = lines.slice(fmStart + 1, fmEnd).join('\n')
|
|
566
|
-
return findFieldByNameInYaml(yamlStr, fmStart + 1, fieldName, lines, info)
|
|
740
|
+
const bounds = findFrontmatterBounds(lines)
|
|
741
|
+
if (!bounds) return undefined
|
|
742
|
+
const yamlStr = lines.slice(bounds.start + 1, bounds.end).join('\n')
|
|
743
|
+
return findFieldByNameInYaml(yamlStr, bounds.start + 1, fieldName, lines, info)
|
|
567
744
|
} catch {
|
|
568
745
|
return undefined
|
|
569
746
|
}
|
|
@@ -598,20 +775,10 @@ export async function findFieldsInCollectionEntry(
|
|
|
598
775
|
|
|
599
776
|
// For markdown, search inside frontmatter only
|
|
600
777
|
const { lines } = cached
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
if (fmStart === -1) fmStart = i
|
|
606
|
-
else {
|
|
607
|
-
fmEnd = i
|
|
608
|
-
break
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
if (fmEnd <= 0) return new Map()
|
|
613
|
-
const yamlStr = lines.slice(fmStart + 1, fmEnd).join('\n')
|
|
614
|
-
return findFieldsByNameInYaml(yamlStr, fmStart + 1, fieldNames, lines, info)
|
|
778
|
+
const bounds = findFrontmatterBounds(lines)
|
|
779
|
+
if (!bounds) return new Map()
|
|
780
|
+
const yamlStr = lines.slice(bounds.start + 1, bounds.end).join('\n')
|
|
781
|
+
return findFieldsByNameInYaml(yamlStr, bounds.start + 1, fieldNames, lines, info)
|
|
615
782
|
} catch {
|
|
616
783
|
return new Map()
|
|
617
784
|
}
|
|
@@ -695,27 +862,12 @@ export async function parseMarkdownContent(
|
|
|
695
862
|
|
|
696
863
|
const { lines } = cached
|
|
697
864
|
|
|
698
|
-
|
|
699
|
-
let frontmatterStart = -1
|
|
700
|
-
let frontmatterEnd = -1
|
|
701
|
-
|
|
702
|
-
for (let i = 0; i < lines.length; i++) {
|
|
703
|
-
const line = lines[i]?.trim()
|
|
704
|
-
if (line === '---') {
|
|
705
|
-
if (frontmatterStart === -1) {
|
|
706
|
-
frontmatterStart = i
|
|
707
|
-
} else {
|
|
708
|
-
frontmatterEnd = i
|
|
709
|
-
break
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
|
|
865
|
+
const bounds = findFrontmatterBounds(lines)
|
|
714
866
|
const frontmatter: Record<string, { value: string; line: number }> = {}
|
|
715
867
|
|
|
716
868
|
// Extract frontmatter fields using yaml parser
|
|
717
|
-
if (
|
|
718
|
-
const yamlStr = lines.slice(
|
|
869
|
+
if (bounds) {
|
|
870
|
+
const yamlStr = lines.slice(bounds.start + 1, bounds.end).join('\n')
|
|
719
871
|
const lineCounter = new LineCounter()
|
|
720
872
|
const doc = parseDocument(yamlStr, { lineCounter })
|
|
721
873
|
|
|
@@ -726,7 +878,7 @@ export async function parseMarkdownContent(
|
|
|
726
878
|
const value = isScalar(pair.value) ? String(pair.value.value) : ''
|
|
727
879
|
const keyRange = (pair.key as any).range
|
|
728
880
|
const yamlLine = keyRange ? lineCounter.linePos(keyRange[0]).line : 0
|
|
729
|
-
const fileLine = yamlLine +
|
|
881
|
+
const fileLine = yamlLine + bounds.start + 1
|
|
730
882
|
if (key && value) {
|
|
731
883
|
frontmatter[key] = { value, line: fileLine }
|
|
732
884
|
}
|
|
@@ -736,7 +888,7 @@ export async function parseMarkdownContent(
|
|
|
736
888
|
}
|
|
737
889
|
|
|
738
890
|
// Extract body (everything after frontmatter)
|
|
739
|
-
const bodyStartLine =
|
|
891
|
+
const bodyStartLine = bounds ? bounds.end + 1 : 0
|
|
740
892
|
const bodyLines = lines.slice(bodyStartLine)
|
|
741
893
|
const body = bodyLines.join('\n').trim()
|
|
742
894
|
|