Skip to content

Commit 04a1f31

Browse files
committed
[FEATURE] Improved support for CSS Variables
- Add inlineThemingParameters/inlineCssVariables params, refactor - Implement inlineCssVariables option - Add very basic test for inlineCssVariables=true - Add marker / metadata variables - Adopt metadata value / enhance object - Now just using JSON.stringify without wrapping the value in quotes - WIP: Generate __asResolvedUrl variables - Enhance url resolving / update expected build output - Update and fix getResolvedUrl - Add another getResolvedUrl test case - Add url() params to tests
1 parent df32e38 commit 04a1f31

File tree

11 files changed

+472
-132
lines changed

11 files changed

+472
-132
lines changed

lib/index.js

Lines changed: 174 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ const ImportCollectorPlugin = require("./plugin/import-collector");
1515
const VariableCollectorPlugin = require("./plugin/variable-collector");
1616
const UrlCollector = require("./plugin/url-collector");
1717

18+
const LESS_OPENUI5_VERSION = require("../package.json").version;
19+
1820
// Workaround for a performance issue in the "css" parser module when used in combination
1921
// with the "colors" module that enhances the String prototype.
2022
// See: https://github.com/reworkcss/css/issues/88
@@ -104,10 +106,14 @@ Builder.prototype.cacheTheme = function(result) {
104106
};
105107

