Skip to content

Commit

Permalink
[FEATURE] Theming Parameters via CSS Variables
Browse files Browse the repository at this point in the history
  • Loading branch information
matz3 committed Sep 2, 2021
1 parent caccdf3 commit 8aabe83
Show file tree
Hide file tree
Showing 9 changed files with 248 additions and 18 deletions.
8 changes: 8 additions & 0 deletions lib/engine.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
let engineInfo;
module.exports.getInfo = function() {
if (!engineInfo) {
const {name, version} = require("../package.json");
engineInfo = {name, version};
}
return engineInfo;
};
15 changes: 12 additions & 3 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const fileUtilsFactory = require("./fileUtils");

const Compiler = require("./Compiler");

const themingParametersDataUri = require("./themingParameters/dataUri");
const themingParameters = require("./themingParameters/index");

// Workaround for a performance issue in the "css" parser module when used in combination
// with the "colors" module that enhances the String prototype.
Expand Down Expand Up @@ -108,6 +108,8 @@ Builder.prototype.cacheTheme = function(result) {
* @param {boolean} options.cssVariables whether or not to enable css variables output
* @param {string} options.lessInput less string input
* @param {string} options.lessInputPath less file input
* @param {themingParameters.STRATEGY} [options.themingParameters=DATA_URI]
* Strategy of including theming parameters within library CSS
* @returns {{css: string, cssRtl: string, variables: {}, imports: [], cssSkeleton: string, cssSkeletonRtl: string, cssVariables: string, cssVariablesSource: string }}
*/
Builder.prototype.build = function(options) {
Expand All @@ -122,9 +124,16 @@ Builder.prototype.build = function(options) {
parser: {},
compiler: {},
library: {},
scope: {}
scope: {},
themingParameters: themingParameters.STRATEGY.DATA_URI
}, options);

if (!themingParameters.STRATEGY[options.themingParameters]) {
return Promise.reject(
new Error("Invalid themingParameters strategy provided! Valid values: CSS_VARIABLES, DATA_URI")
);
}

if (options.compiler.sourceMap) {
return Promise.reject(new Error("compiler.sourceMap option is not supported!"));
}
Expand All @@ -144,7 +153,7 @@ Builder.prototype.build = function(options) {
});

function addInlineParameters(result) {
return themingParametersDataUri.addInlineParameters({result, options});
return themingParameters.addInlineParameters({result, options});
}

