Files
Genarrative/src/routing/routeImageReadyGateUtils.ts
T
kdletters cbc27bad4a
CI / verify (push) Has been cancelled
init with react+axum+spacetimedb
2026-04-26 18:06:23 +08:00

112 lines
2.9 KiB
TypeScript

const CSS_IMAGE_URL_PATTERN =
/url\(\s*(?:"([^"]+)"|'([^']+)'|([^'")]+))\s*\)/gu;
export type ImagePreloadResult = {
url: string;
status: 'loaded' | 'failed' | 'timeout';
};
export function extractCssImageUrls(value: string) {
const urls: string[] = [];
CSS_IMAGE_URL_PATTERN.lastIndex = 0;
let match = CSS_IMAGE_URL_PATTERN.exec(value);
while (match) {
const rawUrl = match[1] ?? match[2] ?? match[3] ?? '';
const normalizedRawUrl = rawUrl.trim();
if (normalizedRawUrl) {
urls.push(normalizedRawUrl);
}
match = CSS_IMAGE_URL_PATTERN.exec(value);
}
return urls;
}
export function normalizePreloadImageUrl(rawUrl: string) {
const trimmedUrl = rawUrl.trim();
if (!trimmedUrl || trimmedUrl === 'none' || trimmedUrl.startsWith('#')) {
return null;
}
if (
trimmedUrl.startsWith('data:') ||
trimmedUrl.startsWith('blob:') ||
trimmedUrl.startsWith('http://') ||
trimmedUrl.startsWith('https://')
) {
return trimmedUrl;
}
const baseUrl =
typeof document === 'undefined'
? 'http://localhost/'
: document.baseURI;
try {
return new URL(trimmedUrl, baseUrl).href;
} catch {
return null;
}
}
function addNormalizedImageUrl(urls: Set<string>, rawUrl: string | null) {
if (!rawUrl) {
return;
}
const normalizedUrl = normalizePreloadImageUrl(rawUrl);
if (normalizedUrl) {
urls.add(normalizedUrl);
}
}
export function hasCssImageUrlChange(mutation: MutationRecord) {
if (!(mutation.target instanceof HTMLElement)) {
return false;
}
const previousUrls = mutation.oldValue
? extractCssImageUrls(mutation.oldValue)
: [];
const currentUrls = [
...extractCssImageUrls(mutation.target.style.backgroundImage),
...extractCssImageUrls(mutation.target.style.borderImageSource),
...extractCssImageUrls(mutation.target.style.listStyleImage),
];
if (previousUrls.length !== currentUrls.length) {
return true;
}
return currentUrls.some((url, index) => url !== previousUrls[index]);
}
export function collectRouteImageUrls(root: HTMLElement) {
const urls = new Set<string>();
const elements = [root, ...Array.from(root.querySelectorAll<HTMLElement>('*'))];
root.querySelectorAll<HTMLImageElement>('img').forEach((image) => {
addNormalizedImageUrl(urls, image.currentSrc);
addNormalizedImageUrl(urls, image.getAttribute('src'));
});
elements.forEach((element) => {
const computedStyle = window.getComputedStyle(element);
[
element.style.backgroundImage,
element.style.borderImageSource,
element.style.listStyleImage,
computedStyle.backgroundImage,
computedStyle.borderImageSource,
computedStyle.listStyleImage,
].forEach((cssImageValue) => {
extractCssImageUrls(cssImageValue).forEach((url) => {
addNormalizedImageUrl(urls, url);
});
});
});
return Array.from(urls);
}