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, 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(); const elements = [ root, ...Array.from(root.querySelectorAll('*')), ]; root.querySelectorAll('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); }