diff --git a/package.json b/package.json index 12c73994..d506d872 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "node-html-parser": "^4.0.0", "postcss-css-variables": "^0.18.0", "postcss-flexbugs-fixes": "^5.0.2", + "postcss-value-parser": "^4.2.0", "regenerator-runtime": "^0.13.7", "text-encoding": "^0.7.0", "typescript": "^4.3.5", @@ -54,7 +55,6 @@ "another-json": "^0.2.0", "base64-arraybuffer": "^0.2.0", "dompurify": "^2.3.0", - "off-color": "^2.0.0", - "postcss-value-parser": "^4.2.0" + "off-color": "^2.0.0" } } diff --git a/scripts/postcss/css-compile-variables.js b/scripts/postcss/css-compile-variables.js index 3ed34513..fa584caa 100644 --- a/scripts/postcss/css-compile-variables.js +++ b/scripts/postcss/css-compile-variables.js @@ -43,17 +43,32 @@ function parseDeclarationValue(value) { const parsed = valueParser(value); const variables = []; parsed.walk(node => { - if (node.type !== "function" && node.value !== "var") { + if (node.type !== "function") { return; } - const variable = node.nodes[0]; - variables.push(variable.value); + 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) { - const RE_VARIABLE_VALUE = /--((.+)--(.+)-(.+))/; + const RE_VARIABLE_VALUE = /(?:--)?((.+)--(.+)-(.+))/; const variableCollection = parseDeclarationValue(decl.value); for (const variable of variableCollection) { const matches = variable.match(RE_VARIABLE_VALUE); @@ -94,6 +109,15 @@ function addResolvedVariablesToRootSelector(root, {Rule, Declaration}) { root.append(newRule); } +function populateMapWithDerivedVariables(map, cssFileLocation) { + const location = cssFileLocation.match(/(.+)\/.+\.css/)?.[1]; + const derivedVariables = [ + ...([...resolvedMap.keys()].filter(v => !aliasMap.has(v))), + ...([...aliasMap.entries()].map(([alias, variable]) => `${alias}=${variable}`)) + ]; + map.set(location, { "derived-variables": derivedVariables }); +} + /** * @callback derive * @param {string} value - The base value on which an operation is applied @@ -104,6 +128,7 @@ function addResolvedVariablesToRootSelector(root, {Rule, Declaration}) { * * @param {Object} opts - Options for the plugin * @param {derive} opts.derive - The callback which contains the logic for resolving derived variables + * @param {Map} opts.compiledVariables - A map that stores derived variables so that manifest source sections can be produced */ module.exports = (opts = {}) => { aliasMap = new Map(); @@ -112,7 +137,12 @@ module.exports = (opts = {}) => { return { postcssPlugin: "postcss-compile-variables", - Once(root, {Rule, Declaration}) { + 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; + } /* Go through the CSS file once to extract all aliases and base variables. We use these when resolving derived variables later. @@ -120,6 +150,16 @@ module.exports = (opts = {}) => { root.walkDecls(decl => extract(decl)); root.walkDecls(decl => resolveDerivedVariable(decl, opts.derive)); addResolvedVariablesToRootSelector(root, {Rule, Declaration}); + if (opts.compiledVariables){ + populateMapWithDerivedVariables(opts.compiledVariables, cssFileLocation); + } + // Publish both the base-variables and derived-variables to the other postcss-plugins + const combinedMap = new Map([...baseVariables, ...resolvedMap]); + result.messages.push({ + type: "resolved-variable-map", + plugin: "postcss-compile-variables", + colorMap: combinedMap, + }); }, }; }; diff --git a/scripts/postcss/test.js b/scripts/postcss/test.js index 36ff9282..cccb3ea7 100644 --- a/scripts/postcss/test.js +++ b/scripts/postcss/test.js @@ -96,6 +96,7 @@ module.exports.tests = function tests() { `; await run( inputCSS, outputCSS, { }, assert); }, + "multiple aliased-derived variable in single declaration is parsed correctly": async (assert) => { const inputCSS = ` :root { @@ -116,6 +117,48 @@ module.exports.tests = function tests() { } `; await run( inputCSS, outputCSS, { }, assert); + }, + + "compiledVariables map is populated": async (assert) => { + const compiledVariables = new Map(); + 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); + }`; + await postcss([plugin({ derive, compiledVariables })]).process(inputCSS, { from: "/foo/bar/test.css", }); + const actualArray = compiledVariables.get("/foo/bar")["derived-variables"]; + const expectedArray = ["icon-color--darker-20", "my-alias=icon-color--darker-20", "my-alias--lighter-15"]; + assert.deepStrictEqual(actualArray.sort(), expectedArray.sort()); + }, + + "derived variable are supported in urls": async (assert) => { + const inputCSS = ` + :root { + --foo-color: #ff0; + } + div { + background-color: var(--foo-color--lighter-50); + background: url("./foo/bar/icon.svg?primary=foo-color--darker-5"); + } + a { + background: url("foo/bar/icon.svg"); + }`; + const transformedColorLighter = offColor("#ff0").lighten(0.5); + const transformedColorDarker = offColor("#ff0").darken(0.05); + const outputCSS = + inputCSS + + ` + :root { + --foo-color--lighter-50: ${transformedColorLighter.hex()}; + --foo-color--darker-5: ${transformedColorDarker.hex()}; + } + `; + await run( inputCSS, outputCSS, {}, assert); } }; };