Support the CSS Functions and Mixins draft spec
Here's the draft spec: https://drafts.csswg.org/css-mixins/
And the CSSWG issue where there have been positive signals: https://github.com/w3c/csswg-drafts/issues/9350
Is there any interest in supporting it now or in the future, possibly under drafts?
I was looking for a way to try this and did some research, then stumbled onto this issue. So I'll do a write-up of my findings :
Since this issue was opened, there's been a lot of working happening behind the scenes on the spec. The editor draft now has mixins, while the public draft only has functions for now.
There's now a couple MDN pages, here and here.
As of now, caniuse shows 58% for @function, and @mixin doesn't have a page yet.
Afaik there's no tooling support, as postcss doesn't support it either.
PostCSS polyfill tracking issue: https://github.com/csstools/postcss-plugins/issues/1666
I just wrote a janky polyfill. Doesn't support argument or return types, nor functions calling functions (let alone with preserved context).
Oh, and when it fails, it does so silently. It's good enough for my case of simple functions.
code
import type {
CustomAtRules,
TokenOrValue,
Variable,
Visitor,
} from "lightningcss";
import { composeVisitors } from "lightningcss";
import type { LightningCSSOptions } from "vite";
function defineVisitor(
visitor: Visitor<CustomAtRules> | (() => Visitor<CustomAtRules>),
): Visitor<CustomAtRules> {
return typeof visitor === "function" ? visitor() : visitor;
}
function* findVars(token: TokenOrValue | TokenOrValue[]): Generator<{
type: "var";
value: Variable;
}> {
if (Array.isArray(token)) {
for (const e of token) yield* findVars(e);
return;
}
switch (token.type) {
case "var": {
yield token;
break;
}
case "function": {
for (const e of token.value.arguments) yield* findVars(e);
break;
}
case "unresolved-color": {
if ("alpha" in token.value) {
for (const a of token.value.alpha) yield* findVars(a);
} else {
for (const a of token.value.dark) yield* findVars(a);
for (const a of token.value.light) yield* findVars(a);
}
break;
}
case "env": {
for (const f of token.value.fallback ?? []) yield* findVars(f);
break;
}
}
}
class CssFunction {
constructor(
public args: Record<string, true /* needs more */>,
public body: TokenOrValue[],
) {}
eval(args: TokenOrValue[]): TokenOrValue[] {
const body: TokenOrValue[] = JSON.parse(JSON.stringify(this.body));
const argsMap = args
.filter((a) => !(a.type === "token" && a.value.type === "comma"))
.reduce((acc, cur, i) => {
acc[Object.keys(this.args)[i]] = cur.value;
return acc;
}, {});
for (const ident of findVars(body)) {
const key = ident.value.name.ident;
if (argsMap[key] && this.args[key]) {
Object.assign(ident, {
type: "token",
value: argsMap[key],
});
}
}
return body;
}
}
const AtFunctionVisitor = defineVisitor(() => {
const fns: Record<string, CssFunction> = {};
return {
Rule: {
custom: {
function(rule) {
if (rule.prelude.type !== "token-list") return;
if (rule.prelude.value[0].type !== "function") return;
const preludeFn = rule.prelude.value[0].value;
if (rule.body.type !== "declaration-list") return;
const bodyBlock = rule.body.value;
const result = bodyBlock.declarations?.[0];
if (
!(result?.property === "custom" && result?.value?.name === "result")
)
return;
fns[preludeFn.name] = new CssFunction(
preludeFn.arguments
.filter((a) => a.type === "dashed-ident")
.map((a) => a.value)
.reduce(
(acc, cur) => {
acc[cur] = true;
return acc;
},
{} as Record<string, true>,
),
result.value.value,
);
return { type: "ignored", value: null };
},
},
},
Function(fn) {
const match = fns[fn.name];
if (match)
return {
type: "function", // hack since we cannot return a token-list
value: {
name: "var",
arguments: [
{ type: "token", value: { type: "ident", value: "--undefined" } },
{ type: "token", value: { type: "comma" } },
...match.eval(fn.arguments),
],
},
};
},
};
});
const options: LightningCSSOptions = {
customAtRules: {
function: {
// https://drafts.csswg.org/css-mixins/#function-rule
prelude: "*", // lightingcss doesn't have syntax for this yet
body: "declaration-list",
},
},
visitor: composeVisitors([AtFunctionVisitor]),
};
export default options;