211 lines
5.7 KiB
JavaScript
211 lines
5.7 KiB
JavaScript
import MagicString from 'magic-string';
|
|
import * as svelte from 'svelte/compiler';
|
|
|
|
/** @typedef {ReturnType<typeof import('svelte/compiler').parseCss>['children']} StyleSheetChildren */
|
|
|
|
/** @typedef {{ property: string; value: string; start: number; end: number; type: 'Declaration' }} Declaration */
|
|
|
|
const parse = svelte.parseCss
|
|
? svelte.parseCss
|
|
: /** @param {string} css */
|
|
(css) => {
|
|
return /** @type {{ css: { children: StyleSheetChildren } }} */ (
|
|
svelte.parse(`<style>${css}</style>`)
|
|
).css;
|
|
};
|
|
|
|
const SKIP_PARSING_REGEX = /url\(/i;
|
|
|
|
/** Capture a single url(...) so we can process them one at a time */
|
|
const URL_FUNCTION_REGEX = /url\(\s*.*?\)/gi;
|
|
|
|
/** Captures the value inside a CSS url(...) */
|
|
const URL_PARAMETER_REGEX = /url\(\s*(['"]?)(.*?)\1\s*\)/i;
|
|
|
|
/** Splits the URL if there's a query string or hash fragment */
|
|
const HASH_OR_QUERY_REGEX = /[#?]/;
|
|
|
|
/**
|
|
* Assets handled by Vite that are referenced in the stylesheet always start
|
|
* with this prefix because Vite emits them into the same directory as the CSS file
|
|
*/
|
|
const VITE_ASSET_PREFIX = './';
|
|
|
|
const AST_OFFSET = '<style>'.length;
|
|
|
|
/**
|
|
* We need to fix the asset URLs in the CSS before we inline them into a document
|
|
* because they are now relative to the document instead of the CSS file.
|
|
* @param {{
|
|
* css: string;
|
|
* vite_assets: Set<string>;
|
|
* static_assets: Set<string>;
|
|
* paths_assets: string;
|
|
* base: string;
|
|
* static_asset_prefix: string;
|
|
* }} opts
|
|
* @returns {string}
|
|
*/
|
|
export function fix_css_urls({
|
|
css,
|
|
vite_assets,
|
|
static_assets,
|
|
paths_assets,
|
|
base,
|
|
static_asset_prefix
|
|
}) {
|
|
// skip parsing if there are no url(...) occurrences
|
|
if (!SKIP_PARSING_REGEX.test(css)) {
|
|
return css;
|
|
}
|
|
|
|
// safe guard in case of trailing slashes (but this should never happen)
|
|
if (paths_assets.endsWith('/')) {
|
|
paths_assets = paths_assets.slice(0, -1);
|
|
}
|
|
|
|
if (base.endsWith('/')) {
|
|
base = base.slice(0, -1);
|
|
}
|
|
|
|
const s = new MagicString(css);
|
|
|
|
const parsed = parse(css);
|
|
|
|
for (const child of parsed.children) {
|
|
find_declarations(child, (declaration) => {
|
|
if (!SKIP_PARSING_REGEX.test) return;
|
|
|
|
const cleaned = tippex_comments_and_strings(declaration.value);
|
|
|
|
/** @type {string} */
|
|
let new_value = declaration.value;
|
|
|
|
/** @type {RegExpExecArray | null} */
|
|
let url_function_found;
|
|
URL_FUNCTION_REGEX.lastIndex = 0;
|
|
while ((url_function_found = URL_FUNCTION_REGEX.exec(cleaned))) {
|
|
const [url_function] = url_function_found;
|
|
|
|
// After finding a legitimate url(...), we want to operate on the original
|
|
// that may have a string inside it
|
|
const original_url_function = declaration.value.slice(
|
|
url_function_found.index,
|
|
url_function_found.index + url_function.length
|
|
);
|
|
|
|
const url_parameter_found = URL_PARAMETER_REGEX.exec(original_url_function);
|
|
if (!url_parameter_found) continue;
|
|
|
|
const [, , url] = url_parameter_found;
|
|
const [url_without_hash_or_query] = url.split(HASH_OR_QUERY_REGEX);
|
|
|
|
/** @type {string | undefined} */
|
|
let new_prefix;
|
|
|
|
// Check if it's an asset processed by Vite...
|
|
let current_prefix = url_without_hash_or_query.slice(0, VITE_ASSET_PREFIX.length);
|
|
let filename = url_without_hash_or_query.slice(VITE_ASSET_PREFIX.length);
|
|
const decoded = decodeURIComponent(filename);
|
|
|
|
if (current_prefix === VITE_ASSET_PREFIX && vite_assets.has(decoded)) {
|
|
new_prefix = paths_assets;
|
|
} else {
|
|
// ...or if it's from the static directory
|
|
current_prefix = url_without_hash_or_query.slice(0, static_asset_prefix.length);
|
|
filename = url_without_hash_or_query.slice(static_asset_prefix.length);
|
|
const decoded = decodeURIComponent(filename);
|
|
|
|
if (current_prefix === static_asset_prefix && static_assets.has(decoded)) {
|
|
new_prefix = base;
|
|
}
|
|
}
|
|
|
|
if (!new_prefix) continue;
|
|
|
|
new_value = new_value.replace(`${current_prefix}${filename}`, `${new_prefix}/${filename}`);
|
|
}
|
|
|
|
if (declaration.value === new_value) return;
|
|
|
|
if (!svelte.parseCss) {
|
|
declaration.start = declaration.start - AST_OFFSET;
|
|
declaration.end = declaration.end - AST_OFFSET;
|
|
}
|
|
|
|
s.update(declaration.start, declaration.end, `${declaration.property}: ${new_value}`);
|
|
});
|
|
}
|
|
|
|
return s.toString();
|
|
}
|
|
|
|
/**
|
|
* @param {StyleSheetChildren[0]} rule
|
|
* @param {(declaration: Declaration) => void} callback
|
|
*/
|
|
function find_declarations(rule, callback) {
|
|
// Vite already inlines relative @import rules, so we don't need to handle them here
|
|
if (!rule.block) return;
|
|
|
|
for (const child of rule.block.children) {
|
|
if (child.type !== 'Declaration') {
|
|
find_declarations(child, callback);
|
|
continue;
|
|
}
|
|
callback(child);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Replaces comment and string contents with whitespace.
|
|
* @param {string} value
|
|
* @returns {string}
|
|
*/
|
|
export function tippex_comments_and_strings(value) {
|
|
let new_value = '';
|
|
let escaped = false;
|
|
let in_comment = false;
|
|
|
|
/** @type {null | '"' | "'"} */
|
|
let quote_mark = null;
|
|
|
|
let i = 0;
|
|
while (i < value.length) {
|
|
const char = value[i];
|
|
|
|
if (in_comment) {
|
|
if (char === '*' && value[i + 1] === '/') {
|
|
in_comment = false;
|
|
new_value += char;
|
|
} else {
|
|
new_value += ' ';
|
|
}
|
|
} else if (!quote_mark && char === '/' && value[i + 1] === '*') {
|
|
in_comment = true;
|
|
new_value += '/*';
|
|
i++; // skip the '*' since we already added it
|
|
} else if (escaped) {
|
|
new_value += ' ';
|
|
escaped = false;
|
|
} else if (quote_mark && char === '\\') {
|
|
escaped = true;
|
|
new_value += ' ';
|
|
} else if (char === quote_mark) {
|
|
quote_mark = null;
|
|
new_value += char;
|
|
} else if (quote_mark) {
|
|
new_value += ' ';
|
|
} else if (quote_mark === null && (char === '"' || char === "'")) {
|
|
quote_mark = char;
|
|
new_value += char;
|
|
} else {
|
|
new_value += char;
|
|
}
|
|
|
|
i++;
|
|
}
|
|
|
|
return new_value;
|
|
}
|