function getScopeVariables(options) {
Expand Down
87 changes: 87 additions & 0 deletions lib/themingParameters/cssVariables.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
const engine = require("../engine");

// match a CSS url
// (taken from sap/ui/core/theming/Parameters.js)
const rCssUrl = /url[\s]*\('?"?([^\'")]*)'?"?\)/;

module.exports = {
addInlineParameters({result, options}) {
// CSS Variables can only be added when the library name is known
if (typeof options.library !== "object" || typeof options.library.name !== "string") {
return;
}

const libraryNameDashed = options.library.name.replace(/\./g, "-");
const themeMetadata = this.getThemeMetadata({result, options});

const themeMetadataVariable = `
:root {
--sapThemeMetaData-UI5-${libraryNameDashed}: ${JSON.stringify(themeMetadata)};
}
`;

const urlVariables = this.getUrlVariables({result});
const themingCssVariables = this.getThemingCssVariables({result});

const themingParameters = `
/* Inline theming parameters (CSS Variables) */
:root {
--sapUiTheme-${libraryNameDashed}: ${JSON.stringify(urlVariables)};
${themingCssVariables}
}
`;

result.css += themeMetadataVariable + themingParameters;
if (options.rtl) {
result.cssRtl += themeMetadataVariable + themingParameters;
}
},
getThemeMetadata({result, options}) {
let scopes;
if (typeof result.variables.scopes === "object") {
scopes = Object.keys(result.variables.scopes);
} else {
scopes = [];
}

const libraryNameSlashed = options.library.name.replace(/\./g, "/");

// TODO: How to get theme name? .theming "sId"? parse from file path? new parameter?
const themeId = "<theme-name>";
// TODO: How to get base theme name(s)? Read from .theming?
const Extends = ["<base-theme>"];

const {version, name} = engine.getInfo();
return {
Path: `UI5.${libraryNameSlashed}.${themeId}.library`,
PathPattern: "/%frameworkId%/%libId%/themes/%themeId%/%fileId%.css",
Extends,
Scopes: scopes,
Engine: {
Version: version,
Name: name
},
Version: {
Build: "<TODO>", // TOOD: add new property options.library.version
Source: "<TODO>" // TOOD: add new property options.library.version
}
};
},
getUrlVariables({result}) {
const urlVariables = {};

// TODO: support scopes (default/scopes top-level properties, see runtime code)
Object.entries(result.variables).map(([name, value]) => {
if (rCssUrl.test(value)) {
urlVariables[name] = value;
}
});

return urlVariables;
},
getThemingCssVariables({result}) {
return Object.entries(result.variables).map(([name, value]) => {
return ` --${name}: ${value};`;
}).join("\n");
},
};
4 changes: 1 addition & 3 deletions lib/themingParameters/dataUri.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module.exports = {
addInlineParameters: function({result, options}) {
// Inline parameters can only be added when the library name is known
if (typeof options.library !== "object" || typeof options.library.name !== "string") {
return result;
return;
}

const parameters = JSON.stringify(result.variables);
Expand All @@ -27,7 +27,5 @@ module.exports = {
// for the css variables build we just add it to the variables
result.cssVariables += parameterStyleRule;
}

return result;
}
};
19 changes: 19 additions & 0 deletions lib/themingParameters/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const themingParameters = {
addInlineParameters({result, options}) {
switch (options.themingParameters) {
case themingParameters.STRATEGY.DATA_URI:
require("./dataUri").addInlineParameters({result, options});
break;
case themingParameters.STRATEGY.CSS_VARIABLES:
require("./cssVariables").addInlineParameters({result, options});
break;
}
return result;
},
STRATEGY: {
DATA_URI: "DATA_URI",
CSS_VARIABLES: "CSS_VARIABLES"
}
};

module.exports = themingParameters;
33 changes: 33 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
"eslint-config-google": "^0.14.0",
"graceful-fs": "^4.2.8",
"mocha": "^8.4.0",
"mock-require": "^3.0.3",
"nyc": "^15.1.0",
"sinon": "^11.1.2"
}
Expand Down
75 changes: 75 additions & 0 deletions test/lib/themingParameters/cssVariables.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/* eslint-env mocha */

const assert = require("assert");
const sinon = require("sinon");
const mock = require("mock-require");

describe("themingParameters/cssVariables", function() {
let themingParametersCssVariables;

before(() => {
mock("../../../lib/engine", {
getInfo: sinon.stub().returns({
name: "less-openui5",
version: "1.0.0-test"
})
});
themingParametersCssVariables = require("../../../lib/themingParameters/cssVariables");
});
after(() => {
mock.stopAll();
});

it("should not add theming parameters when library name is missing", function() {
const result = {};
const options = {};
const returnValue = themingParametersCssVariables.addInlineParameters({result, options});

assert.equal(returnValue, undefined, "nothing should be returned");
assert.deepEqual(result, {}, "result object should not be modified");
});

it("should add theming parameters to css", function() {
const result = {
css: "/* css */",
variables: {foo: "bar"}
};
const options = {
library: {
name: "sap.ui.test"
}
};
const expectedMetadata = {
Path: `UI5.sap/ui/test.<theme-name>.library`, // TODO: theme name placeholder
PathPattern: "/%frameworkId%/%libId%/themes/%themeId%/%fileId%.css",
Extends: ["<base-theme>"], // TODO: base theme placeholder
Scopes: [],
Engine: {
Version: "1.0.0-test",
Name: "less-openui5"
},
Version: {
Build: "<TODO>",
Source: "<TODO>"
}
};

const returnValue = themingParametersCssVariables.addInlineParameters({result, options});

assert.equal(returnValue, undefined, "nothing should be returned");
assert.deepEqual(result, {
variables: {foo: "bar"},
css: `/* css */
:root {
--sapThemeMetaData-UI5-sap-ui-test: ${JSON.stringify(expectedMetadata)};
}
/* Inline theming parameters (CSS Variables) */
:root {
--sapUiTheme-sap-ui-test: {};
--foo: bar;
}
`
}, "result.css should be enhanced");
});
});
24 changes: 12 additions & 12 deletions test/lib/themingParameters/dataUri.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ describe("themingParameters/dataUri", function() {
it("should not add theming parameters when library name is missing", function() {
const result = {};
const options = {};
const returnedResult = themingParametersDataUri.addInlineParameters({result, options});
const returnValue = themingParametersDataUri.addInlineParameters({result, options});

assert.equal(returnedResult, result, "result object reference should be returned");
assert.equal(returnValue, undefined, "nothing should be returned");
assert.deepEqual(result, {}, "result object should not be modified");
});

Expand All @@ -25,9 +25,9 @@ describe("themingParameters/dataUri", function() {
name: "sap.ui.test"
}
};
const returnedResult = themingParametersDataUri.addInlineParameters({result, options});
const returnValue = themingParametersDataUri.addInlineParameters({result, options});

assert.equal(returnedResult, result, "result object reference should be returned");
assert.equal(returnValue, undefined, "nothing should be returned");
assert.deepEqual(result, {
variables: {foo: "bar"},
css: `/* css */
Expand All @@ -50,9 +50,9 @@ describe("themingParameters/dataUri", function() {
name: "sap.ui.test"
}
};
const returnedResult = themingParametersDataUri.addInlineParameters({result, options});
const returnValue = themingParametersDataUri.addInlineParameters({result, options});

assert.equal(returnedResult, result, "result object reference should be returned");
assert.equal(returnValue, undefined, "nothing should be returned");
assert.deepEqual(result, {
variables: {
foo: "50%",
Expand All @@ -78,9 +78,9 @@ data:text/plain;utf-8,%7B%22foo%22%3A%2250%25%22%2C%22bar%22%3A%22%27%5C%22%27%2
},
rtl: true
};
const returnedResult = themingParametersDataUri.addInlineParameters({result, options});
const returnValue = themingParametersDataUri.addInlineParameters({result, options});

assert.equal(returnedResult, result, "result object reference should be returned");
assert.equal(returnValue, undefined, "nothing should be returned");
assert.deepEqual(result, {
variables: {foo: "bar"},
css: `/* css */
Expand All @@ -106,9 +106,9 @@ data:text/plain;utf-8,%7B%22foo%22%3A%2250%25%22%2C%22bar%22%3A%22%27%5C%22%27%2
},
cssVariables: true
};
const returnedResult = themingParametersDataUri.addInlineParameters({result, options});
const returnValue = themingParametersDataUri.addInlineParameters({result, options});

assert.equal(returnedResult, result, "result object reference should be returned");
assert.equal(returnValue, undefined, "nothing should be returned");
assert.deepEqual(result, {
variables: {foo: "bar"},
css: `/* css */
Expand Down Expand Up @@ -136,9 +136,9 @@ data:text/plain;utf-8,%7B%22foo%22%3A%2250%25%22%2C%22bar%22%3A%22%27%5C%22%27%2
rtl: true,
cssVariables: true
};
const returnedResult = themingParametersDataUri.addInlineParameters({result, options});
const returnValue = themingParametersDataUri.addInlineParameters({result, options});

assert.equal(returnedResult, result, "result object reference should be returned");
assert.equal(returnValue, undefined, "nothing should be returned");
assert.deepEqual(result, {
variables: {foo: "bar"},
css: `/* css */
Expand Down

0 comments on commit 8aabe83

Please sign in to comment.