106108
/**
107-
* Creates a themebuild
109+
* Runs a theme build
108110
* @param {object} options
109111
* @param {object} options.compiler compiler object as passed to less
110112
* @param {boolean} options.cssVariables whether or not to enable css variables output
113+
* @param {boolean} [options.inlineThemingParameters=true] Whether theming parameters should be inlined into the
114+
* library CSS content via background-image
115+
* @param {boolean} [options.inlineCssVariables=false] Whether theming parameters should be inlined into the
116+
* library CSS content via CSS Variables
111117
* @param {string} options.lessInput less string input
112118
* @param {string} options.lessInputPath less file input
113119
* @returns {{css: string, cssRtl: string, variables: {}, imports: [], cssSkeleton: string, cssSkeletonRtl: string, cssVariables: string, cssVariablesSource: string }}
@@ -128,7 +134,9 @@ Builder.prototype.build = function(options) {
128134
parser: {},
129135
compiler: {},
130136
library: {},
131-
scope: {}
137+
scope: {},
138+
inlineThemingParameters: true,
139+
inlineCssVariables: false,
132140
}, options);
133141

134142
if (options.compiler.sourceMap) {
@@ -178,7 +186,7 @@ Builder.prototype.build = function(options) {
178186
});
179187
}
180188

181-
function compile(config) {
189+
async function compile(config) {
182190
const parserOptions = clone(options.parser);
183191
let rootpath;
184192

@@ -208,151 +216,193 @@ Builder.prototype.build = function(options) {
208216

209217
const parser = createParser(parserOptions, fnFileHandler);
210218

211-
return new Promise(function(resolve, reject) {
212-
parser.parse(config.content, function(err, tree) {
213-
if (err) {
214-
reject(err);
215-
} else {
216-
resolve(tree);
217-
}
219+
function parseContent(content) {
220+
return new Promise(function(resolve, reject) {
221+
parser.parse(content, function(err, tree) {
222+
if (err) {
223+
reject(err);
224+
} else {
225+
resolve(tree);
226+
}
227+
});
218228
});
219-
}).then(async function(tree) {
220-
const result = {};
221-
222-
result.tree = tree;
229+
}
223230

224-
// plugins to collect imported files and variable values
225-
const oImportCollector = new ImportCollectorPlugin({
226-
importMappings: mFileMappings[filename]
227-
});
228-
const oVariableCollector = new VariableCollectorPlugin(options.compiler);
229-
const oUrlCollector = new UrlCollector();
231+
const tree = await parseContent(config.content);
232+
const result = {tree};
230233

231-
// render to css
232-
result.css = tree.toCSS(Object.assign({}, options.compiler, {
233-
plugins: [oImportCollector, oVariableCollector, oUrlCollector]
234-
}));
234+
// plugins to collect imported files and variable values
235+
const oImportCollector = new ImportCollectorPlugin({
236+
importMappings: mFileMappings[filename]
237+
});
238+
const oVariableCollector = new VariableCollectorPlugin(options.compiler);
239+
const oUrlCollector = new UrlCollector();
240+
241+
// render to css
242+
result.css = tree.toCSS(Object.assign({}, options.compiler, {
243+
plugins: [oImportCollector, oVariableCollector, oUrlCollector]
244+
}));
245+
246+
// retrieve imported files
247+
result.imports = oImportCollector.getImports();
248+
249+
// retrieve reduced set of variables
250+
result.variables = oVariableCollector.getVariables(Object.keys(mFileMappings[filename] || {}));
251+
252+
// retrieve all variables
253+
result.allVariables = oVariableCollector.getAllVariables();
254+
255+
// also compile rtl-version if requested
256+
let oRTL;
257+
if (options.rtl) {
258+
const RTLPlugin = require("./plugin/rtl");
259+
oRTL = new RTLPlugin();
260+
261+
const urls = oUrlCollector.getUrls();
262+
263+
const existingImgRtlUrls = (await Promise.all(
264+
urls.map(async ({currentDirectory, relativeUrl}) => {
265+
const relativeImgRtlUrl = RTLPlugin.getRtlImgUrl(relativeUrl);
266+
if (relativeImgRtlUrl) {
267+
const resolvedImgRtlUrl = path.posix.join(currentDirectory, relativeImgRtlUrl);
268+
if (await that.fileUtils.findFile(resolvedImgRtlUrl, options.rootPaths)) {
269+
return resolvedImgRtlUrl;
270+
}
271+
}
272+
})
273+
)).filter(Boolean);
235274

236-
// retrieve imported files
237-
result.imports = oImportCollector.getImports();
275+
oRTL.setExistingImgRtlPaths(existingImgRtlUrls);
276+
}
238277

239-
// retrieve reduced set of variables
240-
result.variables = oVariableCollector.getVariables(Object.keys(mFileMappings[filename] || {}));
278+
if (oRTL) {
279+
result.cssRtl = tree.toCSS(Object.assign({}, options.compiler, {
280+
plugins: [oRTL]
281+
}));
282+
}
241283

242-
// retrieve all variables
243-
result.allVariables = oVariableCollector.getAllVariables();
284+
if (rootpath) {
285+
result.imports.unshift(rootpath);
286+
}
244287

245-
// also compile rtl-version if requested
246-
let oRTL;
247-
if (options.rtl) {
248-
const RTLPlugin = require("./plugin/rtl");
249-
oRTL = new RTLPlugin();
250-
251-
const urls = oUrlCollector.getUrls();
252-
253-
const existingImgRtlUrls = (await Promise.all(
254-
urls.map(async ({currentDirectory, relativeUrl}) => {
255-
const relativeImgRtlUrl = RTLPlugin.getRtlImgUrl(relativeUrl);
256-
if (relativeImgRtlUrl) {
257-
const resolvedImgRtlUrl = path.posix.join(currentDirectory, relativeImgRtlUrl);
258-
if (await that.fileUtils.findFile(resolvedImgRtlUrl, options.rootPaths)) {
259-
return resolvedImgRtlUrl;
260-
}
261-
}
262-
})
263-
)).filter(Boolean);
288+
// also compile css-variables version if requested
289+
if (options.cssVariables || options.inlineCssVariables) {
290+
// parse the content again to have a clean tree
291+
const cssVariablesSkeletonTree = await parseContent(config.content);
264292

265-
oRTL.setExistingImgRtlPaths(existingImgRtlUrls);
266-
}
293+
// generate the skeleton-css and the less-variables
294+
const CSSVariablesCollectorPlugin = require("./plugin/css-variables-collector");
295+
const oCSSVariablesCollector = new CSSVariablesCollectorPlugin(config);
296+
const oVariableCollector = new VariableCollectorPlugin(options.compiler);
297+
const cssSkeleton = cssVariablesSkeletonTree.toCSS(Object.assign({}, options.compiler, {
298+
plugins: [oCSSVariablesCollector, oVariableCollector]
299+
}));
300+
const varsOverride = oVariableCollector.getAllVariables();
301+
const cssVariablesSource = oCSSVariablesCollector.toLessVariables(varsOverride);
267302

303+
let cssSkeletonRtl;
268304
if (oRTL) {
269-
result.cssRtl = tree.toCSS(Object.assign({}, options.compiler, {
270-
plugins: [oRTL]
305+
const oCSSVariablesCollectorRTL = new CSSVariablesCollectorPlugin(config);
306+
cssSkeletonRtl = cssVariablesSkeletonTree.toCSS(Object.assign({}, options.compiler, {
307+
plugins: [oCSSVariablesCollectorRTL, oRTL]
271308
}));
272309
}
273310

274-
if (rootpath) {
275-
result.imports.unshift(rootpath);
276-
}
311+
// generate the css-variables content out of the less-variables
312+
const cssVariablesTree = await parseContent(cssVariablesSource);
313+
const CSSVariablesPointerPlugin = require("./plugin/css-variables-pointer");
314+
const cssVariables = cssVariablesTree.toCSS(Object.assign({}, options.compiler, {
315+
plugins: [new CSSVariablesPointerPlugin()]
316+
}));
317+
318+
result.cssVariables = cssVariables;
277319

278-
// also compile css-variables version if requested
320+
// Only add additional CSS Variables content if requested
279321
if (options.cssVariables) {
280-
return new Promise(function(resolve, reject) {
281-
// parse the content again to have a clean tree
282-
parser.parse(config.content, function(err, tree) {
283-
if (err) {
284-
reject(err);
285-
} else {
286-
resolve(tree);
287-
}
288-
});
289-
}).then(function(tree) {
290-
// generate the skeleton-css and the less-variables
291-
const CSSVariablesCollectorPlugin = require("./plugin/css-variables-collector");
292-
const oCSSVariablesCollector = new CSSVariablesCollectorPlugin(config);
293-
const oVariableCollector = new VariableCollectorPlugin(options.compiler);
294-
result.cssSkeleton = tree.toCSS(Object.assign({}, options.compiler, {
295-
plugins: [oCSSVariablesCollector, oVariableCollector]
296-
}));
297-
const varsOverride = oVariableCollector.getAllVariables();
298-
result.cssVariablesSource = oCSSVariablesCollector.toLessVariables(varsOverride);
299-
if (oRTL) {
300-
const oCSSVariablesCollectorRTL = new CSSVariablesCollectorPlugin(config);
301-
result.cssSkeletonRtl = tree.toCSS(Object.assign({}, options.compiler, {
302-
plugins: [oCSSVariablesCollectorRTL, oRTL]
303-
}));
304-
}
305-
return tree;
306-
}).then(function(tree) {
307-
// generate the css-variables content out of the less-variables
308-
return new Promise(function(resolve, reject) {
309-
parser.parse(result.cssVariablesSource, function(err, tree) {
310-
if (err) {
311-
reject(err);
312-
} else {
313-
const CSSVariablesPointerPlugin = require("./plugin/css-variables-pointer");
314-
result.cssVariables = tree.toCSS(Object.assign({}, options.compiler, {
315-
plugins: [new CSSVariablesPointerPlugin()]
316-
}));
317-
resolve(result);
318-
}
319-
});
320-
});
321-
});
322+
result.cssSkeleton = cssSkeleton;
323+
if (oRTL) {
324+
result.cssSkeletonRtl = cssSkeletonRtl;
325+
}
326+
result.cssVariablesSource = cssVariablesSource;
322327
}
328+
}
323329

324-
return result;
325-
});
330+
return result;
326331
}
327332

328-
function addInlineParameters(result) {
329-
return new Promise(function(resolve, reject) {
330-
if (typeof options.library === "object" && typeof options.library.name === "string") {
331-
const parameters = JSON.stringify(result.variables);
333+
async function addInlineParameters(result) {
334+
// Inline parameters can only be added when the library name is known
335+
if (typeof options.library !== "object" || typeof options.library.name !== "string") {
336+
return result;
337+
}
332338

333-
// properly escape the parameters to be part of a data-uri
334-
// + escaping single quote (') as it is used to surround the data-uri: url('...')
335-
const escapedParameters = encodeURIComponent(parameters).replace(/'/g, function(char) {
336-
return escape(char);
337-
});
339+
if (options.inlineThemingParameters === true) {
340+
const parameters = JSON.stringify(result.variables);
338341

339-
// embed parameter variables as plain-text string into css
340-
const parameterStyleRule = "\n/* Inline theming parameters */\n#sap-ui-theme-" +
341-
options.library.name.replace(/\./g, "\\.") +
342-
"{background-image:url('data:text/plain;utf-8," + escapedParameters + "')}\n";
342+
// properly escape the parameters to be part of a data-uri
343+
// + escaping single quote (') as it is used to surround the data-uri: url('...')
344+
const escapedParameters = encodeURIComponent(parameters).replace(/'/g, function(char) {
345+
return escape(char);
346+
});
343347

