forked from mystiq/hydrogen-web
Merge pull request #701 from vector-im/css-compile-variables-plugin
Theming - Postcss plugin to compile variables
This commit is contained in:
commit
66304ed7e0
6 changed files with 315 additions and 3 deletions
|
@ -10,6 +10,7 @@
|
||||||
"lint-ts": "eslint src/ -c .ts-eslintrc.js --ext .ts",
|
"lint-ts": "eslint src/ -c .ts-eslintrc.js --ext .ts",
|
||||||
"lint-ci": "eslint src/",
|
"lint-ci": "eslint src/",
|
||||||
"test": "impunity --entry-point src/platform/web/main.js src/platform/web/Platform.js --force-esm-dirs lib/ src/ --root-dir src/",
|
"test": "impunity --entry-point src/platform/web/main.js src/platform/web/Platform.js --force-esm-dirs lib/ src/ --root-dir src/",
|
||||||
|
"test:postcss": "impunity --entry-point scripts/postcss/test.js ",
|
||||||
"start": "vite --port 3000",
|
"start": "vite --port 3000",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"build:sdk": "./scripts/sdk/build.sh"
|
"build:sdk": "./scripts/sdk/build.sh"
|
||||||
|
@ -30,6 +31,7 @@
|
||||||
"acorn": "^8.6.0",
|
"acorn": "^8.6.0",
|
||||||
"acorn-walk": "^8.2.0",
|
"acorn-walk": "^8.2.0",
|
||||||
"aes-js": "^3.1.2",
|
"aes-js": "^3.1.2",
|
||||||
|
"bs58": "^4.0.1",
|
||||||
"core-js": "^3.6.5",
|
"core-js": "^3.6.5",
|
||||||
"es6-promise": "https://github.com/bwindels/es6-promise.git#bwindels/expose-flush",
|
"es6-promise": "https://github.com/bwindels/es6-promise.git#bwindels/expose-flush",
|
||||||
"escodegen": "^2.0.0",
|
"escodegen": "^2.0.0",
|
||||||
|
@ -45,13 +47,14 @@
|
||||||
"text-encoding": "^0.7.0",
|
"text-encoding": "^0.7.0",
|
||||||
"typescript": "^4.3.5",
|
"typescript": "^4.3.5",
|
||||||
"vite": "^2.6.14",
|
"vite": "^2.6.14",
|
||||||
"xxhashjs": "^0.2.2",
|
"xxhashjs": "^0.2.2"
|
||||||
"bs58": "^4.0.1"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz",
|
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz",
|
||||||
"another-json": "^0.2.0",
|
"another-json": "^0.2.0",
|
||||||
"base64-arraybuffer": "^0.2.0",
|
"base64-arraybuffer": "^0.2.0",
|
||||||
"dompurify": "^2.3.0"
|
"dompurify": "^2.3.0",
|
||||||
|
"off-color": "^2.0.0",
|
||||||
|
"postcss-value-parser": "^4.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
18
scripts/.eslintrc.js
Normal file
18
scripts/.eslintrc.js
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
module.exports = {
|
||||||
|
"env": {
|
||||||
|
"node": true,
|
||||||
|
"es6": true
|
||||||
|
},
|
||||||
|
"extends": "eslint:recommended",
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 2020,
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"no-console": "off",
|
||||||
|
"no-empty": "off",
|
||||||
|
"no-prototype-builtins": "off",
|
||||||
|
"no-unused-vars": "warn"
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
31
scripts/postcss/color.js
Normal file
31
scripts/postcss/color.js
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const offColor = require("off-color").offColor;
|
||||||
|
|
||||||
|
module.exports.derive = function (value, operation, argument) {
|
||||||
|
const argumentAsNumber = parseInt(argument);
|
||||||
|
switch (operation) {
|
||||||
|
case "darker": {
|
||||||
|
const newColorString = offColor(value).darken(argumentAsNumber / 100).hex();
|
||||||
|
return newColorString;
|
||||||
|
}
|
||||||
|
case "lighter": {
|
||||||
|
const newColorString = offColor(value).lighten(argumentAsNumber / 100).hex();
|
||||||
|
return newColorString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
127
scripts/postcss/css-compile-variables.js
Normal file
127
scripts/postcss/css-compile-variables.js
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const valueParser = require("postcss-value-parser");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
let aliasMap;
|
||||||
|
let resolvedMap;
|
||||||
|
let baseVariables;
|
||||||
|
|
||||||
|
function getValueFromAlias(alias) {
|
||||||
|
const derivedVariable = aliasMap.get(alias);
|
||||||
|
return baseVariables.get(derivedVariable) ?? resolvedMap.get(derivedVariable);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseDeclarationValue(value) {
|
||||||
|
const parsed = valueParser(value);
|
||||||
|
const variables = [];
|
||||||
|
parsed.walk(node => {
|
||||||
|
if (node.type !== "function" && node.value !== "var") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const variable = node.nodes[0];
|
||||||
|
variables.push(variable.value);
|
||||||
|
});
|
||||||
|
return variables;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveDerivedVariable(decl, derive) {
|
||||||
|
const RE_VARIABLE_VALUE = /--((.+)--(.+)-(.+))/;
|
||||||
|
const variableCollection = parseDeclarationValue(decl.value);
|
||||||
|
for (const variable of variableCollection) {
|
||||||
|
const matches = variable.match(RE_VARIABLE_VALUE);
|
||||||
|
if (matches) {
|
||||||
|
const [, wholeVariable, baseVariable, operation, argument] = matches;
|
||||||
|
const value = baseVariables.get(baseVariable) ?? getValueFromAlias(baseVariable);
|
||||||
|
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);
|
||||||
|
resolvedMap.set(wholeVariable, derivedValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extract(decl) {
|
||||||
|
if (decl.variable) {
|
||||||
|
// see if right side is of form "var(--foo)"
|
||||||
|
const wholeVariable = decl.value.match(/var\(--(.+)\)/)?.[1];
|
||||||
|
// remove -- from the prop
|
||||||
|
const prop = decl.prop.substring(2);
|
||||||
|
if (wholeVariable) {
|
||||||
|
aliasMap.set(prop, wholeVariable);
|
||||||
|
// Since this is an alias, we shouldn't store it in baseVariables
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
baseVariables.set(prop, decl.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addResolvedVariablesToRootSelector(root, {Rule, Declaration}) {
|
||||||
|
const newRule = new Rule({ selector: ":root", source: root.source });
|
||||||
|
// Add derived css variables to :root
|
||||||
|
resolvedMap.forEach((value, key) => {
|
||||||
|
const declaration = new Declaration({prop: `--${key}`, value});
|
||||||
|
newRule.append(declaration);
|
||||||
|
});
|
||||||
|
root.append(newRule);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Object} opts - Options for the plugin
|
||||||
|
* @param {derive} opts.derive - The callback which contains the logic for resolving derived variables
|
||||||
|
*/
|
||||||
|
module.exports = (opts = {}) => {
|
||||||
|
aliasMap = new Map();
|
||||||
|
resolvedMap = new Map();
|
||||||
|
baseVariables = new Map();
|
||||||
|
return {
|
||||||
|
postcssPlugin: "postcss-compile-variables",
|
||||||
|
|
||||||
|
Once(root, {Rule, Declaration}) {
|
||||||
|
/*
|
||||||
|
Go through the CSS file once to extract all aliases and base variables.
|
||||||
|
We use these when resolving derived variables later.
|
||||||
|
*/
|
||||||
|
root.walkDecls(decl => extract(decl));
|
||||||
|
root.walkDecls(decl => resolveDerivedVariable(decl, opts.derive));
|
||||||
|
addResolvedVariablesToRootSelector(root, {Rule, Declaration});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.postcss = true;
|
121
scripts/postcss/test.js
Normal file
121
scripts/postcss/test.js
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const offColor = require("off-color").offColor;
|
||||||
|
const postcss = require("postcss");
|
||||||
|
const plugin = require("./css-compile-variables");
|
||||||
|
const derive = require("./color").derive;
|
||||||
|
|
||||||
|
async function run(input, output, opts = {}, assert) {
|
||||||
|
let result = await postcss([plugin({ ...opts, derive })]).process(input, { from: undefined, });
|
||||||
|
assert.strictEqual(
|
||||||
|
result.css.replaceAll(/\s/g, ""),
|
||||||
|
output.replaceAll(/\s/g, "")
|
||||||
|
);
|
||||||
|
assert.strictEqual(result.warnings().length, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.tests = function tests() {
|
||||||
|
return {
|
||||||
|
"derived variables are resolved": async (assert) => {
|
||||||
|
const inputCSS = `
|
||||||
|
:root {
|
||||||
|
--foo-color: #ff0;
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
background-color: var(--foo-color--lighter-50);
|
||||||
|
}`;
|
||||||
|
const transformedColor = offColor("#ff0").lighten(0.5);
|
||||||
|
const outputCSS =
|
||||||
|
inputCSS +
|
||||||
|
`
|
||||||
|
:root {
|
||||||
|
--foo-color--lighter-50: ${transformedColor.hex()};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
await run( inputCSS, outputCSS, {}, assert);
|
||||||
|
},
|
||||||
|
|
||||||
|
"derived variables work with alias": async (assert) => {
|
||||||
|
const inputCSS = `
|
||||||
|
:root {
|
||||||
|
--icon-color: #fff;
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
background: var(--icon-color--darker-20);
|
||||||
|
--my-alias: var(--icon-color--darker-20);
|
||||||
|
color: var(--my-alias--lighter-15);
|
||||||
|
}`;
|
||||||
|
const colorDarker = offColor("#fff").darken(0.2).hex();
|
||||||
|
const aliasLighter = offColor(colorDarker).lighten(0.15).hex();
|
||||||
|
const outputCSS = inputCSS + `:root {
|
||||||
|
--icon-color--darker-20: ${colorDarker};
|
||||||
|
--my-alias--lighter-15: ${aliasLighter};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
await run(inputCSS, outputCSS, { }, assert);
|
||||||
|
},
|
||||||
|
|
||||||
|
"derived variable throws if base not present in config": async (assert) => {
|
||||||
|
const css = `:root {
|
||||||
|
color: var(--icon-color--darker-20);
|
||||||
|
}`;
|
||||||
|
assert.rejects(async () => await postcss([plugin({ variables: {} })]).process(css, { from: undefined, }));
|
||||||
|
},
|
||||||
|
|
||||||
|
"multiple derived variable in single declaration is parsed correctly": async (assert) => {
|
||||||
|
const inputCSS = `
|
||||||
|
:root {
|
||||||
|
--foo-color: #ff0;
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
background-color: linear-gradient(var(--foo-color--lighter-50), var(--foo-color--darker-20));
|
||||||
|
}`;
|
||||||
|
const transformedColor1 = offColor("#ff0").lighten(0.5);
|
||||||
|
const transformedColor2 = offColor("#ff0").darken(0.2);
|
||||||
|
const outputCSS =
|
||||||
|
inputCSS +
|
||||||
|
`
|
||||||
|
:root {
|
||||||
|
--foo-color--lighter-50: ${transformedColor1.hex()};
|
||||||
|
--foo-color--darker-20: ${transformedColor2.hex()};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
await run( inputCSS, outputCSS, { }, assert);
|
||||||
|
},
|
||||||
|
"multiple aliased-derived variable in single declaration is parsed correctly": async (assert) => {
|
||||||
|
const inputCSS = `
|
||||||
|
:root {
|
||||||
|
--foo-color: #ff0;
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
--my-alias: var(--foo-color);
|
||||||
|
background-color: linear-gradient(var(--my-alias--lighter-50), var(--my-alias--darker-20));
|
||||||
|
}`;
|
||||||
|
const transformedColor1 = offColor("#ff0").lighten(0.5);
|
||||||
|
const transformedColor2 = offColor("#ff0").darken(0.2);
|
||||||
|
const outputCSS =
|
||||||
|
inputCSS +
|
||||||
|
`
|
||||||
|
:root {
|
||||||
|
--my-alias--lighter-50: ${transformedColor1.hex()};
|
||||||
|
--my-alias--darker-20: ${transformedColor2.hex()};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
await run( inputCSS, outputCSS, { }, assert);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
12
yarn.lock
12
yarn.lock
|
@ -1133,6 +1133,13 @@ nth-check@^2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
boolbase "^1.0.0"
|
boolbase "^1.0.0"
|
||||||
|
|
||||||
|
off-color@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/off-color/-/off-color-2.0.0.tgz#ecf3bda52e9a78dde535db86361e048741a56631"
|
||||||
|
integrity sha512-JJ9ObbY2CzgT7F8PpdpHGNjQa7QbU8f4DkY3cCxYUq9NezYUMmL/oSofCc5MMaiUnNNBEFCc4w1unMA+R8syvw==
|
||||||
|
dependencies:
|
||||||
|
core-js "^3.6.5"
|
||||||
|
|
||||||
once@^1.3.0:
|
once@^1.3.0:
|
||||||
version "1.4.0"
|
version "1.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
|
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
|
||||||
|
@ -1215,6 +1222,11 @@ postcss-flexbugs-fixes@^5.0.2:
|
||||||
resolved "https://registry.yarnpkg.com/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-5.0.2.tgz#2028e145313074fc9abe276cb7ca14e5401eb49d"
|
resolved "https://registry.yarnpkg.com/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-5.0.2.tgz#2028e145313074fc9abe276cb7ca14e5401eb49d"
|
||||||
integrity sha512-18f9voByak7bTktR2QgDveglpn9DTbBWPUzSOe9g0N4WR/2eSt6Vrcbf0hmspvMI6YWGywz6B9f7jzpFNJJgnQ==
|
integrity sha512-18f9voByak7bTktR2QgDveglpn9DTbBWPUzSOe9g0N4WR/2eSt6Vrcbf0hmspvMI6YWGywz6B9f7jzpFNJJgnQ==
|
||||||
|
|
||||||
|
postcss-value-parser@^4.2.0:
|
||||||
|
version "4.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
|
||||||
|
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
|
||||||
|
|
||||||
postcss@^8.3.8:
|
postcss@^8.3.8:
|
||||||
version "8.3.9"
|
version "8.3.9"
|
||||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.9.tgz#98754caa06c4ee9eb59cc48bd073bb6bd3437c31"
|
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.9.tgz#98754caa06c4ee9eb59cc48bd073bb6bd3437c31"
|
||||||
|
|
Loading…
Reference in a new issue