@nuasite/cms 0.39.0 → 0.39.1
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 +1 -1
- package/src/build-processor.ts +17 -3
- package/src/dev-middleware.ts +16 -3
- package/src/html-processor.ts +11 -0
- package/src/source-finder/image-finder.ts +9 -2
- package/src/source-finder/search-index.ts +104 -57
- package/src/source-finder/snippet-utils.ts +16 -3
- package/src/source-finder/source-lookup.ts +5 -1
- package/src/types.ts +4 -0
package/dist/editor.js
CHANGED
|
@@ -381,7 +381,7 @@ function WS(t, e) {
|
|
|
381
381
|
function KS(t, e) {
|
|
382
382
|
return typeof e == "function" ? e(t) : e;
|
|
383
383
|
}
|
|
384
|
-
const v3 = "0.39.
|
|
384
|
+
const v3 = "0.39.1", k3 = v3, nt = {
|
|
385
385
|
/** Highlight overlay for hovered elements */
|
|
386
386
|
HIGHLIGHT: 2147483644,
|
|
387
387
|
/** Hover outline for elements/components */
|
package/package.json
CHANGED
package/src/build-processor.ts
CHANGED
|
@@ -5,7 +5,7 @@ import path from 'node:path'
|
|
|
5
5
|
import { fileURLToPath } from 'node:url'
|
|
6
6
|
import { getProjectRoot } from './config'
|
|
7
7
|
import { detectArrayPattern, extractArrayElementProps } from './handlers/array-ops'
|
|
8
|
-
import { extractPropsFromSource, findComponentInvocationLine, findFrontmatterEnd } from './handlers/component-ops'
|
|
8
|
+
import { extractPropsFromSource, findComponentInvocationLine, findFrontmatterEnd, getPageFileCandidates } from './handlers/component-ops'
|
|
9
9
|
import { extractComponentName, processHtml } from './html-processor'
|
|
10
10
|
import type { ManifestWriter } from './manifest-writer'
|
|
11
11
|
import { generateComponentPreviews } from './preview-generator'
|
|
@@ -374,13 +374,27 @@ async function processFile(
|
|
|
374
374
|
}
|
|
375
375
|
}
|
|
376
376
|
|
|
377
|
+
const pageFiles = getPageFileCandidates(pagePath)
|
|
378
|
+
|
|
377
379
|
// Process entries in parallel for better performance
|
|
378
380
|
const entryLookups = Object.values(result.entries).map(async (entry) => {
|
|
379
381
|
// Handle image entries specially - always search by image src
|
|
380
382
|
// The sourcePath from HTML attributes may point to a shared Image component
|
|
381
383
|
// rather than the file that actually uses the component with the src value
|
|
382
384
|
if (entry.imageMetadata?.src) {
|
|
383
|
-
const
|
|
385
|
+
const preferredLocation = entry.sourcePath
|
|
386
|
+
? {
|
|
387
|
+
file: entry.sourcePath,
|
|
388
|
+
line: entry.sourceLine,
|
|
389
|
+
srcOccurrence: entry.imageMetadata.srcOccurrence,
|
|
390
|
+
}
|
|
391
|
+
: undefined
|
|
392
|
+
const imageSource = await findImageSourceLocation(
|
|
393
|
+
entry.imageMetadata.src,
|
|
394
|
+
entry.imageMetadata.srcSet,
|
|
395
|
+
pageFiles,
|
|
396
|
+
preferredLocation,
|
|
397
|
+
)
|
|
384
398
|
if (imageSource) {
|
|
385
399
|
entry.sourcePath = imageSource.file
|
|
386
400
|
entry.sourceLine = imageSource.line
|
|
@@ -412,7 +426,7 @@ async function processFile(
|
|
|
412
426
|
}
|
|
413
427
|
|
|
414
428
|
// Fall back to searching Astro files
|
|
415
|
-
const sourceLocation = await findSourceLocation(entry.text, entry.tag)
|
|
429
|
+
const sourceLocation = await findSourceLocation(entry.text, entry.tag, pageFiles)
|
|
416
430
|
if (sourceLocation) {
|
|
417
431
|
entry.sourcePath = sourceLocation.file
|
|
418
432
|
entry.sourceLine = sourceLocation.line
|
package/src/dev-middleware.ts
CHANGED
|
@@ -591,20 +591,33 @@ export async function enhanceManifestInBackground(
|
|
|
591
591
|
}
|
|
592
592
|
}
|
|
593
593
|
|
|
594
|
-
const
|
|
594
|
+
const pageFiles = getPageFileCandidates(pagePath)
|
|
595
|
+
const enhanced = await enhanceManifestWithSourceSnippets(entries, collectionDefinitions, pageFiles)
|
|
595
596
|
|
|
596
597
|
// Fallback for entries without sourcePath — search index can still find them
|
|
597
598
|
for (const entry of Object.values(enhanced)) {
|
|
598
599
|
if (entry.sourceSnippet || entry.sourcePath) continue
|
|
599
600
|
if (entry.imageMetadata?.src) {
|
|
600
|
-
const
|
|
601
|
+
const preferredLocation = entry.sourcePath
|
|
602
|
+
? {
|
|
603
|
+
file: entry.sourcePath,
|
|
604
|
+
line: entry.sourceLine,
|
|
605
|
+
srcOccurrence: entry.imageMetadata.srcOccurrence,
|
|
606
|
+
}
|
|
607
|
+
: undefined
|
|
608
|
+
const loc = await findImageSourceLocation(
|
|
609
|
+
entry.imageMetadata.src,
|
|
610
|
+
entry.imageMetadata.srcSet,
|
|
611
|
+
pageFiles,
|
|
612
|
+
preferredLocation,
|
|
613
|
+
)
|
|
601
614
|
if (loc) {
|
|
602
615
|
entry.sourcePath = loc.file
|
|
603
616
|
entry.sourceLine = loc.line
|
|
604
617
|
entry.sourceSnippet = loc.snippet
|
|
605
618
|
}
|
|
606
619
|
} else if (entry.text && entry.tag) {
|
|
607
|
-
const loc = await findSourceLocation(entry.text, entry.tag)
|
|
620
|
+
const loc = await findSourceLocation(entry.text, entry.tag, pageFiles)
|
|
608
621
|
if (loc) {
|
|
609
622
|
entry.sourcePath = loc.file
|
|
610
623
|
entry.sourceLine = loc.line
|
package/src/html-processor.ts
CHANGED
|
@@ -662,6 +662,12 @@ export async function processHtml(
|
|
|
662
662
|
sourceLine?: number
|
|
663
663
|
}
|
|
664
664
|
const imageEntries = new Map<string, ImageEntry>()
|
|
665
|
+
// Per (sourceFile, src) DOM-order occurrence counter. When the same image
|
|
666
|
+
// URL appears N times rendered from the same source file, this lets us
|
|
667
|
+
// map each rendered `<img>` to the Nth index entry for that (file, src)
|
|
668
|
+
// — disambiguating cases where data-astro-source-loc alone doesn't (e.g.
|
|
669
|
+
// when only a wrapper element carries the source attribution).
|
|
670
|
+
const srcOccurrenceCounts = new Map<string, number>()
|
|
665
671
|
root.querySelectorAll('img').forEach((node) => {
|
|
666
672
|
// Skip if already marked
|
|
667
673
|
if (node.getAttribute(attributeName)) return
|
|
@@ -682,12 +688,17 @@ export async function processHtml(
|
|
|
682
688
|
|
|
683
689
|
const { sourceFile, sourceLine } = findAncestorSourceLocation(node)
|
|
684
690
|
|
|
691
|
+
const occurrenceKey = `${sourceFile ?? ''}\0${src}`
|
|
692
|
+
const srcOccurrence = srcOccurrenceCounts.get(occurrenceKey) ?? 0
|
|
693
|
+
srcOccurrenceCounts.set(occurrenceKey, srcOccurrence + 1)
|
|
694
|
+
|
|
685
695
|
// Build image metadata
|
|
686
696
|
const metadata: ImageMetadata = {
|
|
687
697
|
src,
|
|
688
698
|
alt: node.getAttribute('alt') || '',
|
|
689
699
|
srcSet: node.getAttribute('srcset') || undefined,
|
|
690
700
|
sizes: node.getAttribute('sizes') || undefined,
|
|
701
|
+
srcOccurrence,
|
|
691
702
|
}
|
|
692
703
|
|
|
693
704
|
// Store image info for manifest
|
|
@@ -141,21 +141,28 @@ function parseSrcsetUrls(srcSet: string): string[] {
|
|
|
141
141
|
* Also checks srcset URLs as fallback when src doesn't match (e.g., when src
|
|
142
142
|
* is a local upload path but srcset contains CDN-transformed original URLs).
|
|
143
143
|
* Uses pre-built search index for fast lookups.
|
|
144
|
+
*
|
|
145
|
+
* `pageFiles` disambiguates the same image URL across pages.
|
|
146
|
+
* `preferredLocation` disambiguates the same image URL within one page —
|
|
147
|
+
* pass the manifest entry's Astro attribution and the lookup will return the
|
|
148
|
+
* matching per-occurrence index entry when one exists.
|
|
144
149
|
*/
|
|
145
150
|
export async function findImageSourceLocation(
|
|
146
151
|
imageSrc: string,
|
|
147
152
|
imageSrcSet?: string,
|
|
153
|
+
pageFiles?: readonly string[],
|
|
154
|
+
preferredLocation?: { file: string; line?: number; srcOccurrence?: number },
|
|
148
155
|
): Promise<SourceLocation | undefined> {
|
|
149
156
|
// Use index if available (much faster)
|
|
150
157
|
if (isSearchIndexInitialized()) {
|
|
151
|
-
const result = findInImageIndex(imageSrc)
|
|
158
|
+
const result = findInImageIndex(imageSrc, pageFiles, preferredLocation)
|
|
152
159
|
if (result) return result
|
|
153
160
|
|
|
154
161
|
// Fallback: try URLs extracted from srcset
|
|
155
162
|
if (imageSrcSet) {
|
|
156
163
|
const srcsetUrls = parseSrcsetUrls(imageSrcSet)
|
|
157
164
|
for (const url of srcsetUrls) {
|
|
158
|
-
const srcsetResult = findInImageIndex(url)
|
|
165
|
+
const srcsetResult = findInImageIndex(url, pageFiles, preferredLocation)
|
|
159
166
|
if (srcsetResult) return srcsetResult
|
|
160
167
|
}
|
|
161
168
|
}
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
setSearchIndexInitialized,
|
|
25
25
|
} from './cache'
|
|
26
26
|
import { extractAstroImageOriginalUrl, extractImageSnippet, extractInnerHtmlFromSnippet, normalizeText } from './snippet-utils'
|
|
27
|
-
import type { CachedParsedFile, SearchIndexEntry, SourceLocation } from './types'
|
|
27
|
+
import type { CachedParsedFile, ImageIndexEntry, SearchIndexEntry, SourceLocation } from './types'
|
|
28
28
|
|
|
29
29
|
/** Collection data files live under this path — used to prefer them over templates */
|
|
30
30
|
const CONTENT_DIR_PREFIX = 'src/content/'
|
|
@@ -1268,6 +1268,37 @@ function textEntryToLocation(entry: SearchIndexEntry): SourceLocation {
|
|
|
1268
1268
|
}
|
|
1269
1269
|
}
|
|
1270
1270
|
|
|
1271
|
+
/** Helper to build SourceLocation from an image index entry */
|
|
1272
|
+
function imageEntryToLocation(entry: ImageIndexEntry): SourceLocation {
|
|
1273
|
+
return {
|
|
1274
|
+
file: entry.file,
|
|
1275
|
+
line: entry.line,
|
|
1276
|
+
snippet: entry.snippet,
|
|
1277
|
+
type: 'static',
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
/**
|
|
1282
|
+
* Classify a candidate result by file priority and stash it into the right
|
|
1283
|
+
* bucket. Returns the result if it's a collection-file match (caller should
|
|
1284
|
+
* return immediately — collection data files are always authoritative).
|
|
1285
|
+
*/
|
|
1286
|
+
interface RankedMatches {
|
|
1287
|
+
page?: SourceLocation
|
|
1288
|
+
other?: SourceLocation
|
|
1289
|
+
}
|
|
1290
|
+
function rankAndStash(
|
|
1291
|
+
file: string,
|
|
1292
|
+
result: SourceLocation,
|
|
1293
|
+
pageFiles: readonly string[] | undefined,
|
|
1294
|
+
matches: RankedMatches,
|
|
1295
|
+
): SourceLocation | undefined {
|
|
1296
|
+
if (isCollectionFile(file)) return result
|
|
1297
|
+
if (pageFiles?.includes(file)) matches.page ??= result
|
|
1298
|
+
else matches.other ??= result
|
|
1299
|
+
return undefined
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1271
1302
|
/**
|
|
1272
1303
|
* Look up an i18n dictionary entry by its literal key (e.g. `nav.prague4`),
|
|
1273
1304
|
* preferring the match whose value equals `normalizedText` so the right locale
|
|
@@ -1325,55 +1356,56 @@ function toProjectRelativePath(file: string): string {
|
|
|
1325
1356
|
}
|
|
1326
1357
|
|
|
1327
1358
|
/**
|
|
1328
|
-
* Fast text lookup using pre-built index
|
|
1359
|
+
* Fast text lookup using pre-built index.
|
|
1360
|
+
*
|
|
1361
|
+
* Priority order: collection data files > entries in `pageFiles` > anything
|
|
1362
|
+
* else. Translation-tag (i18n dictionary) hits beat non-collection template
|
|
1363
|
+
* matches — a dictionary entry is an authoritative translatable signal
|
|
1364
|
+
* whereas a same-text template hit is often coincidental.
|
|
1329
1365
|
*/
|
|
1330
|
-
export function findInTextIndex(
|
|
1366
|
+
export function findInTextIndex(
|
|
1367
|
+
textContent: string,
|
|
1368
|
+
tag: string,
|
|
1369
|
+
pageFiles?: readonly string[],
|
|
1370
|
+
): SourceLocation | undefined {
|
|
1331
1371
|
const normalizedSearch = normalizeText(textContent)
|
|
1332
1372
|
const tagLower = tag.toLowerCase()
|
|
1333
1373
|
const index = getTextSearchIndex()
|
|
1334
|
-
|
|
1335
|
-
// Single pass for exact matches: collect the best same-tag template hit
|
|
1336
|
-
// *and* any i18n dictionary hit at once. A JSON dictionary entry is an
|
|
1337
|
-
// authoritative translatable signal, so it beats a non-collection
|
|
1338
|
-
// template match (which is often a coincidental same-text element).
|
|
1339
|
-
let bestMatch: SourceLocation | undefined
|
|
1374
|
+
const matches: RankedMatches = {}
|
|
1340
1375
|
let translationHit: SourceLocation | undefined
|
|
1376
|
+
|
|
1341
1377
|
for (const entry of index) {
|
|
1342
1378
|
if (entry.normalizedText !== normalizedSearch) continue
|
|
1343
1379
|
if (entry.tag === tagLower) {
|
|
1344
|
-
const
|
|
1345
|
-
if (
|
|
1346
|
-
bestMatch ??= result
|
|
1380
|
+
const collectionHit = rankAndStash(entry.file, textEntryToLocation(entry), pageFiles, matches)
|
|
1381
|
+
if (collectionHit) return collectionHit
|
|
1347
1382
|
} else if (entry.tag === TRANSLATION_TAG_MARKER) {
|
|
1348
1383
|
translationHit ??= textEntryToLocation(entry)
|
|
1349
1384
|
}
|
|
1350
1385
|
}
|
|
1351
1386
|
if (translationHit) return translationHit
|
|
1352
|
-
|
|
1387
|
+
const sameTag = matches.page ?? matches.other
|
|
1388
|
+
if (sameTag) return sameTag
|
|
1353
1389
|
|
|
1354
|
-
// Then try partial match for longer text — prefer collection data files
|
|
1355
1390
|
if (normalizedSearch.length > 10) {
|
|
1356
1391
|
const textPreview = normalizedSearch.slice(0, Math.min(30, normalizedSearch.length))
|
|
1357
1392
|
for (const entry of index) {
|
|
1358
|
-
if (entry.tag
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
}
|
|
1393
|
+
if (entry.tag !== tagLower) continue
|
|
1394
|
+
if (!entry.normalizedText.includes(textPreview)) continue
|
|
1395
|
+
const collectionHit = rankAndStash(entry.file, textEntryToLocation(entry), pageFiles, matches)
|
|
1396
|
+
if (collectionHit) return collectionHit
|
|
1363
1397
|
}
|
|
1364
|
-
|
|
1398
|
+
const partial = matches.page ?? matches.other
|
|
1399
|
+
if (partial) return partial
|
|
1365
1400
|
}
|
|
1366
1401
|
|
|
1367
|
-
// Try any tag match — prefer collection data files
|
|
1368
1402
|
for (const entry of index) {
|
|
1369
|
-
if (entry.normalizedText
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
bestMatch ??= result
|
|
1373
|
-
}
|
|
1403
|
+
if (entry.normalizedText !== normalizedSearch) continue
|
|
1404
|
+
const collectionHit = rankAndStash(entry.file, textEntryToLocation(entry), pageFiles, matches)
|
|
1405
|
+
if (collectionHit) return collectionHit
|
|
1374
1406
|
}
|
|
1375
1407
|
|
|
1376
|
-
return
|
|
1408
|
+
return matches.page ?? matches.other
|
|
1377
1409
|
}
|
|
1378
1410
|
|
|
1379
1411
|
/**
|
|
@@ -1388,34 +1420,57 @@ function extractPathname(src: string): string {
|
|
|
1388
1420
|
}
|
|
1389
1421
|
|
|
1390
1422
|
/**
|
|
1391
|
-
* Fast image lookup using pre-built index
|
|
1423
|
+
* Fast image lookup using pre-built index.
|
|
1424
|
+
*
|
|
1425
|
+
* Priority order:
|
|
1426
|
+
* 1. `preferredLocation` (file, srcOccurrence) — Nth index entry for
|
|
1427
|
+
* (src, file). Deterministic per-DOM-order; preferred over `line` because
|
|
1428
|
+
* Astro's `data-astro-source-loc` often points at a common ancestor and
|
|
1429
|
+
* collides for multiple imgs sharing the same src.
|
|
1430
|
+
* 2. `preferredLocation` (file, line) exact match — used when occurrence
|
|
1431
|
+
* isn't provided (Astro's stamping happens to be precise enough).
|
|
1432
|
+
* 3. Collection data files (always authoritative).
|
|
1433
|
+
* 4. Entries in `pageFiles` — disambiguates same image across multiple pages.
|
|
1434
|
+
* 5. Any other match.
|
|
1392
1435
|
*/
|
|
1393
|
-
export function findInImageIndex(
|
|
1436
|
+
export function findInImageIndex(
|
|
1437
|
+
imageSrc: string,
|
|
1438
|
+
pageFiles?: readonly string[],
|
|
1439
|
+
preferredLocation?: { file: string; line?: number; srcOccurrence?: number },
|
|
1440
|
+
): SourceLocation | undefined {
|
|
1394
1441
|
const index = getImageSearchIndex()
|
|
1395
1442
|
|
|
1396
1443
|
// Dev-mode optimized URLs (`/_image?href=...`, `/@image/...?f=...`) embed the source
|
|
1397
1444
|
// path; try both the raw URL and the decoded path so callers don't need to pre-decode.
|
|
1398
1445
|
const decoded = extractAstroImageOriginalUrl(imageSrc)
|
|
1399
1446
|
const candidates = decoded && decoded !== imageSrc ? [imageSrc, decoded] : [imageSrc]
|
|
1400
|
-
|
|
1401
|
-
//
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1447
|
+
// Astro stamps absolute paths in `data-astro-source-file`; the index uses
|
|
1448
|
+
// project-relative paths. Normalize before comparing.
|
|
1449
|
+
const preferredFile = preferredLocation ? toProjectRelativePath(preferredLocation.file) : undefined
|
|
1450
|
+
const preferredLine = preferredLocation?.line
|
|
1451
|
+
const preferredOccurrence = preferredLocation?.srcOccurrence
|
|
1452
|
+
|
|
1453
|
+
const matches: RankedMatches = {}
|
|
1454
|
+
let occurrenceCounter = 0
|
|
1455
|
+
let occurrenceMatch: SourceLocation | undefined
|
|
1456
|
+
let lineMatch: SourceLocation | undefined
|
|
1405
1457
|
for (const entry of index) {
|
|
1406
1458
|
if (!candidates.includes(entry.src)) continue
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
return result // Collection data file — always preferred
|
|
1459
|
+
if (preferredFile && entry.file === preferredFile) {
|
|
1460
|
+
if (preferredOccurrence !== undefined && occurrenceCounter++ === preferredOccurrence) {
|
|
1461
|
+
occurrenceMatch ??= imageEntryToLocation(entry)
|
|
1462
|
+
}
|
|
1463
|
+
if (preferredLine !== undefined && entry.line === preferredLine) {
|
|
1464
|
+
lineMatch ??= imageEntryToLocation(entry)
|
|
1465
|
+
}
|
|
1415
1466
|
}
|
|
1416
|
-
|
|
1467
|
+
const collectionHit = rankAndStash(entry.file, imageEntryToLocation(entry), pageFiles, matches)
|
|
1468
|
+
if (collectionHit) return collectionHit
|
|
1417
1469
|
}
|
|
1418
|
-
if (
|
|
1470
|
+
if (occurrenceMatch) return occurrenceMatch
|
|
1471
|
+
if (lineMatch) return lineMatch
|
|
1472
|
+
const exact = matches.page ?? matches.other
|
|
1473
|
+
if (exact) return exact
|
|
1419
1474
|
|
|
1420
1475
|
// Fallback: path suffix matching for CDN-transformed URLs
|
|
1421
1476
|
// e.g., rendered src "/cdn-cgi/image/.../assets/photo.webp" should match
|
|
@@ -1423,19 +1478,11 @@ export function findInImageIndex(imageSrc: string): SourceLocation | undefined {
|
|
|
1423
1478
|
const targetPath = extractPathname(imageSrc)
|
|
1424
1479
|
for (const entry of index) {
|
|
1425
1480
|
const entryPath = extractPathname(entry.src)
|
|
1426
|
-
if (entryPath.length
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
snippet: entry.snippet,
|
|
1431
|
-
type: 'static',
|
|
1432
|
-
}
|
|
1433
|
-
if (isCollectionFile(entry.file)) {
|
|
1434
|
-
return result
|
|
1435
|
-
}
|
|
1436
|
-
bestMatch ??= result
|
|
1437
|
-
}
|
|
1481
|
+
if (entryPath.length <= 5) continue
|
|
1482
|
+
if (!targetPath.endsWith(entryPath) && !entryPath.endsWith(targetPath)) continue
|
|
1483
|
+
const collectionHit = rankAndStash(entry.file, imageEntryToLocation(entry), pageFiles, matches)
|
|
1484
|
+
if (collectionHit) return collectionHit
|
|
1438
1485
|
}
|
|
1439
1486
|
|
|
1440
|
-
return
|
|
1487
|
+
return matches.page ?? matches.other
|
|
1441
1488
|
}
|
|
@@ -629,6 +629,7 @@ function resolveTranslationKeyFromSnippet(
|
|
|
629
629
|
export async function enhanceManifestWithSourceSnippets(
|
|
630
630
|
entries: Record<string, ManifestEntry>,
|
|
631
631
|
collectionDefinitions?: Record<string, CollectionDefinition>,
|
|
632
|
+
pageFiles?: readonly string[],
|
|
632
633
|
): Promise<Record<string, ManifestEntry>> {
|
|
633
634
|
// Ensure the search index is ready (returns immediately if already built,
|
|
634
635
|
// otherwise waits for the in-flight initialization or triggers a new one).
|
|
@@ -750,7 +751,19 @@ export async function enhanceManifestWithSourceSnippets(
|
|
|
750
751
|
}
|
|
751
752
|
|
|
752
753
|
// ── Non-collection images: find via search index / AST ──
|
|
753
|
-
const
|
|
754
|
+
const preferredLocation = entry.sourcePath
|
|
755
|
+
? {
|
|
756
|
+
file: entry.sourcePath,
|
|
757
|
+
line: entry.sourceLine,
|
|
758
|
+
srcOccurrence: entry.imageMetadata.srcOccurrence,
|
|
759
|
+
}
|
|
760
|
+
: undefined
|
|
761
|
+
const imageLocation = await findImageSourceLocation(
|
|
762
|
+
entry.imageMetadata.src,
|
|
763
|
+
entry.imageMetadata.srcSet,
|
|
764
|
+
pageFiles,
|
|
765
|
+
preferredLocation,
|
|
766
|
+
)
|
|
754
767
|
if (imageLocation) {
|
|
755
768
|
const sourceHash = generateSourceHash(imageLocation.snippet || entry.imageMetadata.src)
|
|
756
769
|
const updated: ManifestEntry = {
|
|
@@ -862,7 +875,7 @@ export async function enhanceManifestWithSourceSnippets(
|
|
|
862
875
|
? findVariableHitInFile(trimmedEntryText, entry.tag, entry.sourcePath)
|
|
863
876
|
: undefined
|
|
864
877
|
const noCoords = !entry.sourcePath || !entry.sourceLine
|
|
865
|
-
const winner = sameFileHit ?? (noCoords ? findInTextIndex(trimmedEntryText, entry.tag) : undefined)
|
|
878
|
+
const winner = sameFileHit ?? (noCoords ? findInTextIndex(trimmedEntryText, entry.tag, pageFiles) : undefined)
|
|
866
879
|
|
|
867
880
|
if (winner) {
|
|
868
881
|
if (isTranslationFilePath(winner.file)) {
|
|
@@ -1059,7 +1072,7 @@ export async function enhanceManifestWithSourceSnippets(
|
|
|
1059
1072
|
|
|
1060
1073
|
// Last resort — consult the text index (covers i18n JSON dictionaries
|
|
1061
1074
|
// and any other indexed text that shares no tag with the rendered element).
|
|
1062
|
-
const indexHit = findInTextIndex(trimmedText, entry.tag)
|
|
1075
|
+
const indexHit = findInTextIndex(trimmedText, entry.tag, pageFiles)
|
|
1063
1076
|
if (indexHit && indexHit.file !== entry.sourcePath) {
|
|
1064
1077
|
const resolved = await applyTranslationSource(entry, indexHit, attributes, colorClasses)
|
|
1065
1078
|
return [id, resolved] as const
|
|
@@ -17,14 +17,18 @@ import type { SourceLocation } from './types'
|
|
|
17
17
|
/**
|
|
18
18
|
* Find source file and line number for text content.
|
|
19
19
|
* Uses pre-built search index for fast lookups.
|
|
20
|
+
*
|
|
21
|
+
* `pageFiles` disambiguates when the same text appears on multiple pages —
|
|
22
|
+
* the lookup prefers index entries whose file is one of the candidates.
|
|
20
23
|
*/
|
|
21
24
|
export async function findSourceLocation(
|
|
22
25
|
textContent: string,
|
|
23
26
|
tag: string,
|
|
27
|
+
pageFiles?: readonly string[],
|
|
24
28
|
): Promise<SourceLocation | undefined> {
|
|
25
29
|
// Use index if available (much faster)
|
|
26
30
|
if (isSearchIndexInitialized()) {
|
|
27
|
-
const indexHit = findInTextIndex(textContent, tag)
|
|
31
|
+
const indexHit = findInTextIndex(textContent, tag, pageFiles)
|
|
28
32
|
if (indexHit) return indexHit
|
|
29
33
|
// Fall through to slow search on miss — covers cases the per-file
|
|
30
34
|
// indexer can't pre-emit, like a child component's `.map()` over a
|
package/src/types.ts
CHANGED
|
@@ -70,6 +70,10 @@ export interface ImageMetadata {
|
|
|
70
70
|
srcSet?: string
|
|
71
71
|
/** Image sizes attribute */
|
|
72
72
|
sizes?: string
|
|
73
|
+
/** 0-based DOM-order index of this `<img>` among same-(src, sourceFile)
|
|
74
|
+
* occurrences on the page. Disambiguates source location when the same
|
|
75
|
+
* image URL appears multiple times in the same source file. */
|
|
76
|
+
srcOccurrence?: number
|
|
73
77
|
}
|
|
74
78
|
|
|
75
79
|
/** Identifies the (collection, entry, field) destination for an editor upload. */
|