112 lines
2.9 KiB
TypeScript
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);
|
|
}
|