Files
openmaic/OpenMAIC/lib/export/css-asset-parser.ts
2026-08-16 14:58:47 +08:00

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;
}