diff --git a/lib/index.js b/lib/index.js index f833f8c5..cca573a3 100644 --- a/lib/index.js +++ b/lib/index.js @@ -70,6 +70,15 @@ const Builder = function(options) { this.fileUtils = fileUtilsFactory(this.customFs); }; +let engineInfo; +Builder.getEngineInfo = function() { + if (!engineInfo) { + const {name, version} = require("../package.json"); + engineInfo = {name, version}; + } + return engineInfo; +}; + Builder.prototype.getThemeCache = function(rootPath) { return this.themeCacheMapping[rootPath]; }; @@ -104,10 +113,14 @@ Builder.prototype.cacheTheme = function(result) { }; /** - * Creates a themebuild + * Runs a theme build * @param {object} options * @param {object} options.compiler compiler object as passed to less * @param {boolean} options.cssVariables whether or not to enable css variables output + * @param {boolean} [options.inlineThemingParameters=true] Whether theming parameters should be inlined into the + * library CSS content via background-image + * @param {boolean} [options.inlineCssVariables=false] Whether theming parameters should be inlined into the + * library CSS content via CSS Variables * @param {string} options.lessInput less string input * @param {string} options.lessInputPath less file input * @returns {{css: string, cssRtl: string, variables: {}, imports: [], cssSkeleton: string, cssSkeletonRtl: string, cssVariables: string, cssVariablesSource: string }} @@ -128,7 +141,9 @@ Builder.prototype.build = function(options) { parser: {}, compiler: {}, library: {}, - scope: {} + scope: {}, + inlineThemingParameters: true, + inlineCssVariables: false, }, options); if (options.compiler.sourceMap) { @@ -178,7 +193,7 @@ Builder.prototype.build = function(options) { }); } - function compile(config) { + async function compile(config) { const parserOptions = clone(options.parser); let rootpath; @@ -208,151 +223,201 @@ Builder.prototype.build = function(options) { const parser = createParser(parserOptions, fnFileHandler); - return new Promise(function(resolve, reject) { - parser.parse(config.content, function(err, tree) { - if (err) { - reject(err); - } else { - resolve(tree); - } + function parseContent(content) { + return new Promise(function(resolve, reject) { + parser.parse(content, function(err, tree) { + if (err) { + reject(err); + } else { + resolve(tree); + } + }); }); - }).then(async function(tree) { - const result = {}; + } - result.tree = tree; + const tree = await parseContent(config.content); + const result = {tree}; - // plugins to collect imported files and variable values - const oImportCollector = new ImportCollectorPlugin({ - importMappings: mFileMappings[filename] - }); - const oVariableCollector = new VariableCollectorPlugin(options.compiler); - const oUrlCollector = new UrlCollector(); - - // render to css - result.css = tree.toCSS(Object.assign({}, options.compiler, { - plugins: [oImportCollector, oVariableCollector, oUrlCollector] - })); + // plugins to collect imported files and variable values + const oImportCollector = new ImportCollectorPlugin({ + importMappings: mFileMappings[filename] + }); + const oVariableCollector = new VariableCollectorPlugin(options.compiler); + const oUrlCollector = new UrlCollector(); + + // render to css + result.css = tree.toCSS(Object.assign({}, options.compiler, { + plugins: [oImportCollector, oVariableCollector, oUrlCollector] + })); + + // retrieve imported files + result.imports = oImportCollector.getImports(); + + // retrieve reduced set of variables + result.variables = oVariableCollector.getVariables(Object.keys(mFileMappings[filename] || {})); + + // retrieve all variables + result.allVariables = oVariableCollector.getAllVariables(); + + // also compile rtl-version if requested + let oRTL; + if (options.rtl) { + const RTLPlugin = require("./plugin/rtl"); + oRTL = new RTLPlugin(); + + const urls = oUrlCollector.getUrls(); + + const existingImgRtlUrls = (await Promise.all( + urls.map(async ({currentDirectory, relativeUrl}) => { + const relativeImgRtlUrl = RTLPlugin.getRtlImgUrl(relativeUrl); + if (relativeImgRtlUrl) { + const resolvedImgRtlUrl = path.posix.join(currentDirectory, relativeImgRtlUrl); + if (await that.fileUtils.findFile(resolvedImgRtlUrl, options.rootPaths)) { + return resolvedImgRtlUrl; + } + } + }) + )).filter(Boolean); - // retrieve imported files - result.imports = oImportCollector.getImports(); + oRTL.setExistingImgRtlPaths(existingImgRtlUrls); + } - // retrieve reduced set of variables - result.variables = oVariableCollector.getVariables(Object.keys(mFileMappings[filename] || {})); + if (oRTL) { + result.cssRtl = tree.toCSS(Object.assign({}, options.compiler, { + plugins: [oRTL] + })); + } - // retrieve all variables - result.allVariables = oVariableCollector.getAllVariables(); + if (rootpath) { + result.imports.unshift(rootpath); + } - // also compile rtl-version if requested - let oRTL; - if (options.rtl) { - const RTLPlugin = require("./plugin/rtl"); - oRTL = new RTLPlugin(); - - const urls = oUrlCollector.getUrls(); - - const existingImgRtlUrls = (await Promise.all( - urls.map(async ({currentDirectory, relativeUrl}) => { - const relativeImgRtlUrl = RTLPlugin.getRtlImgUrl(relativeUrl); - if (relativeImgRtlUrl) { - const resolvedImgRtlUrl = path.posix.join(currentDirectory, relativeImgRtlUrl); - if (await that.fileUtils.findFile(resolvedImgRtlUrl, options.rootPaths)) { - return resolvedImgRtlUrl; - } - } - }) - )).filter(Boolean); + // also compile css-variables version if requested + if (options.cssVariables || options.inlineCssVariables) { + // parse the content again to have a clean tree + const cssVariablesSkeletonTree = await parseContent(config.content); - oRTL.setExistingImgRtlPaths(existingImgRtlUrls); - } + // generate the skeleton-css and the less-variables + const CSSVariablesCollectorPlugin = require("./plugin/css-variables-collector"); + const oCSSVariablesCollector = new CSSVariablesCollectorPlugin(config); + const oVariableCollector = new VariableCollectorPlugin(options.compiler); + const cssSkeleton = cssVariablesSkeletonTree.toCSS(Object.assign({}, options.compiler, { + plugins: [oCSSVariablesCollector, oVariableCollector] + })); + const varsOverride = oVariableCollector.getAllVariables(); + const cssVariablesSource = oCSSVariablesCollector.toLessVariables(varsOverride); + const cssVariablesOnly = oCSSVariablesCollector.getCssVariablesDeclaration(); + let cssSkeletonRtl; if (oRTL) { - result.cssRtl = tree.toCSS(Object.assign({}, options.compiler, { - plugins: [oRTL] + const oCSSVariablesCollectorRTL = new CSSVariablesCollectorPlugin(config); + cssSkeletonRtl = cssVariablesSkeletonTree.toCSS(Object.assign({}, options.compiler, { + plugins: [oCSSVariablesCollectorRTL, oRTL] })); } - if (rootpath) { - result.imports.unshift(rootpath); - } + // generate the css-variables content out of the less-variables + const cssVariablesTree = await parseContent(cssVariablesSource); + const CSSVariablesPointerPlugin = require("./plugin/css-variables-pointer"); + const cssVariables = cssVariablesTree.toCSS(Object.assign({}, options.compiler, { + plugins: [new CSSVariablesPointerPlugin()] + })); + + result.cssVariables = cssVariables; - // also compile css-variables version if requested + // Only add additional CSS Variables content if requested if (options.cssVariables) { - return new Promise(function(resolve, reject) { - // parse the content again to have a clean tree - parser.parse(config.content, function(err, tree) { - if (err) { - reject(err); - } else { - resolve(tree); - } - }); - }).then(function(tree) { - // generate the skeleton-css and the less-variables - const CSSVariablesCollectorPlugin = require("./plugin/css-variables-collector"); - const oCSSVariablesCollector = new CSSVariablesCollectorPlugin(config); - const oVariableCollector = new VariableCollectorPlugin(options.compiler); - result.cssSkeleton = tree.toCSS(Object.assign({}, options.compiler, { - plugins: [oCSSVariablesCollector, oVariableCollector] - })); - const varsOverride = oVariableCollector.getAllVariables(); - result.cssVariablesSource = oCSSVariablesCollector.toLessVariables(varsOverride); - if (oRTL) { - const oCSSVariablesCollectorRTL = new CSSVariablesCollectorPlugin(config); - result.cssSkeletonRtl = tree.toCSS(Object.assign({}, options.compiler, { - plugins: [oCSSVariablesCollectorRTL, oRTL] - })); - } - return tree; - }).then(function(tree) { - // generate the css-variables content out of the less-variables - return new Promise(function(resolve, reject) { - parser.parse(result.cssVariablesSource, function(err, tree) { - if (err) { - reject(err); - } else { - const CSSVariablesPointerPlugin = require("./plugin/css-variables-pointer"); - result.cssVariables = tree.toCSS(Object.assign({}, options.compiler, { - plugins: [new CSSVariablesPointerPlugin()] - })); - resolve(result); - } - }); - }); - }); + result.cssSkeleton = cssSkeleton; + if (oRTL) { + result.cssSkeletonRtl = cssSkeletonRtl; + } + result.cssVariablesSource = cssVariablesSource; + result.cssVariablesOnly = cssVariablesOnly; } + } - return result; - }); + return result; } - function addInlineParameters(result) { - return new Promise(function(resolve, reject) { - if (typeof options.library === "object" && typeof options.library.name === "string") { - const parameters = JSON.stringify(result.variables); + async function addInlineParameters(result) { + // Inline parameters can only be added when the library name is known + if (typeof options.library !== "object" || typeof options.library.name !== "string") { + return result; + } - // properly escape the parameters to be part of a data-uri - // + escaping single quote (') as it is used to surround the data-uri: url('...') - const escapedParameters = encodeURIComponent(parameters).replace(/'/g, function(char) { - return escape(char); - }); + if (options.inlineThemingParameters === true) { + const parameters = JSON.stringify(result.variables); - // embed parameter variables as plain-text string into css - const parameterStyleRule = "\n/* Inline theming parameters */\n#sap-ui-theme-" + - options.library.name.replace(/\./g, "\\.") + - "{background-image:url('data:text/plain;utf-8," + escapedParameters + "')}\n"; + // properly escape the parameters to be part of a data-uri + // + escaping single quote (') as it is used to surround the data-uri: url('...') + const escapedParameters = encodeURIComponent(parameters).replace(/'/g, function(char) { + return escape(char); + }); - // embed parameter variables as plain-text string into css - result.css += parameterStyleRule; - if (options.rtl) { - result.cssRtl += parameterStyleRule; - } - if (options.cssVariables) { - // for the css variables build we just add it to the variables - result.cssVariables += parameterStyleRule; - } + // embed parameter variables as plain-text string into css + const parameterStyleRule = "\n/* Inline theming parameters */\n#sap-ui-theme-" + + options.library.name.replace(/\./g, "\\.") + + "{background-image:url('data:text/plain;utf-8," + escapedParameters + "')}\n"; + + // embed parameter variables as plain-text string into css + result.css += parameterStyleRule; + if (options.rtl) { + result.cssRtl += parameterStyleRule; } - resolve(result); - }); + if (options.cssVariables) { + // for the css variables build we just add it to the variables + result.cssVariables += parameterStyleRule; + } + } + + if (options.inlineCssVariables === true) { + // let scopes; + // if (typeof result.variables.scopes === "object") { + // scopes = Object.keys(result.variables.scopes); + // } else { + // scopes = []; + // } + + // const libraryNameSlashed = options.library.name.replace(/\./g, "/"); + const libraryNameDashed = options.library.name.replace(/\./g, "-"); + + // TODO: How to get theme name? .theming "sId"? parse from file path? new parameter? + // const themeId = ""; + + // const {version, name} = Builder.getEngineInfo(); + // const metadataJson = JSON.stringify({ + // Path: `UI5.${libraryNameSlashed}.${themeId}.library`, + // PathPattern: "/%frameworkId%/%libId%/themes/%themeId%/%fileId%.css", + // Extends: ["base"], // TODO: Read from .theming? + // Scopes: scopes, + // Engine: { + // Version: version, + // Name: name + // }, + // Version: { + // Build: "", // TOOD: add new property options.library.version + // Source: "" // TOOD: add new property options.library.version + // } + // }); + + // const additionalVariables = ` + // :root { + // --sapUiTheme-${libraryNameDashed}: true; + // --sapThemeMetaData-UI5-${libraryNameDashed}: ${metadataJson}; + // } + // `; + // const additionalVariables = ` + // :root { + // --sapUiTheme-${libraryNameDashed}: true; + // } + // `; + result.css += result.cssVariables; + if (options.rtl) { + result.cssRtl += result.cssVariables; + } + } + + return result; } function getScopeVariables(options) { @@ -606,3 +671,11 @@ Builder.prototype.build = function(options) { }; module.exports.Builder = Builder; + +// Set engine info during unit test execution to have a static version that can be used in the expected build results +if (process.env.NODE_ENV === "test") { + engineInfo = { + name: "less-openui5", + version: "0.0.0-test" + }; +} diff --git a/lib/plugin/css-variables-collector.js b/lib/plugin/css-variables-collector.js index 6b9fdc35..9cb8f77f 100644 --- a/lib/plugin/css-variables-collector.js +++ b/lib/plugin/css-variables-collector.js @@ -13,6 +13,26 @@ const CSSVariablesCollectorPlugin = module.exports = function(config) { this.parenStack = []; }; +CSSVariablesCollectorPlugin.getResolvedUrl = function({rawUrl, filename, libraryName}) { + if (!libraryName) { + return null; + } + const libraryNamespace = libraryName.replace(/\./g, "/"); + const namespaceIndex = filename.indexOf(libraryNamespace); + if (namespaceIndex === -1) { + // TODO: log warning/error? + return null; + } + if (rawUrl.startsWith("/")) { + return rawUrl; + } + const baseUi5Url = "ui5://" + filename.substr(namespaceIndex); + // rawUrl will be resolved against ui5:// path if it is not absolute + // An absolute rawUrl will be returned as-is + const url = new URL(rawUrl, baseUi5Url); + return url.href; +}, + CSSVariablesCollectorPlugin.prototype = { // needed to keep the less variable references intact to use this info for the CSS variables references @@ -58,24 +78,37 @@ CSSVariablesCollectorPlugin.prototype = { }); let lessVariables = ""; Object.keys(vars).forEach((value, index) => { - lessVariables += "@" + value + ": " + vars[value].css + ";\n"; + const variableName = value; + const variableValue = vars[value].css; + lessVariables += `@${variableName}: ${variableValue};\n`; }); Object.keys(this.calcVars).forEach((value, index) => { - lessVariables += "@" + value + ": " + this.calcVars[value].css + ";\n"; - }); - lessVariables += "\n:root {\n"; - Object.keys(vars).forEach((value, index) => { - if (vars[value].export) { - lessVariables += "--" + value + ": @" + value + ";\n"; - } + const variableName = value; + const variableValue = this.calcVars[value].css; + lessVariables += `@${variableName}: ${variableValue};\n`; }); - Object.keys(this.calcVars).forEach((value, index) => { - if (this.calcVars[value].export) { - lessVariables += "--" + value + ": @" + value + ";\n"; + lessVariables += "\n" + this.getCssVariablesDeclaration({includeCalcVars: true}); + return lessVariables; + }, + getCssVariablesDeclaration({includeCalcVars = false} = {}) { + let content = ":root {\n"; + if (typeof this.config.libName === "string") { + content += `--sapUiTheme-${this.config.libName.replace(/\./g, "-")}: true;\n`; + } + Object.keys(this.vars).forEach((value) => { + if (this.vars[value].export) { + content += `--${value}: @${value};\n`; } }); - lessVariables += "}\n"; - return lessVariables; + if (includeCalcVars) { + Object.keys(this.calcVars).forEach((value) => { + if (this.calcVars[value].export) { + content += `--${value}: @${value};\n`; + } + }); + } + content += "}\n"; + return content; }, _getCSS(node) { @@ -159,13 +192,33 @@ CSSVariablesCollectorPlugin.prototype = { if (!this._isInMixinOrParen() && isVarDeclaration) { // add the variable declaration to the list of vars const varName = node.name.substr(1); - const isVarInLib = this._isVarInLibrary({ - filename: node.currentFileInfo.filename - }); - this.vars[varName] = { + const filename = node.currentFileInfo.filename; + const isVarInLib = this._isVarInLibrary({filename}); + const varEntry = { css: this._getCSS(node.value), export: isVarInLib }; + this.vars[varName] = varEntry; + + if (varEntry.export) { + // Create additional _asResolvedUrl variable for runtime resolution of relative urls + const urlMatch = /url[\s]*\('?"?([^'")]*)'?"?\)/.exec(varEntry.css); + if (urlMatch) { + const resolvedUrl = CSSVariablesCollectorPlugin.getResolvedUrl({ + rawUrl: urlMatch[1], + filename, + libraryName: this.config.libName + }); + // if (resolvedUrl) { + // const resolvedUrlVariableName = `${varName}__asResolvedUrl`; + // const resolvedUrlVariableEntry = { + // css: `"${resolvedUrl}"`, + // export: true + // }; + // this.vars[resolvedUrlVariableName] = resolvedUrlVariableEntry; + // } + } + } } // store the rule context for the call variable extraction this.ruleStack.push(node); diff --git a/package-lock.json b/package-lock.json index c4d0bde4..7a5a39b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -850,6 +850,15 @@ "yaml": "^1.10.0" } }, + "cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.1" + } + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", diff --git a/package.json b/package.json index 55cf29a6..4e294518 100644 --- a/package.json +++ b/package.json @@ -23,8 +23,8 @@ }, "scripts": { "lint": "eslint ./", - "unit": "mocha test/*.js", - "unit-debug": "mocha --inspect --inspect-brk test/*.js", + "unit": "cross-env NODE_ENV=test mocha test/*.js", + "unit-debug": "cross-env NODE_ENV=test mocha --inspect --inspect-brk test/*.js", "coverage": "nyc npm run unit", "test": "npm run lint && npm run coverage && npm run depcheck", "preversion": "npm test", @@ -87,6 +87,7 @@ "mime": "^1.6.0" }, "devDependencies": { + "cross-env": "^7.0.3", "depcheck": "^1.4.2", "eslint": "^7.32.0", "eslint-config-google": "^0.14.0", diff --git a/test/expected/inlineCssVariables/lib3/my/other/ui/lib/themes/bar/library-RTL.css b/test/expected/inlineCssVariables/lib3/my/other/ui/lib/themes/bar/library-RTL.css new file mode 100644 index 00000000..f1f93012 --- /dev/null +++ b/test/expected/inlineCssVariables/lib3/my/other/ui/lib/themes/bar/library-RTL.css @@ -0,0 +1,13 @@ +.myOtherUiLibRule1 { + color: #ffffff; +} +.myOtherUiLibRule2 { + padding: 1px 4px 3px 2px; +} +.barContrast.myOtherUiLibRule1, +.barContrast .myOtherUiLibRule1 { + color: #000000; +} + +/* Inline theming parameters */ +#sap-ui-theme-my\.other\.ui\.lib{background-image:url('data:text/plain;utf-8,%7B%22default%22%3A%7B%22_my_other_ui_lib_MyControl_color1%22%3A%22%23ffffff%22%2C%22_my_other_ui_lib_MyOtherControl_color1%22%3A%22%23ffffff%22%2C%22_my_other_ui_lib_MyControl_color2%22%3A%22%23008000%22%7D%2C%22scopes%22%3A%7B%22barContrast%22%3A%7B%22_my_other_ui_lib_MyControl_color1%22%3A%22%23000000%22%2C%22_my_other_ui_lib_MyOtherControl_color1%22%3A%22%23000000%22%7D%7D%7D')} diff --git a/test/expected/inlineCssVariables/lib3/my/other/ui/lib/themes/bar/library.css b/test/expected/inlineCssVariables/lib3/my/other/ui/lib/themes/bar/library.css new file mode 100644 index 00000000..e4238a0a --- /dev/null +++ b/test/expected/inlineCssVariables/lib3/my/other/ui/lib/themes/bar/library.css @@ -0,0 +1,13 @@ +.myOtherUiLibRule1 { + color: #ffffff; +} +.myOtherUiLibRule2 { + padding: 1px 2px 3px 4px; +} +.barContrast.myOtherUiLibRule1, +.barContrast .myOtherUiLibRule1 { + color: #000000; +} + +/* Inline theming parameters */ +#sap-ui-theme-my\.other\.ui\.lib{background-image:url('data:text/plain;utf-8,%7B%22default%22%3A%7B%22_my_other_ui_lib_MyControl_color1%22%3A%22%23ffffff%22%2C%22_my_other_ui_lib_MyOtherControl_color1%22%3A%22%23ffffff%22%2C%22_my_other_ui_lib_MyControl_color2%22%3A%22%23008000%22%7D%2C%22scopes%22%3A%7B%22barContrast%22%3A%7B%22_my_other_ui_lib_MyControl_color1%22%3A%22%23000000%22%2C%22_my_other_ui_lib_MyOtherControl_color1%22%3A%22%23000000%22%7D%7D%7D')} diff --git a/test/expected/inlineCssVariables/lib3/my/other/ui/lib/themes/base/library-RTL.css b/test/expected/inlineCssVariables/lib3/my/other/ui/lib/themes/base/library-RTL.css new file mode 100644 index 00000000..e1511f69 --- /dev/null +++ b/test/expected/inlineCssVariables/lib3/my/other/ui/lib/themes/base/library-RTL.css @@ -0,0 +1,15 @@ +.myOtherUiLibRule1 { + color: #fefefe; +} +.myOtherUiLibRule2 { + padding: 1px 4px 3px 2px; +} + +:root { + --sapUiTheme-my-other-ui-lib: true; + --sapThemeMetaData-UI5-my-other-ui-lib: {"Path":"UI5.my/other/ui/lib..library","PathPattern":"/%frameworkId%/%libId%/themes/%themeId%/%fileId%.css","Extends":["base"],"Scopes":[],"Engine":{"Version":"0.0.0-test","Name":"less-openui5"},"Version":{"Build":"","Source":""}}; +} +:root { + --_my_other_ui_lib_MyControl_color1: var(--color1); + --_my_other_ui_lib_MyOtherControl_color1: var(--color1); +} diff --git a/test/expected/inlineCssVariables/lib3/my/other/ui/lib/themes/base/library.css b/test/expected/inlineCssVariables/lib3/my/other/ui/lib/themes/base/library.css new file mode 100644 index 00000000..024760b2 --- /dev/null +++ b/test/expected/inlineCssVariables/lib3/my/other/ui/lib/themes/base/library.css @@ -0,0 +1,15 @@ +.myOtherUiLibRule1 { + color: #fefefe; +} +.myOtherUiLibRule2 { + padding: 1px 2px 3px 4px; +} + +:root { + --sapUiTheme-my-other-ui-lib: true; + --sapThemeMetaData-UI5-my-other-ui-lib: {"Path":"UI5.my/other/ui/lib..library","PathPattern":"/%frameworkId%/%libId%/themes/%themeId%/%fileId%.css","Extends":["base"],"Scopes":[],"Engine":{"Version":"0.0.0-test","Name":"less-openui5"},"Version":{"Build":"","Source":""}}; +} +:root { + --_my_other_ui_lib_MyControl_color1: var(--color1); + --_my_other_ui_lib_MyOtherControl_color1: var(--color1); +} diff --git a/test/expected/inlineCssVariables/lib3/my/other/ui/lib/themes/foo/library-RTL.css b/test/expected/inlineCssVariables/lib3/my/other/ui/lib/themes/foo/library-RTL.css new file mode 100644 index 00000000..a0e0c321 --- /dev/null +++ b/test/expected/inlineCssVariables/lib3/my/other/ui/lib/themes/foo/library-RTL.css @@ -0,0 +1,13 @@ +.myOtherUiLibRule1 { + color: #ffffff; +} +.myOtherUiLibRule2 { + padding: 1px 4px 3px 2px; +} +.fooContrast.myOtherUiLibRule1, +.fooContrast .myOtherUiLibRule1 { + color: #000000; +} + +/* Inline theming parameters */ +#sap-ui-theme-my\.other\.ui\.lib{background-image:url('data:text/plain;utf-8,%7B%22default%22%3A%7B%22_my_other_ui_lib_MyControl_color1%22%3A%22%23ffffff%22%2C%22_my_other_ui_lib_MyOtherControl_color1%22%3A%22%23ffffff%22%2C%22_my_other_ui_lib_MyControl_color2%22%3A%22%23008000%22%7D%2C%22scopes%22%3A%7B%22fooContrast%22%3A%7B%22_my_other_ui_lib_MyControl_color1%22%3A%22%23000000%22%2C%22_my_other_ui_lib_MyOtherControl_color1%22%3A%22%23000000%22%7D%7D%7D')} diff --git a/test/expected/inlineCssVariables/lib3/my/other/ui/lib/themes/foo/library.css b/test/expected/inlineCssVariables/lib3/my/other/ui/lib/themes/foo/library.css new file mode 100644 index 00000000..bd068dcc --- /dev/null +++ b/test/expected/inlineCssVariables/lib3/my/other/ui/lib/themes/foo/library.css @@ -0,0 +1,13 @@ +.myOtherUiLibRule1 { + color: #ffffff; +} +.myOtherUiLibRule2 { + padding: 1px 2px 3px 4px; +} +.fooContrast.myOtherUiLibRule1, +.fooContrast .myOtherUiLibRule1 { + color: #000000; +} + +/* Inline theming parameters */ +#sap-ui-theme-my\.other\.ui\.lib{background-image:url('data:text/plain;utf-8,%7B%22default%22%3A%7B%22_my_other_ui_lib_MyControl_color1%22%3A%22%23ffffff%22%2C%22_my_other_ui_lib_MyOtherControl_color1%22%3A%22%23ffffff%22%2C%22_my_other_ui_lib_MyControl_color2%22%3A%22%23008000%22%7D%2C%22scopes%22%3A%7B%22fooContrast%22%3A%7B%22_my_other_ui_lib_MyControl_color1%22%3A%22%23000000%22%2C%22_my_other_ui_lib_MyOtherControl_color1%22%3A%22%23000000%22%7D%7D%7D')} diff --git a/test/expected/libraries/lib1/my/ui/lib/themes/foo/css_variables.css b/test/expected/libraries/lib1/my/ui/lib/themes/foo/css_variables.css index 73cdbe0f..53a012a1 100644 --- a/test/expected/libraries/lib1/my/ui/lib/themes/foo/css_variables.css +++ b/test/expected/libraries/lib1/my/ui/lib/themes/foo/css_variables.css @@ -1,6 +1,7 @@ :root { --color1: #ffffff; --url1: url('../base/111'); + --url1__asResolvedUrl: "ui5://my/ui/lib/themes/base/111"; } .fooContrast { --color1: #000000; diff --git a/test/expected/libraries/lib1/my/ui/lib/themes/foo/css_variables.source.less b/test/expected/libraries/lib1/my/ui/lib/themes/foo/css_variables.source.less index af3758ba..ee423e2d 100644 --- a/test/expected/libraries/lib1/my/ui/lib/themes/foo/css_variables.source.less +++ b/test/expected/libraries/lib1/my/ui/lib/themes/foo/css_variables.source.less @@ -1,7 +1,9 @@ @color1: #ffffff; @url1: url('../base/111'); +@url1__asResolvedUrl: "ui5://my/ui/lib/themes/base/111"; :root { --color1: @color1; --url1: @url1; +--url1__asResolvedUrl: @url1__asResolvedUrl; } diff --git a/test/test-css-variables-collector-plugin.js b/test/test-css-variables-collector-plugin.js new file mode 100644 index 00000000..a9e1dbc2 --- /dev/null +++ b/test/test-css-variables-collector-plugin.js @@ -0,0 +1,220 @@ +/* eslint-env mocha */ +"use strict"; + +const assert = require("assert"); +const path = require("path"); + +const createParser = require("../lib/less/parser"); +function parseContent({parserOptions, fnFileHandler, content}) { + return new Promise(function(resolve, reject) { + const parser = createParser(parserOptions, fnFileHandler); + parser.parse(content, function(err, tree) { + if (err) { + reject(err); + } else { + resolve(tree); + } + }); + }); +} +function createFileHandler(files) { + return function(file, currentFileInfo, handleDataAndCallCallback, callback) { + let pathname; + + // support absolute paths such as "/resources/my/base.less" + if (path.posix.isAbsolute(file)) { + pathname = path.posix.normalize(file); + } else { + pathname = path.posix.join(currentFileInfo.currentDirectory, file); + } + + if (files[pathname]) { + handleDataAndCallCallback(pathname, files[pathname]); + } else { + callback({type: "File", message: "Could not find file at path '" + pathname + "'"}); + } + }; +} + +// tested module +const CSSVariablesCollectorPlugin = require("../lib/plugin/css-variables-collector"); + +describe("CSSVariablesCollectorPlugin.getResolvedUrl", function() { + it("should resolve relative url to ui5:// url (filename with /resources/ - UI5 Tooling)", function() { + assert.strictEqual( + CSSVariablesCollectorPlugin.getResolvedUrl({ + rawUrl: "img/foo.png", + filename: "/resources/sap/ui/foo/themes/base/Foo.less", + libraryName: "sap.ui.foo" + }), + "ui5://sap/ui/foo/themes/base/img/foo.png" + ); + }); + it("should resolve relative url to ui5:// url (filename with / - grunt-openui5/connect-openui5)", function() { + assert.strictEqual( + CSSVariablesCollectorPlugin.getResolvedUrl({ + rawUrl: "img/foo.png", + filename: "/sap/ui/foo/themes/base/Foo.less", + libraryName: "sap.ui.foo" + }), + "ui5://sap/ui/foo/themes/base/img/foo.png" + ); + }); + it("should resolve relative url to ui5:// url (filename absolute fs path - custom usage)", function() { + assert.strictEqual( + CSSVariablesCollectorPlugin.getResolvedUrl({ + rawUrl: "img/foo.png", + filename: "/Users/root/sap/ui/foo/themes/base/Foo.less", + libraryName: "sap.ui.foo" + }), + "ui5://sap/ui/foo/themes/base/img/foo.png" + ); + }); + it("should return server-absolute url as-is", function() { + assert.strictEqual( + CSSVariablesCollectorPlugin.getResolvedUrl({ + rawUrl: "/assets/img/foo.png", + filename: "/resources/sap/ui/foo/themes/base/Foo.less", + libraryName: "sap.ui.foo" + }), + "/assets/img/foo.png" + ); + }); + it("should return absolute http url as-is", function() { + assert.strictEqual( + CSSVariablesCollectorPlugin.getResolvedUrl({ + rawUrl: "http://example.com/assets/img/foo.png", + filename: "/resources/sap/ui/foo/themes/base/Foo.less", + libraryName: "sap.ui.foo" + }), + "http://example.com/assets/img/foo.png" + ); + }); + it("Error: should return null when library namespace is not part of filename", function() { + assert.strictEqual( + CSSVariablesCollectorPlugin.getResolvedUrl({ + rawUrl: "img/foo.png", + filename: "/resources/sap/ui/foo/themes/base/Foo.less", + libraryName: "sap.ui.bar" + }), + null + ); + }); + it("Error: should return null when libraryName is not given", function() { + assert.strictEqual( + CSSVariablesCollectorPlugin.getResolvedUrl({ + rawUrl: "img/foo.png", + filename: "/resources/sap/ui/foo/themes/base/Foo.less", + libraryName: undefined + }), + null + ); + }); +}); + +describe("CSSVariablesCollectorPlugin", function() { + it("should collect variables and modify tree to produce skeleton CSS", async function() { + const oCSSVariablesCollector = new CSSVariablesCollectorPlugin({ + libPath: "sap/ui/foo", + libName: "sap.ui.foo", + prefix: "_sap_ui_foo_" + }); + const tree = await parseContent({ + content: ` + @myVar: red; + .rule { + color: @myVar; + background-color: fade(@myVar, 50%) + } + `, + parserOptions: { + relativeUrls: true, + filename: "sap/ui/foo/themes/base/library.source.less" + } + }); + + const cssSkeleton = tree.toCSS({ + plugins: [oCSSVariablesCollector] + }); + + assert.strictEqual(cssSkeleton, + `.rule { + color: var(--myVar); + background-color: var(--_sap_ui_foo_function_fade1); +} +`); + + const cssVariablesSource = oCSSVariablesCollector.toLessVariables({}); + assert.strictEqual(cssVariablesSource, + `@myVar: #ff0000; +@_sap_ui_foo_function_fade1: fade(@myVar, 50%); + +:root { +--myVar: @myVar; +--_sap_ui_foo_function_fade1: @_sap_ui_foo_function_fade1; +} +`); + + const cssVariablesOnly = oCSSVariablesCollector.getCssVariablesDeclaration(); + assert.strictEqual(cssVariablesOnly, + `:root { +--myVar: @myVar; +} +`); + }); + it("should provide proper relative URLs", async function() { + const oCSSVariablesCollector = new CSSVariablesCollectorPlugin({ + libPath: "sap/ui/foo", + libName: "sap.ui.foo", + prefix: "_sap_ui_foo_" + }); + const tree = await parseContent({ + content: ` + @import "../base/shared.less"; + .rule { + background-image: @myUrl; + } + `, + parserOptions: { + relativeUrls: true, + filename: "sap/ui/foo/themes/sap_fiori_3/library.source.less" + }, + fnFileHandler: createFileHandler({ + "sap/ui/foo/themes/base/shared.less": ` + @myUrl: url(./fancy.png); + ` + }) + }); + + const cssSkeleton = tree.toCSS({ + plugins: [oCSSVariablesCollector] + }); + + assert.strictEqual(cssSkeleton, + `.rule { + background-image: var(--myUrl); +} +`); + + const cssVariablesSource = oCSSVariablesCollector.toLessVariables({ + myUrl: "url(../base/fancy.png)" + }); + assert.strictEqual(cssVariablesSource, + `@myUrl: url(../base/fancy.png); +@myUrl__asResolvedUrl: "ui5://sap/ui/foo/themes/base/fancy.png"; + +:root { +--myUrl: @myUrl; +--myUrl__asResolvedUrl: @myUrl__asResolvedUrl; +} +`); + + const cssVariablesOnly = oCSSVariablesCollector.getCssVariablesDeclaration(); + assert.strictEqual(cssVariablesOnly, + `:root { +--myUrl: @myUrl; +--myUrl__asResolvedUrl: @myUrl__asResolvedUrl; +} +`); + }); +}); diff --git a/test/test-css-vars.js b/test/test-css-vars.js index b0e70d0a..c07421ed 100644 --- a/test/test-css-vars.js +++ b/test/test-css-vars.js @@ -38,6 +38,46 @@ describe("css vars", function() { "css variables source should be correctly generated."); }); }); + + it("should generate the correct css variables for foo theme", function() { + return new Builder().build({ + lessInputPath: "my/ui/lib/themes/foo/library.source.less", + rootPaths: [ + "test/fixtures/libraries/lib1", + "test/fixtures/libraries/lib2" + ], + library: { + name: "my.ui.lib" + }, + cssVariables: true + }).then(function(result) { + const oVariablesExpected = { + "default": { + "color1": "#ffffff", + "url1": "url('../base/111')", + + }, + "scopes": { + "fooContrast": { + "color1": "#000000" + } + } + }; + + assert.equal(result.css, readFile("test/expected/libraries/lib1/my/ui/lib/themes/foo/library.css"), + "css should be correctly generated."); + assert.equal(result.cssRtl, readFile("test/expected/libraries/lib1/my/ui/lib/themes/foo/library-RTL.css"), + "rtl css should be correctly generated."); + assert.deepEqual(result.variables, oVariablesExpected, "variables should be correctly collected."); + assert.deepEqual(result.allVariables, oVariablesExpected, "allVariables should be correctly collected."); + assert.equal(result.cssSkeleton, readFile("test/expected/libraries/lib1/my/ui/lib/themes/foo/library_skeleton.css"), + "library_skeleton.css should be correctly generated."); + assert.equal(result.cssSkeletonRtl, readFile("test/expected/libraries/lib1/my/ui/lib/themes/foo/library_skeleton-RTL.css"), + "library_skeleton-RTL.css should be correctly generated."); + assert.equal(result.cssVariables, readFile("test/expected/libraries/lib1/my/ui/lib/themes/foo/css_variables.css"), + "css variables should be correctly generated."); + }); + }); }); it("should generate the correct css variables for foo theme", function() { diff --git a/test/test.js b/test/test.js index 00d59975..96ca9606 100644 --- a/test/test.js +++ b/test/test.js @@ -394,6 +394,48 @@ describe("libraries (my/other/ui/lib)", function() { }); }); +describe("inlineCssVariables / inlineThemingParameters", function() { + it("should create base theme (inlineCssVariables: true / inlineThemingParameters: false)", function() { + return new Builder().build({ + lessInputPath: "my/other/ui/lib/themes/base/library.source.less", + rootPaths: [ + "test/fixtures/libraries/lib1", + "test/fixtures/libraries/lib3" + ], + library: { + name: "my.other.ui.lib" + }, + inlineCssVariables: true, + inlineThemingParameters: false + }).then(function(result) { + assert.strictEqual(result.css, readFile("test/expected/inlineCssVariables/lib3/my/other/ui/lib/themes/base/library.css"), "css should be correctly generated."); + assert.strictEqual(result.cssRtl, readFile("test/expected/inlineCssVariables/lib3/my/other/ui/lib/themes/base/library-RTL.css"), "rtl css should be correctly generated."); + assert.deepStrictEqual(result.variables, { + "_my_other_ui_lib_MyControl_color1": "#fefefe", + "_my_other_ui_lib_MyOtherControl_color1": "#fefefe" + }, "variables should be correctly collected."); + assert.deepStrictEqual(result.allVariables, { + "_my_other_ui_lib_MyControl_color1": "#fefefe", + "_my_other_ui_lib_MyOtherControl_color1": "#fefefe", + "color1": "#fefefe", + "url1": "url('../../../../../../my/ui/lib/themes/base/111')", + }, "allVariables should be correctly collected."); + assert.deepStrictEqual(result.imports, [ + path.join("test", "fixtures", "libraries", "lib3", "my", "other", "ui", "lib", "themes", "base", "library.source.less"), + path.join("test", "fixtures", "libraries", "lib1", "my", "ui", "lib", "themes", "base", "global.less"), + path.join("test", "fixtures", "libraries", "lib3", "my", "other", "ui", "lib", "themes", "base", "MyControl.less"), + path.join("test", "fixtures", "libraries", "lib3", "my", "other", "ui", "lib", "themes", "base", "sub-directory", "MyOtherControl.less") + ], "import list should be correct."); + + // When using inlineCssVariables=true without cssVariables=true, there shouldn't be additional files exposed + assert.strictEqual(result.cssSkeleton, undefined); + assert.strictEqual(result.cssSkeletonRtl, undefined); + // assert.strictEqual(result.cssVariables, undefined); // TODO + assert.strictEqual(result.cssVariablesSource, undefined); + }); + }); +}); + describe("error handling", function() { it("should have correct error in case of undefined variable usage (lessInput)", function() { return new Builder().build({