import { createFileLocator, hasWildcard, isMediaPath, normalizePath, parseStoragePath } from "./path-utils.js"; const ATTRIBUTE_PATTERNS = [ /\b(?:src|href|poster)\s*=\s*["']([^"'<>]+)["']/gi, /url\(\s*["']?([^"')]+)["']?\s*\)/gi ]; export function collectStringCandidates(value, visit, path = []) { if (typeof value === "string") { visit(value, path); return; } if (Array.isArray(value)) { for (const [index, entry] of value.entries()) collectStringCandidates(entry, visit, [...path, index]); return; } if ((value !== null) && (typeof value === "object")) { for (const [key, entry] of Object.entries(value)) collectStringCandidates(entry, visit, [...path, key]); } } export function extractReferencesFromValue(value, source) { const references = []; collectStringCandidates(value, (candidate, candidatePath) => { const direct = resolveReference(candidate, source); if (direct && isMediaPath(direct.path)) { references.push({ sourceType: source.sourceType, sourceScope: source.sourceScope, sourceLabel: source.sourceLabel, sourceName: source.sourceName, sourceUuid: source.sourceUuid, sourceTrail: source.resolveSourceTrail?.(candidatePath) ?? null, rawValue: candidate, normalized: { ...direct, targetKind: hasWildcard(direct.path) ? "wildcard" : "local-file" } }); } for (const pattern of ATTRIBUTE_PATTERNS) { pattern.lastIndex = 0; let match; while ((match = pattern.exec(candidate))) { const nested = resolveReference(match[1], source); if (!nested || !isMediaPath(nested.path)) continue; references.push({ sourceType: source.sourceType, sourceScope: source.sourceScope, sourceLabel: source.sourceLabel, sourceName: source.sourceName, sourceUuid: source.sourceUuid, sourceTrail: source.resolveSourceTrail?.(candidatePath) ?? null, rawValue: match[1], normalized: { ...nested, targetKind: hasWildcard(nested.path) ? "wildcard" : "local-file" } }); } } }); return dedupeReferences(references); } function resolveReference(rawValue, source) { const direct = parseStoragePath(rawValue); if (direct) return direct; return resolvePackageRelativeReference(rawValue, source); } function resolvePackageRelativeReference(rawValue, source) { if (typeof rawValue !== "string") return null; if (/^(?:https?:|data:|blob:)/i.test(rawValue.trim())) return null; const path = normalizePath(rawValue); if (!path || !path.includes("/")) return null; const ownerType = source.sourceScope?.ownerType; const ownerId = source.sourceScope?.ownerId; if (!ownerType || !ownerId) return null; if (ownerType === "module") { return createFileLocator("data", `modules/${ownerId}/${path}`); } if (ownerType === "system") { return createFileLocator("data", `systems/${ownerId}/${path}`); } if (ownerType === "world") { return createFileLocator("data", path); } return null; } function dedupeReferences(references) { const seen = new Set(); return references.filter(reference => { const trailKey = Array.isArray(reference.sourceTrail) ? reference.sourceTrail.map(node => node.uuid ?? node.label ?? "").join(">") : ""; const key = [ reference.sourceType, reference.sourceLabel, trailKey, reference.normalized?.locator ?? "", reference.rawValue ].join("|"); if (seen.has(key)) return false; seen.add(key); return true; }); }