less
Version:
Leaner CSS
133 lines (117 loc) • 4.36 kB
JavaScript
// @ts-check
/** @import { EvalContext, CSSOutput, TreeVisitor, FileInfo } from './node.js' */
import Node from './node.js';
import Anonymous from './anonymous.js';
import FunctionCaller from '../functions/function-caller.js';
//
// A function call node.
//
class Call extends Node {
get type() { return 'Call'; }
/**
* @param {string} name
* @param {Node[]} args
* @param {number} index
* @param {FileInfo} currentFileInfo
*/
constructor(name, args, index, currentFileInfo) {
super();
this.name = name;
this.args = args;
this.calc = name === 'calc';
this._index = index;
this._fileInfo = currentFileInfo;
}
/** @param {TreeVisitor} visitor */
accept(visitor) {
if (this.args) {
this.args = visitor.visitArray(this.args);
}
}
//
// When evaluating a function call,
// we either find the function in the functionRegistry,
// in which case we call it, passing the evaluated arguments,
// if this returns null or we cannot find the function, we
// simply print it out as it appeared originally [2].
//
// The reason why we evaluate the arguments, is in the case where
// we try to pass a variable to a function, like: `saturate(@color)`.
// The function should receive the value, not the variable.
//
/**
* @param {EvalContext} context
* @returns {Node}
*/
eval(context) {
/**
* Turn off math for calc(), and switch back on for evaluating nested functions
*/
const currentMathContext = context.mathOn;
context.mathOn = !this.calc;
if (this.calc || context.inCalc) {
context.enterCalc();
}
const exitCalc = () => {
if (this.calc || context.inCalc) {
context.exitCalc();
}
context.mathOn = currentMathContext;
};
/** @type {Node | string | boolean | null | undefined} */
let result;
const funcCaller = new FunctionCaller(this.name, context, this.getIndex(), this.fileInfo());
if (funcCaller.isValid()) {
try {
result = funcCaller.call(this.args);
exitCalc();
} catch (e) {
// eslint-disable-next-line no-prototype-builtins
if (/** @type {Record<string, unknown>} */ (e).hasOwnProperty('line') && /** @type {Record<string, unknown>} */ (e).hasOwnProperty('column')) {
throw e;
}
throw {
type: /** @type {Record<string, string>} */ (e).type || 'Runtime',
message: `Error evaluating function \`${this.name}\`${/** @type {Error} */ (e).message ? `: ${/** @type {Error} */ (e).message}` : ''}`,
index: this.getIndex(),
filename: this.fileInfo().filename,
line: /** @type {Record<string, number>} */ (e).lineNumber,
column: /** @type {Record<string, number>} */ (e).columnNumber
};
}
}
if (result !== null && result !== undefined) {
// Results that that are not nodes are cast as Anonymous nodes
// Falsy values or booleans are returned as empty nodes
if (!(result instanceof Node)) {
if (!result || result === true) {
result = new Anonymous(null);
}
else {
result = new Anonymous(result.toString());
}
}
result._index = this._index;
result._fileInfo = this._fileInfo;
return result;
}
const args = this.args.map(a => a.eval(context));
exitCalc();
return new Call(this.name, args, this.getIndex(), this.fileInfo());
}
/**
* @param {EvalContext} context
* @param {CSSOutput} output
*/
genCSS(context, output) {
output.add(`${this.name}(`, this.fileInfo(), this.getIndex());
for (let i = 0; i < this.args.length; i++) {
this.args[i].genCSS(context, output);
if (i + 1 < this.args.length) {
output.add(', ');
}
}
output.add(')');
}
}
export default Call;