vector-im-hydrogen-web/scripts/postcss/css-compile-variables.js

173 lines
6.7 KiB
JavaScript
Raw Normal View History

2022-03-09 17:22:11 +05:30
/*
2025-01-17 17:05:05 +00:00
Copyright 2025 New Vector Ltd.
2022-03-09 17:22:11 +05:30
Copyright 2021 The Matrix.org Foundation C.I.C.
2025-01-17 17:05:05 +00:00
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
2022-03-09 17:22:11 +05:30
*/
const valueParser = require("postcss-value-parser");
2022-03-03 19:58:46 +05:30
2022-03-23 17:25:12 +05:30
/**
* This plugin derives new css variables from a given set of base variables.
* A derived css variable has the form --base--operation-argument; meaning that the derived
* variable has a value that is generated from the base variable "base" by applying "operation"
* with given "argument".
*
* eg: given the base variable --foo-color: #40E0D0, --foo-color--darker-20 is a css variable
* derived from foo-color by making it 20% more darker.
*
* All derived variables are added to the :root section.
*
* The actual derivation is done outside the plugin in a callback.
*/
function getValueFromAlias(alias, {aliasMap, baseVariables, resolvedMap}) {
2022-03-23 17:12:14 +05:30
const derivedVariable = aliasMap.get(alias);
2022-03-14 23:26:37 +05:30
return baseVariables.get(derivedVariable) ?? resolvedMap.get(derivedVariable);
2022-03-03 19:58:46 +05:30
}
function parseDeclarationValue(value) {
const parsed = valueParser(value);
const variables = [];
parsed.walk(node => {
2022-04-06 12:23:55 +05:30
if (node.type !== "function") {
return;
2022-03-07 11:33:44 +05:30
}
2022-04-06 12:23:55 +05:30
switch (node.value) {
case "var": {
const variable = node.nodes[0];
variables.push(variable.value);
break;
}
case "url": {
const url = node.nodes[0].value;
// resolve url with some absolute url so that we get the query params without using regex
const params = new URL(url, "file://foo/bar/").searchParams;
const primary = params.get("primary");
const secondary = params.get("secondary");
if (primary) { variables.push(primary); }
if (secondary) { variables.push(secondary); }
break;
}
}
});
return variables;
}
function resolveDerivedVariable(decl, derive, maps, isDark) {
const { baseVariables, resolvedMap } = maps;
2022-04-06 12:23:55 +05:30
const RE_VARIABLE_VALUE = /(?:--)?((.+)--(.+)-(.+))/;
const variableCollection = parseDeclarationValue(decl.value);
for (const variable of variableCollection) {
const matches = variable.match(RE_VARIABLE_VALUE);
if (matches) {
2022-03-23 20:39:24 +05:30
const [, wholeVariable, baseVariable, operation, argument] = matches;
const value = baseVariables.get(baseVariable) ?? getValueFromAlias(baseVariable, maps);
2022-03-10 16:05:13 +05:30
if (!value) {
throw new Error(`Cannot derive from ${baseVariable} because it is neither defined in config nor is it an alias!`);
}
const derivedValue = derive(value, operation, argument, isDark);
resolvedMap.set(wholeVariable, derivedValue);
2022-03-07 11:33:44 +05:30
}
2022-03-03 19:58:46 +05:30
}
}
function extract(decl, {aliasMap, baseVariables}) {
if (decl.variable) {
2022-03-14 23:26:37 +05:30
// see if right side is of form "var(--foo)"
2022-03-23 20:39:24 +05:30
const wholeVariable = decl.value.match(/var\(--(.+)\)/)?.[1];
// remove -- from the prop
const prop = decl.prop.substring(2);
if (wholeVariable) {
2022-03-23 20:39:24 +05:30
aliasMap.set(prop, wholeVariable);
2022-03-14 23:26:37 +05:30
// Since this is an alias, we shouldn't store it in baseVariables
return;
}
2022-03-23 20:39:24 +05:30
baseVariables.set(prop, decl.value);
2022-03-03 19:58:46 +05:30
}
}
function addResolvedVariablesToRootSelector(root, {Rule, Declaration}, {resolvedMap}) {
const newRule = new Rule({ selector: ":root", source: root.source });
// Add derived css variables to :root
resolvedMap.forEach((value, key) => {
2022-03-23 20:39:24 +05:30
const declaration = new Declaration({prop: `--${key}`, value});
newRule.append(declaration);
});
root.append(newRule);
}
function populateMapWithDerivedVariables(map, cssFileLocation, {resolvedMap, aliasMap}) {
2022-04-01 16:23:33 +05:30
const location = cssFileLocation.match(/(.+)\/.+\.css/)?.[1];
2022-04-01 20:43:42 +05:30
const derivedVariables = [
2022-04-01 16:23:33 +05:30
...([...resolvedMap.keys()].filter(v => !aliasMap.has(v))),
...([...aliasMap.entries()].map(([alias, variable]) => `${alias}=${variable}`))
2022-04-01 20:43:42 +05:30
];
2022-07-05 20:07:48 +05:30
const sharedObject = map.get(location);
const output = { "derived-variables": derivedVariables };
if (sharedObject) {
Object.assign(sharedObject, output);
}
else {
map.set(location, output);
}
2022-04-01 16:23:33 +05:30
}
2022-03-10 17:19:04 +05:30
/**
* @callback derive
* @param {string} value - The base value on which an operation is applied
* @param {string} operation - The operation to be applied (eg: darker, lighter...)
* @param {string} argument - The argument for this operation
2022-04-12 21:02:30 +05:30
* @param {boolean} isDark - Indicates whether this theme is dark
2022-03-10 17:19:04 +05:30
*/
/**
*
* @param {Object} opts - Options for the plugin
* @param {derive} opts.derive - The callback which contains the logic for resolving derived variables
2022-04-01 16:23:33 +05:30
* @param {Map} opts.compiledVariables - A map that stores derived variables so that manifest source sections can be produced
2022-03-03 19:58:46 +05:30
*/
module.exports = (opts = {}) => {
const aliasMap = new Map();
const resolvedMap = new Map();
const baseVariables = new Map();
const maps = { aliasMap, resolvedMap, baseVariables };
2022-03-07 11:33:44 +05:30
return {
postcssPlugin: "postcss-compile-variables",
2022-03-03 19:58:46 +05:30
2022-03-27 20:06:26 +05:30
Once(root, {Rule, Declaration, result}) {
const cssFileLocation = root.source.input.from;
if (cssFileLocation.includes("type=runtime")) {
// If this is a runtime theme, don't derive variables.
return;
}
const isDark = cssFileLocation.includes("dark=true");
2022-03-07 11:33:44 +05:30
/*
2022-03-14 23:26:37 +05:30
Go through the CSS file once to extract all aliases and base variables.
We use these when resolving derived variables later.
2022-03-07 11:33:44 +05:30
*/
root.walkDecls(decl => extract(decl, maps));
root.walkDecls(decl => resolveDerivedVariable(decl, opts.derive, maps, isDark));
addResolvedVariablesToRootSelector(root, {Rule, Declaration}, maps);
2022-04-01 16:23:33 +05:30
if (opts.compiledVariables){
populateMapWithDerivedVariables(opts.compiledVariables, cssFileLocation, maps);
2022-04-01 16:23:33 +05:30
}
// Also produce a mapping from alias to completely resolved color
const resolvedAliasMap = new Map();
aliasMap.forEach((value, key) => {
resolvedAliasMap.set(key, resolvedMap.get(value));
});
// Publish the base-variables, derived-variables and resolved aliases to the other postcss-plugins
const combinedMap = new Map([...baseVariables, ...resolvedMap, ...resolvedAliasMap]);
2022-03-27 20:06:26 +05:30
result.messages.push({
type: "resolved-variable-map",
plugin: "postcss-compile-variables",
colorMap: combinedMap,
2022-04-01 16:23:33 +05:30
});
2022-03-07 11:33:44 +05:30
},
};
2022-03-03 19:58:46 +05:30
};
module.exports.postcss = true;