113 lines
3.6 KiB
JavaScript
113 lines
3.6 KiB
JavaScript
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;
|
|
});
|
|
}
|