diff --git a/lib/engine.js b/lib/engine.js new file mode 100644 index 00000000..e4f9a14f --- /dev/null +++ b/lib/engine.js @@ -0,0 +1,8 @@ +let engineInfo; +module.exports.getInfo = function() { + if (!engineInfo) { + const {name, version} = require("../package.json"); + engineInfo = {name, version}; + } + return engineInfo; +}; diff --git a/lib/index.js b/lib/index.js index 0d7783a6..42d28577 100644 --- a/lib/index.js +++ b/lib/index.js @@ -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. @@ -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) { @@ -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!")); } @@ -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) { diff --git a/lib/themingParameters/cssVariables.js b/lib/themingParameters/cssVariables.js new file mode 100644 index 00000000..e0a26560 --- /dev/null +++ b/lib/themingParameters/cssVariables.js @@ -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 = ""; + // TODO: How to get base theme name(s)? Read from .theming? + const Extends = [""]; + + 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: "", // TOOD: add new property options.library.version + Source: "" // 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"); + }, +}; diff --git a/lib/themingParameters/dataUri.js b/lib/themingParameters/dataUri.js index fcbf1ecf..9610bf6e 100644 --- a/lib/themingParameters/dataUri.js +++ b/lib/themingParameters/dataUri.js @@ -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); @@ -27,7 +27,5 @@ module.exports = { // for the css variables build we just add it to the variables result.cssVariables += parameterStyleRule; } - - return result; } }; diff --git a/lib/themingParameters/index.js b/lib/themingParameters/index.js new file mode 100644 index 00000000..3876f514 --- /dev/null +++ b/lib/themingParameters/index.js @@ -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; diff --git a/package-lock.json b/package-lock.json index 66f0c812..435008fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2188,6 +2188,33 @@ } } }, + "mock-require": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/mock-require/-/mock-require-3.0.3.tgz", + "integrity": "sha512-lLzfLHcyc10MKQnNUCv7dMcoY/2Qxd6wJfbqCcVk3LDb8An4hF6ohk5AztrvgKhJCqj36uyzi/p5se+tvyD+Wg==", + "dev": true, + "requires": { + "get-caller-file": "^1.0.2", + "normalize-path": "^2.1.1" + }, + "dependencies": { + "get-caller-file": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", + "dev": true + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } + } + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -2764,6 +2791,12 @@ "es6-error": "^4.0.1" } }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "dev": true + }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", diff --git a/package.json b/package.json index ae19c199..dece0efe 100644 --- a/package.json +++ b/package.json @@ -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" } diff --git a/test/lib/themingParameters/cssVariables.js b/test/lib/themingParameters/cssVariables.js new file mode 100644 index 00000000..e9ebabd3 --- /dev/null +++ b/test/lib/themingParameters/cssVariables.js @@ -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..library`, // TODO: theme name placeholder + PathPattern: "/%frameworkId%/%libId%/themes/%themeId%/%fileId%.css", + Extends: [""], // TODO: base theme placeholder + Scopes: [], + Engine: { + Version: "1.0.0-test", + Name: "less-openui5" + }, + Version: { + Build: "", + Source: "" + } + }; + + 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"); + }); +}); diff --git a/test/lib/themingParameters/dataUri.js b/test/lib/themingParameters/dataUri.js index a7d64ea8..db0b1d85 100644 --- a/test/lib/themingParameters/dataUri.js +++ b/test/lib/themingParameters/dataUri.js @@ -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"); }); @@ -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 */ @@ -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%", @@ -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 */ @@ -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 */ @@ -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 */