Skip to content

[FEATURE] Improved support for CSS Variables #168

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
323 changes: 198 additions & 125 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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];
};
Expand Down Expand Up @@ -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 }}
Expand All @@ -128,7 +141,9 @@ Builder.prototype.build = function(options) {
parser: {},
compiler: {},
library: {},
scope: {}
scope: {},
inlineThemingParameters: true,
inlineCssVariables: false,
}, options);

if (options.compiler.sourceMap) {
Expand Down Expand Up @@ -178,7 +193,7 @@ Builder.prototype.build = function(options) {
});
}

function compile(config) {
async function compile(config) {
const parserOptions = clone(options.parser);
let rootpath;

Expand Down Expand Up @@ -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 = "<theme-name>";

// 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: "<TODO>", // TOOD: add new property options.library.version
// Source: "<TODO>" // 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) {
Expand Down Expand Up @@ -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"
};
}
Loading