344-
// embed parameter variables as plain-text string into css
345-
result.css += parameterStyleRule;
346-
if (options.rtl) {
347-
result.cssRtl += parameterStyleRule;
348-
}
349-
if (options.cssVariables) {
350-
// for the css variables build we just add it to the variables
351-
result.cssVariables += parameterStyleRule;
348+
// embed parameter variables as plain-text string into css
349+
const parameterStyleRule = "\n/* Inline theming parameters */\n#sap-ui-theme-" +
350+
options.library.name.replace(/\./g, "\\.") +
351+
"{background-image:url('data:text/plain;utf-8," + escapedParameters + "')}\n";
352+
353+
// embed parameter variables as plain-text string into css
354+
result.css += parameterStyleRule;
355+
if (options.rtl) {
356+
result.cssRtl += parameterStyleRule;
357+
}
358+
if (options.cssVariables) {
359+
// for the css variables build we just add it to the variables
360+
result.cssVariables += parameterStyleRule;
361+
}
362+
}
363+
364+
if (options.inlineCssVariables === true) {
365+
let scopes;
366+
if (typeof result.variables.scopes === "object") {
367+
scopes = Object.keys(result.variables.scopes);
368+
} else {
369+
scopes = [];
370+
}
371+
372+
const libraryNameSlashed = options.library.name.replace(/\./g, "/");
373+
const libraryNameDashed = options.library.name.replace(/\./g, "-");
374+
375+
// TODO: How to get theme name? .theming "sId"? parse from file path? new parameter?
376+
const themeId = "<theme-name>";
377+
378+
const metadataJson = JSON.stringify({
379+
"Path": `UI5.${libraryNameSlashed}.${themeId}.library`,
380+
"PathPattern": "/%frameworkId%/%libId%/themes/%themeId%/%fileId%.css",
381+
"Extends": ["base"], // TODO: Read from .theming?
382+
"Scopes": scopes,
383+
"Engine": {
384+
"Version": LESS_OPENUI5_VERSION,
385+
"Name": "less-openui5"
386+
},
387+
"Version": {
388+
"Build": "<TODO>", // TOOD: add new property options.library.version
389+
"Source": "<TODO>" // TOOD: add new property options.library.version
352390
}
391+
});
392+
393+
const additionalVariables = `
394+
:root {
395+
--sapUiTheme-${libraryNameDashed}: true;
396+
--sapThemeMetaData-UI5-${libraryNameDashed}: ${metadataJson};
397+
}
398+
`;
399+
result.css = additionalVariables + result.cssVariables + "\n" + result.css;
400+
if (options.rtl) {
401+
result.cssRtl = additionalVariables + result.cssVariables + "\n" + result.cssRtl;
353402
}
354-
resolve(result);
355-
});
403+
}
404+
405+
return result;
356406
}
357407

358408
function getScopeVariables(options) {

0 commit comments

Comments
 (0)