diff --git a/package.json b/package.json index d506d872..af863ced 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "lint-ts": "eslint src/ -c .ts-eslintrc.js --ext .ts", "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:postcss": "impunity --entry-point scripts/postcss/test.js ", + "test:postcss": "impunity --entry-point scripts/postcss/tests/css-compile-variables.test.js scripts/postcss/tests/css-url-to-variables.test.js", "start": "vite --port 3000", "build": "vite build", "build:sdk": "./scripts/sdk/build.sh" diff --git a/scripts/postcss/css-url-to-variables.js b/scripts/postcss/css-url-to-variables.js new file mode 100644 index 00000000..1d4666f4 --- /dev/null +++ b/scripts/postcss/css-url-to-variables.js @@ -0,0 +1,85 @@ +/* +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 extracts content inside url() into css variables and adds the variables to the root section. + * This plugin is used in conjunction with css-url-processor plugin to colorize svg icons. + */ +let counter; +let urlVariables; +const idToPrepend = "icon-url"; + +function findAndReplaceUrl(decl) { + const value = decl.value; + const parsed = valueParser(value); + parsed.walk(node => { + if (node.type !== "function" || node.value !== "url") { + return; + } + const url = node.nodes[0].value; + if (!url.match(/\.svg\?primary=.+/)) { + return; + } + const variableName = `${idToPrepend}-${counter++}`; + urlVariables.set(variableName, url); + node.value = "var"; + node.nodes = [{ type: "word", value: `--${variableName}` }]; + }); + decl.assign({prop: decl.prop, value: parsed.toString()}) +} + +function addResolvedVariablesToRootSelector(root, { Rule, Declaration }) { + const newRule = new Rule({ selector: ":root", source: root.source }); + // Add derived css variables to :root + urlVariables.forEach((value, key) => { + const declaration = new Declaration({ prop: `--${key}`, value: `url("${value}")`}); + newRule.append(declaration); + }); + root.append(newRule); +} + +function populateMapWithIcons(map, cssFileLocation) { + const location = cssFileLocation.match(/(.+)\/.+\.css/)?.[1]; + const sharedObject = map.get(location); + sharedObject["icon"] = Object.fromEntries(urlVariables); +} + +/* * + * @type {import('postcss').PluginCreator} + */ +module.exports = (opts = {}) => { + urlVariables = new Map(); + counter = 0; + return { + postcssPlugin: "postcss-url-to-variable", + + Once(root, { Rule, Declaration }) { + root.walkDecls(decl => findAndReplaceUrl(decl)); + if (urlVariables.size) { + addResolvedVariablesToRootSelector(root, { Rule, Declaration }); + } + if (opts.compiledVariables){ + const cssFileLocation = root.source.input.from; + populateMapWithIcons(opts.compiledVariables, cssFileLocation); + } + }, + }; +}; + +module.exports.postcss = true; + diff --git a/scripts/postcss/tests/common.js b/scripts/postcss/tests/common.js new file mode 100644 index 00000000..78ae847e --- /dev/null +++ b/scripts/postcss/tests/common.js @@ -0,0 +1,30 @@ +/* +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 postcss = require("postcss"); + +module.exports.createTestRunner = function (plugin) { + return async function run(input, output, opts = {}, assert) { + let result = await postcss([plugin(opts)]).process(input, { from: undefined, }); + assert.strictEqual( + result.css.replaceAll(/\s/g, ""), + output.replaceAll(/\s/g, "") + ); + assert.strictEqual(result.warnings().length, 0); + }; +} + + diff --git a/scripts/postcss/test.js b/scripts/postcss/tests/css-compile-variables.test.js similarity index 88% rename from scripts/postcss/test.js rename to scripts/postcss/tests/css-compile-variables.test.js index cccb3ea7..e40751db 100644 --- a/scripts/postcss/test.js +++ b/scripts/postcss/tests/css-compile-variables.test.js @@ -16,17 +16,9 @@ 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); -} +const plugin = require("../css-compile-variables"); +const derive = require("../color").derive; +const run = require("./common").createTestRunner(plugin); module.exports.tests = function tests() { return { @@ -46,7 +38,7 @@ module.exports.tests = function tests() { --foo-color--lighter-50: ${transformedColor.hex()}; } `; - await run( inputCSS, outputCSS, {}, assert); + await run( inputCSS, outputCSS, {derive}, assert); }, "derived variables work with alias": async (assert) => { @@ -66,7 +58,7 @@ module.exports.tests = function tests() { --my-alias--lighter-15: ${aliasLighter}; } `; - await run(inputCSS, outputCSS, { }, assert); + await run(inputCSS, outputCSS, {derive}, assert); }, "derived variable throws if base not present in config": async (assert) => { @@ -94,7 +86,7 @@ module.exports.tests = function tests() { --foo-color--darker-20: ${transformedColor2.hex()}; } `; - await run( inputCSS, outputCSS, { }, assert); + await run( inputCSS, outputCSS, {derive}, assert); }, "multiple aliased-derived variable in single declaration is parsed correctly": async (assert) => { @@ -116,7 +108,7 @@ module.exports.tests = function tests() { --my-alias--darker-20: ${transformedColor2.hex()}; } `; - await run( inputCSS, outputCSS, { }, assert); + await run( inputCSS, outputCSS, {derive}, assert); }, "compiledVariables map is populated": async (assert) => { @@ -158,7 +150,7 @@ module.exports.tests = function tests() { --foo-color--darker-5: ${transformedColorDarker.hex()}; } `; - await run( inputCSS, outputCSS, {}, assert); + await run( inputCSS, outputCSS, {derive}, assert); } }; }; diff --git a/scripts/postcss/tests/css-url-to-variables.test.js b/scripts/postcss/tests/css-url-to-variables.test.js new file mode 100644 index 00000000..f406a38a --- /dev/null +++ b/scripts/postcss/tests/css-url-to-variables.test.js @@ -0,0 +1,71 @@ +/* +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 plugin = require("../css-url-to-variables"); +const run = require("./common").createTestRunner(plugin); +const postcss = require("postcss"); + +module.exports.tests = function tests() { + return { + "url is replaced with variable": async (assert) => { + const inputCSS = `div { + background: no-repeat center/80% url("../img/image.svg?primary=main-color--darker-20"); + } + button { + background: url("/home/foo/bar/cool.svg?primary=blue&secondary=green"); + }`; + const outputCSS = + `div { + background: no-repeat center/80% var(--icon-url-0); + } + button { + background: var(--icon-url-1); + }`+ + ` + :root { + --icon-url-0: url("../img/image.svg?primary=main-color--darker-20"); + --icon-url-1: url("/home/foo/bar/cool.svg?primary=blue&secondary=green"); + } + `; + await run(inputCSS, outputCSS, { }, assert); + }, + "non svg urls without query params are not replaced": async (assert) => { + const inputCSS = `div { + background: no-repeat url("./img/foo/bar/image.png"); + }`; + await run(inputCSS, inputCSS, {}, assert); + }, + "map is populated with icons": async (assert) => { + const compiledVariables = new Map(); + compiledVariables.set("/foo/bar", { "derived-variables": ["background-color--darker-20", "accent-color--lighter-15"] }); + const inputCSS = `div { + background: no-repeat center/80% url("../img/image.svg?primary=main-color--darker-20"); + } + button { + background: url("/home/foo/bar/cool.svg?primary=blue&secondary=green"); + }`; + const expectedObject = { + "icon-url-0": "../img/image.svg?primary=main-color--darker-20", + "icon-url-1": "/home/foo/bar/cool.svg?primary=blue&secondary=green", + }; + await postcss([plugin({compiledVariables})]).process(inputCSS, { from: "/foo/bar/test.css", }); + const sharedVariable = compiledVariables.get("/foo/bar"); + assert.deepEqual(["background-color--darker-20", "accent-color--lighter-15"], sharedVariable["derived-variables"]); + assert.deepEqual(expectedObject, sharedVariable["icon"]); + } + }; +}; +