UNPKG

less-openui5

Version:

Build OpenUI5 themes with Less.js

230 lines (199 loc) 7.2 kB
"use strict"; const less = require("../thirdparty/less"); const CSSVariablesCollectorPlugin = module.exports = function(config) { this.config = config; // eslint-disable-next-line new-cap this.native = new less.tree.visitor(this); this.vars = {}; this.calcVars = {}; this.ruleStack = []; this.mixinStack = []; this.parenStack = []; this.fontFaceDirectiveStack = []; }; CSSVariablesCollectorPlugin.prototype = { // needed to keep the less variable references intact to use this info for the CSS variables references isPreEvalVisitor: true, isReplacing: true, _isInMixinOrParenOrFontFaceDirective() { return this.mixinStack.length > 0 || this.parenStack.length > 0 || this.fontFaceDirectiveStack.length > 0; }, _isVarInRule() { return this.ruleStack.length > 0 && !this.ruleStack[this.ruleStack.length - 1].variable; }, _isVarInLibrary({filename} = {}) { // for libraries we check that the file is within the libraries theme path // in all other cases with no filename (indicates calculated variables) // or in case of variables in standalone less files we just include them! const regex = new RegExp(`(^|/)${this.config.libPath}/themes/`); const include = !filename || (this.config.libPath ? regex.test(filename) : true); return include; }, _isRelevant() { return !this._isInMixinOrParenOrFontFaceDirective() && this._isVarInRule(); }, toLessVariables(varsOverride) { const vars = {}; Object.keys(this.vars).forEach((variableName) => { const override = this.vars[variableName].updateAfterEval && varsOverride[variableName] !== undefined; vars[variableName] = { css: override ? varsOverride[variableName] : this.vars[variableName].css, export: this.vars[variableName].export }; }); let lessVariables = ""; Object.keys(vars).forEach((variableName) => { const variableValue = vars[variableName].css; lessVariables += `@${variableName}: ${variableValue};\n`; }); Object.keys(this.calcVars).forEach((variableName) => { const variableValue = this.calcVars[variableName].css; lessVariables += `@${variableName}: ${variableValue};\n`; }); lessVariables += "\n:root {\n"; Object.keys(vars).forEach((variableName) => { if (vars[variableName].export) { lessVariables += `--${variableName}: @${variableName};\n`; } }); Object.keys(this.calcVars).forEach((variableName) => { if (this.calcVars[variableName].export) { lessVariables += `--${variableName}: @${variableName};\n`; } }); lessVariables += "}\n"; return lessVariables; }, _getCSS(node) { let css = ""; // override: do not evaluate variables less.tree.Variable.prototype.genCSS = function(env, output) { new less.tree.Anonymous(this.name, this.index, this.currentFileInfo, this.mapLines).genCSS(env, output); }; // override: keep quoting for less variables const fnQuotedgenCSS = less.tree.Quoted.prototype.genCSS; less.tree.Quoted.prototype.genCSS = function(env, output) { new less.tree.Anonymous((this.escaped ? "~" : "") + this.quote + this.value + this.quote, this.index, this.currentFileInfo, this.mapLines).genCSS(env, output); }; // add the variable declaration to the list of vars css = node.toCSS(); // reset overrides less.tree.Variable.prototype.genCSS = undefined; less.tree.Quoted.prototype.genCSS = fnQuotedgenCSS; return css; }, run(root) { return this.native.visit(root); }, visitOperation(node, visitArgs) { if (this._isRelevant()) { // console.log("visitOperation", this.ruleStack[this.ruleStack.length - 1], this._getCSS(node)); return new less.tree.Call("calc", [new less.tree.Expression([node.operands[0], new less.tree.Anonymous(node.op), node.operands[1]])]); } return node; }, visitCall(node, visitArgs) { // if variables are used inside rules, generate a new calculated variable for it! const isRelevantFunction = typeof less.tree.functions[node.name] === "function" && ["rgba"].indexOf(node.name) === -1; if (this._isRelevant() && isRelevantFunction) { // console.log("visitCall", this.ruleStack[this.ruleStack.length - 1], this._getCSS(node)); const css = this._getCSS(node); let newName = this.config.prefix + "function_" + node.name + Object.keys(this.vars).length; // check for duplicate value in vars already for (const name in this.calcVars) { if (this.calcVars[name].css === css) { newName = name; break; } } this.calcVars[newName] = { css: css, export: this._isVarInLibrary() }; return new less.tree.Call("var", [new less.tree.Anonymous("--" + newName, node.index, node.currentFileInfo, node.mapLines)]); } return node; }, visitNegative(node, visitArgs) { // convert negative into calc function if (this._isRelevant()) { // console.log("visitNegative", this.ruleStack[this.ruleStack.length - 1], this._getCSS(node)); return new less.tree.Call("calc", [new less.tree.Expression([new less.tree.Anonymous("-1"), new less.tree.Anonymous("*"), node.value])]); } return node; }, visitVariable(node, visitArgs) { // convert less variables into CSS variables if (this._isRelevant()) { return new less.tree.Call("var", [new less.tree.Anonymous(node.name.replace(/^@/, "--"), node.index, node.currentFileInfo, node.mapLines)]); } return node; }, visitRule(node, visitArgs) { // check rule for being a variable declaration const isVarDeclaration = typeof node.name === "string" && node.name.startsWith("@"); if (!this._isInMixinOrParenOrFontFaceDirective() && 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] = { css: this._getCSS(node.value), export: isVarInLib }; } // store the rule context for the call variable extraction this.ruleStack.push(node); return node; }, visitRuleOut(node) { // remove rule context this.ruleStack.pop(); return node; }, visitMixinDefinition(node, visitArgs) { // store the mixin context this.mixinStack.push(node); return node; }, visitMixinDefinitionOut(node) { // remove mixin context this.mixinStack.pop(); return node; }, visitParen(node, visitArgs) { // store the parenthesis context this.parenStack.push(node); return node; }, visitParenOut(node) { // remove parenthesis context this.parenStack.pop(); return node; }, visitDirective(node, visitArgs) { // store the @font-face directive context if (node.name === "@font-face") { this.fontFaceDirectiveStack.push(node); } return node; }, visitDirectiveOut(node) { // remove @font-face directive context if (node.name === "@font-face") { this.fontFaceDirectiveStack.pop(); } return node; }, visitUrl(node, visitArgs) { // we mark the less variables which should be updated after eval // => strangewise less variables with "none" values are also urls // after the less variables have been evaluated if (this.ruleStack.length > 0 && this.ruleStack[0].variable) { this.vars[this.ruleStack[0].name.substr(1)].updateAfterEval = true; } return node; } };