86 lines
3.0 KiB
TypeScript
86 lines
3.0 KiB
TypeScript
import postcss, { type AtRule, type Root } from 'postcss';
|
|
import valueParser, { type Node as CssValueNode } from 'postcss-value-parser';
|
|
|
|
export interface CssUrlReference {
|
|
raw: string;
|
|
start: number;
|
|
end: number;
|
|
}
|
|
|
|
export function cssUrlReferences(value: string): CssUrlReference[] {
|
|
const refs: CssUrlReference[] = [];
|
|
valueParser(value).walk((node: CssValueNode) => {
|
|
if (node.type !== 'function' || node.value.toLowerCase() !== 'url') return;
|
|
const raw = valueParser.stringify(node.nodes).trim();
|
|
const unquoted =
|
|
(raw.startsWith('"') && raw.endsWith('"')) || (raw.startsWith("'") && raw.endsWith("'"))
|
|
? raw.slice(1, -1)
|
|
: raw;
|
|
refs.push({ raw: unquoted, start: node.sourceIndex, end: node.sourceEndIndex });
|
|
return false;
|
|
});
|
|
return refs;
|
|
}
|
|
|
|
export function rewriteCssValue(
|
|
value: string,
|
|
replacementFor: (raw: string) => string | undefined,
|
|
): string {
|
|
return cssUrlReferences(value)
|
|
.sort((left, right) => right.start - left.start)
|
|
.reduce((rewritten, ref) => {
|
|
const replacement = replacementFor(ref.raw);
|
|
return replacement === undefined
|
|
? rewritten
|
|
: rewritten.slice(0, ref.start) + `url(${replacement})` + rewritten.slice(ref.end);
|
|
}, value);
|
|
}
|
|
|
|
export function cssImportReference(rule: AtRule): { url: string; conditions: string } | null {
|
|
const parsed = valueParser(rule.params);
|
|
const node = parsed.nodes.find(
|
|
(candidate) => candidate.type !== 'space' && candidate.type !== 'comment',
|
|
);
|
|
if (!node) return null;
|
|
let url: string | null = null;
|
|
if (node.type === 'string') url = node.value;
|
|
if (node.type === 'function' && node.value.toLowerCase() === 'url') {
|
|
const raw = valueParser.stringify(node.nodes).trim();
|
|
url =
|
|
(raw.startsWith('"') && raw.endsWith('"')) || (raw.startsWith("'") && raw.endsWith("'"))
|
|
? raw.slice(1, -1)
|
|
: raw;
|
|
}
|
|
if (!url) return null;
|
|
return { url, conditions: rule.params.slice(node.sourceEndIndex).trim() };
|
|
}
|
|
|
|
export function parseCss(css: string, cssUrl: string): Root {
|
|
try {
|
|
return postcss.parse(css, { from: undefined });
|
|
} catch (error) {
|
|
throw new Error(
|
|
`interactive-css-parse-failed:${cssUrl}:${error instanceof Error ? error.message : String(error)}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
export function collectCssAssetReferences(
|
|
css: string,
|
|
context: 'stylesheet' | 'declaration-list' = 'stylesheet',
|
|
): Array<{ kind: 'css-url' | 'css-import'; url: string }> {
|
|
const root = parseCss(context === 'stylesheet' ? css : `.x{${css}}`, 'inline-css');
|
|
const refs: Array<{ kind: 'css-url' | 'css-import'; url: string }> = [];
|
|
root.walkAtRules((rule) => {
|
|
if (rule.name.toLowerCase() !== 'import') return;
|
|
const reference = cssImportReference(rule);
|
|
if (reference) refs.push({ kind: 'css-import', url: reference.url });
|
|
});
|
|
root.walkDecls((declaration) => {
|
|
for (const ref of cssUrlReferences(declaration.value)) {
|
|
refs.push({ kind: 'css-url', url: ref.raw });
|
|
}
|
|
});
|
|
return refs;
|
|
}
|