diff --git a/lib/less/browser.js b/lib/less/browser.js index 2023c546..aed3d999 100644 --- a/lib/less/browser.js +++ b/lib/less/browser.js @@ -54,11 +54,7 @@ less.poll = less.poll || (isFileProtocol ? 1000 : 1500); //Setup user functions if (options.functions) { - for(var func in options.functions) { - if (options.functions.hasOwnProperty(func)) { - less.tree.functions[func] = options.functions[func]; - } - } + less.functions.functionRegistry.addMultiple(options.functions); } var dumpLineNumbers = /!dumpLineNumbers:(comments|mediaquery|all)/.exec(location.hash); @@ -352,7 +348,7 @@ function loadStyleSheet(sheet, callback, reload, remaining, modifyVars) { } less.environment.loadFile(env, sheet.href, null, function loadInitialFileCallback(e, data, path, webInfo) { - + var newFileInfo = { currentDirectory: less.environment.getPath(env, path), filename: path, diff --git a/lib/less/functions.js b/lib/less/functions.js deleted file mode 100644 index e55f6af6..00000000 --- a/lib/less/functions.js +++ /dev/null @@ -1,736 +0,0 @@ -module.exports = function (less, tree) { - -var functions = { - rgb: function (r, g, b) { - return this.rgba(r, g, b, 1.0); - }, - rgba: function (r, g, b, a) { - var rgb = [r, g, b].map(function (c) { return scaled(c, 255); }); - a = number(a); - return new(tree.Color)(rgb, a); - }, - hsl: function (h, s, l) { - return this.hsla(h, s, l, 1.0); - }, - hsla: function (h, s, l, a) { - function hue(h) { - h = h < 0 ? h + 1 : (h > 1 ? h - 1 : h); - if (h * 6 < 1) { return m1 + (m2 - m1) * h * 6; } - else if (h * 2 < 1) { return m2; } - else if (h * 3 < 2) { return m1 + (m2 - m1) * (2/3 - h) * 6; } - else { return m1; } - } - - h = (number(h) % 360) / 360; - s = clamp(number(s)); l = clamp(number(l)); a = clamp(number(a)); - - var m2 = l <= 0.5 ? l * (s + 1) : l + s - l * s; - var m1 = l * 2 - m2; - - return this.rgba(hue(h + 1/3) * 255, - hue(h) * 255, - hue(h - 1/3) * 255, - a); - }, - - hsv: function(h, s, v) { - return this.hsva(h, s, v, 1.0); - }, - - hsva: function(h, s, v, a) { - h = ((number(h) % 360) / 360) * 360; - s = number(s); v = number(v); a = number(a); - - var i, f; - i = Math.floor((h / 60) % 6); - f = (h / 60) - i; - - var vs = [v, - v * (1 - s), - v * (1 - f * s), - v * (1 - (1 - f) * s)]; - var perm = [[0, 3, 1], - [2, 0, 1], - [1, 0, 3], - [1, 2, 0], - [3, 1, 0], - [0, 1, 2]]; - - return this.rgba(vs[perm[i][0]] * 255, - vs[perm[i][1]] * 255, - vs[perm[i][2]] * 255, - a); - }, - - hue: function (color) { - return new(tree.Dimension)(color.toHSL().h); - }, - saturation: function (color) { - return new(tree.Dimension)(color.toHSL().s * 100, '%'); - }, - lightness: function (color) { - return new(tree.Dimension)(color.toHSL().l * 100, '%'); - }, - hsvhue: function(color) { - return new(tree.Dimension)(color.toHSV().h); - }, - hsvsaturation: function (color) { - return new(tree.Dimension)(color.toHSV().s * 100, '%'); - }, - hsvvalue: function (color) { - return new(tree.Dimension)(color.toHSV().v * 100, '%'); - }, - red: function (color) { - return new(tree.Dimension)(color.rgb[0]); - }, - green: function (color) { - return new(tree.Dimension)(color.rgb[1]); - }, - blue: function (color) { - return new(tree.Dimension)(color.rgb[2]); - }, - alpha: function (color) { - return new(tree.Dimension)(color.toHSL().a); - }, - luma: function (color) { - return new(tree.Dimension)(color.luma() * color.alpha * 100, '%'); - }, - luminance: function (color) { - var luminance = - (0.2126 * color.rgb[0] / 255) - + (0.7152 * color.rgb[1] / 255) - + (0.0722 * color.rgb[2] / 255); - - return new(tree.Dimension)(luminance * color.alpha * 100, '%'); - }, - saturate: function (color, amount) { - // filter: saturate(3.2); - // should be kept as is, so check for color - if (!color.rgb) { - return null; - } - var hsl = color.toHSL(); - - hsl.s += amount.value / 100; - hsl.s = clamp(hsl.s); - return hsla(hsl); - }, - desaturate: function (color, amount) { - var hsl = color.toHSL(); - - hsl.s -= amount.value / 100; - hsl.s = clamp(hsl.s); - return hsla(hsl); - }, - lighten: function (color, amount) { - var hsl = color.toHSL(); - - hsl.l += amount.value / 100; - hsl.l = clamp(hsl.l); - return hsla(hsl); - }, - darken: function (color, amount) { - var hsl = color.toHSL(); - - hsl.l -= amount.value / 100; - hsl.l = clamp(hsl.l); - return hsla(hsl); - }, - fadein: function (color, amount) { - var hsl = color.toHSL(); - - hsl.a += amount.value / 100; - hsl.a = clamp(hsl.a); - return hsla(hsl); - }, - fadeout: function (color, amount) { - var hsl = color.toHSL(); - - hsl.a -= amount.value / 100; - hsl.a = clamp(hsl.a); - return hsla(hsl); - }, - fade: function (color, amount) { - var hsl = color.toHSL(); - - hsl.a = amount.value / 100; - hsl.a = clamp(hsl.a); - return hsla(hsl); - }, - spin: function (color, amount) { - var hsl = color.toHSL(); - var hue = (hsl.h + amount.value) % 360; - - hsl.h = hue < 0 ? 360 + hue : hue; - - return hsla(hsl); - }, - // - // Copyright (c) 2006-2009 Hampton Catlin, Nathan Weizenbaum, and Chris Eppstein - // http://sass-lang.com - // - mix: function (color1, color2, weight) { - if (!weight) { - weight = new(tree.Dimension)(50); - } - var p = weight.value / 100.0; - var w = p * 2 - 1; - var a = color1.toHSL().a - color2.toHSL().a; - - var w1 = (((w * a == -1) ? w : (w + a) / (1 + w * a)) + 1) / 2.0; - var w2 = 1 - w1; - - var rgb = [color1.rgb[0] * w1 + color2.rgb[0] * w2, - color1.rgb[1] * w1 + color2.rgb[1] * w2, - color1.rgb[2] * w1 + color2.rgb[2] * w2]; - - var alpha = color1.alpha * p + color2.alpha * (1 - p); - - return new(tree.Color)(rgb, alpha); - }, - greyscale: function (color) { - return this.desaturate(color, new(tree.Dimension)(100)); - }, - contrast: function (color, dark, light, threshold) { - // filter: contrast(3.2); - // should be kept as is, so check for color - if (!color.rgb) { - return null; - } - if (typeof light === 'undefined') { - light = this.rgba(255, 255, 255, 1.0); - } - if (typeof dark === 'undefined') { - dark = this.rgba(0, 0, 0, 1.0); - } - //Figure out which is actually light and dark! - if (dark.luma() > light.luma()) { - var t = light; - light = dark; - dark = t; - } - if (typeof threshold === 'undefined') { - threshold = 0.43; - } else { - threshold = number(threshold); - } - if (color.luma() < threshold) { - return light; - } else { - return dark; - } - }, - e: function (str) { - return new(tree.Anonymous)(str instanceof tree.JavaScript ? str.evaluated : str.value); - }, - escape: function (str) { - return new(tree.Anonymous)(encodeURI(str.value).replace(/=/g, "%3D").replace(/:/g, "%3A").replace(/#/g, "%23").replace(/;/g, "%3B").replace(/\(/g, "%28").replace(/\)/g, "%29")); - }, - replace: function (string, pattern, replacement, flags) { - var result = string.value; - - result = result.replace(new RegExp(pattern.value, flags ? flags.value : ''), replacement.value); - return new(tree.Quoted)(string.quote || '', result, string.escaped); - }, - '%': function (string /* arg, arg, ...*/) { - var args = Array.prototype.slice.call(arguments, 1), - result = string.value; - - for (var i = 0; i < args.length; i++) { - /*jshint loopfunc:true */ - result = result.replace(/%[sda]/i, function(token) { - var value = token.match(/s/i) ? args[i].value : args[i].toCSS(); - return token.match(/[A-Z]$/) ? encodeURIComponent(value) : value; - }); - } - result = result.replace(/%%/g, '%'); - return new(tree.Quoted)(string.quote || '', result, string.escaped); - }, - unit: function (val, unit) { - if(!(val instanceof tree.Dimension)) { - throw { type: "Argument", message: "the first argument to unit must be a number" + (val instanceof tree.Operation ? ". Have you forgotten parenthesis?" : "") }; - } - if (unit) { - if (unit instanceof tree.Keyword) { - unit = unit.value; - } else { - unit = unit.toCSS(); - } - } else { - unit = ""; - } - return new(tree.Dimension)(val.value, unit); - }, - convert: function (val, unit) { - return val.convertTo(unit.value); - }, - round: function (n, f) { - var fraction = typeof(f) === "undefined" ? 0 : f.value; - return _math(function(num) { return num.toFixed(fraction); }, null, n); - }, - pi: function () { - return new(tree.Dimension)(Math.PI); - }, - mod: function(a, b) { - return new(tree.Dimension)(a.value % b.value, a.unit); - }, - pow: function(x, y) { - if (typeof x === "number" && typeof y === "number") { - x = new(tree.Dimension)(x); - y = new(tree.Dimension)(y); - } else if (!(x instanceof tree.Dimension) || !(y instanceof tree.Dimension)) { - throw { type: "Argument", message: "arguments must be numbers" }; - } - - return new(tree.Dimension)(Math.pow(x.value, y.value), x.unit); - }, - _minmax: function (isMin, args) { - args = Array.prototype.slice.call(args); - switch(args.length) { - case 0: throw { type: "Argument", message: "one or more arguments required" }; - } - var i, j, current, currentUnified, referenceUnified, unit, unitStatic, unitClone, - order = [], // elems only contains original argument values. - values = {}; // key is the unit.toString() for unified tree.Dimension values, - // value is the index into the order array. - for (i = 0; i < args.length; i++) { - current = args[i]; - if (!(current instanceof tree.Dimension)) { - if(Array.isArray(args[i].value)) { - Array.prototype.push.apply(args, Array.prototype.slice.call(args[i].value)); - } - continue; - } - currentUnified = current.unit.toString() === "" && unitClone !== undefined ? new(tree.Dimension)(current.value, unitClone).unify() : current.unify(); - unit = currentUnified.unit.toString() === "" && unitStatic !== undefined ? unitStatic : currentUnified.unit.toString(); - unitStatic = unit !== "" && unitStatic === undefined || unit !== "" && order[0].unify().unit.toString() === "" ? unit : unitStatic; - unitClone = unit !== "" && unitClone === undefined ? current.unit.toString() : unitClone; - j = values[""] !== undefined && unit !== "" && unit === unitStatic ? values[""] : values[unit]; - if (j === undefined) { - if(unitStatic !== undefined && unit !== unitStatic) { - throw{ type: "Argument", message: "incompatible types" }; - } - values[unit] = order.length; - order.push(current); - continue; - } - referenceUnified = order[j].unit.toString() === "" && unitClone !== undefined ? new(tree.Dimension)(order[j].value, unitClone).unify() : order[j].unify(); - if ( isMin && currentUnified.value < referenceUnified.value || - !isMin && currentUnified.value > referenceUnified.value) { - order[j] = current; - } - } - if (order.length == 1) { - return order[0]; - } - args = order.map(function (a) { return a.toCSS(this.env); }).join(this.env.compress ? "," : ", "); - return new(tree.Anonymous)((isMin ? "min" : "max") + "(" + args + ")"); - }, - min: function () { - return this._minmax(true, arguments); - }, - max: function () { - return this._minmax(false, arguments); - }, - "get-unit": function (n) { - return new(tree.Anonymous)(n.unit); - }, - argb: function (color) { - return new(tree.Anonymous)(color.toARGB()); - }, - percentage: function (n) { - return new(tree.Dimension)(n.value * 100, '%'); - }, - color: function(c) { - if ((c instanceof tree.Quoted) && - (/^#([a-f0-9]{6}|[a-f0-9]{3})$/i.test(c.value))) { - return new(tree.Color)(c.value.slice(1)); - } - if ((c instanceof tree.Color) || (c = tree.Color.fromKeyword(c.value))) { - c.keyword = undefined; - return c; - } - throw { - type: "Argument", - message: "argument must be a color keyword or 3/6 digit hex e.g. #FFF" - }; - }, - iscolor: function (n) { - return this._isa(n, tree.Color); - }, - isnumber: function (n) { - return this._isa(n, tree.Dimension); - }, - isstring: function (n) { - return this._isa(n, tree.Quoted); - }, - iskeyword: function (n) { - return this._isa(n, tree.Keyword); - }, - isurl: function (n) { - return this._isa(n, tree.URL); - }, - ispixel: function (n) { - return this.isunit(n, 'px'); - }, - ispercentage: function (n) { - return this.isunit(n, '%'); - }, - isem: function (n) { - return this.isunit(n, 'em'); - }, - isunit: function (n, unit) { - return (n instanceof tree.Dimension) && n.unit.is(unit.value || unit) ? tree.True : tree.False; - }, - _isa: function (n, Type) { - return (n instanceof Type) ? tree.True : tree.False; - }, - tint: function(color, amount) { - return this.mix(this.rgb(255,255,255), color, amount); - }, - shade: function(color, amount) { - return this.mix(this.rgb(0, 0, 0), color, amount); - }, - extract: function(values, index) { - index = index.value - 1; // (1-based index) - // handle non-array values as an array of length 1 - // return 'undefined' if index is invalid - return Array.isArray(values.value) - ? values.value[index] : Array(values)[index]; - }, - length: function(values) { - var n = Array.isArray(values.value) ? values.value.length : 1; - return new tree.Dimension(n); - }, - - "data-uri": function(mimetypeNode, filePathNode) { - - if (!less.environment.supportsDataURI(this.env)) { - return new tree.URL(filePathNode || mimetypeNode, this.currentFileInfo).eval(this.env); - } - - var mimetype = mimetypeNode.value; - var filePath = (filePathNode && filePathNode.value); - - var useBase64 = false; - - if (arguments.length < 2) { - filePath = mimetype; - } - - var fragmentStart = filePath.indexOf('#'); - var fragment = ''; - if (fragmentStart!==-1) { - fragment = filePath.slice(fragmentStart); - filePath = filePath.slice(0, fragmentStart); - } - - if (this.env.isPathRelative(filePath)) { - if (this.currentFileInfo.relativeUrls) { - filePath = less.environment.join(this.currentFileInfo.currentDirectory, filePath); - } else { - filePath = less.environment.join(this.currentFileInfo.entryPath, filePath); - } - } - - // detect the mimetype if not given - if (arguments.length < 2) { - - mimetype = less.environment.mimeLookup(this.env, filePath); - - // use base 64 unless it's an ASCII or UTF-8 format - var charset = less.environment.charsetLookup(this.env, mimetype); - useBase64 = ['US-ASCII', 'UTF-8'].indexOf(charset) < 0; - if (useBase64) { mimetype += ';base64'; } - } - else { - useBase64 = /;base64$/.test(mimetype); - } - - var buf = less.environment.readFileSync(filePath); - - // IE8 cannot handle a data-uri larger than 32KB. If this is exceeded - // and the --ieCompat flag is enabled, return a normal url() instead. - var DATA_URI_MAX_KB = 32, - fileSizeInKB = parseInt((buf.length / 1024), 10); - if (fileSizeInKB >= DATA_URI_MAX_KB) { - - if (this.env.ieCompat !== false) { - if (!this.env.silent) { - console.warn("Skipped data-uri embedding of %s because its size (%dKB) exceeds IE8-safe %dKB!", filePath, fileSizeInKB, DATA_URI_MAX_KB); - } - - return new tree.URL(filePathNode || mimetypeNode, this.currentFileInfo).eval(this.env); - } - } - - buf = useBase64 ? buf.toString('base64') - : encodeURIComponent(buf); - - var uri = "\"data:" + mimetype + ',' + buf + fragment + "\""; - return new(tree.URL)(new(tree.Anonymous)(uri)); - }, - - "svg-gradient": function(direction) { - - function throwArgumentDescriptor() { - throw { type: "Argument", message: "svg-gradient expects direction, start_color [start_position], [color position,]..., end_color [end_position]" }; - } - - if (arguments.length < 3) { - throwArgumentDescriptor(); - } - var stops = Array.prototype.slice.call(arguments, 1), - gradientDirectionSvg, - gradientType = "linear", - rectangleDimension = 'x="0" y="0" width="1" height="1"', - useBase64 = true, - renderEnv = {compress: false}, - returner, - directionValue = direction.toCSS(renderEnv), - i, color, position, positionValue, alpha; - - switch (directionValue) { - case "to bottom": - gradientDirectionSvg = 'x1="0%" y1="0%" x2="0%" y2="100%"'; - break; - case "to right": - gradientDirectionSvg = 'x1="0%" y1="0%" x2="100%" y2="0%"'; - break; - case "to bottom right": - gradientDirectionSvg = 'x1="0%" y1="0%" x2="100%" y2="100%"'; - break; - case "to top right": - gradientDirectionSvg = 'x1="0%" y1="100%" x2="100%" y2="0%"'; - break; - case "ellipse": - case "ellipse at center": - gradientType = "radial"; - gradientDirectionSvg = 'cx="50%" cy="50%" r="75%"'; - rectangleDimension = 'x="-50" y="-50" width="101" height="101"'; - break; - default: - throw { type: "Argument", message: "svg-gradient direction must be 'to bottom', 'to right', 'to bottom right', 'to top right' or 'ellipse at center'" }; - } - returner = '' + - '' + - '<' + gradientType + 'Gradient id="gradient" gradientUnits="userSpaceOnUse" ' + gradientDirectionSvg + '>'; - - for (i = 0; i < stops.length; i+= 1) { - if (stops[i].value) { - color = stops[i].value[0]; - position = stops[i].value[1]; - } else { - color = stops[i]; - position = undefined; - } - - if (!(color instanceof tree.Color) || (!((i === 0 || i+1 === stops.length) && position === undefined) && !(position instanceof tree.Dimension))) { - throwArgumentDescriptor(); - } - positionValue = position ? position.toCSS(renderEnv) : i === 0 ? "0%" : "100%"; - alpha = color.alpha; - returner += ''; - } - returner += '' + - ''; - - if (useBase64) { - try { - returner = less.environment.encodeBase64(this.env, returner); - } catch(e) { - useBase64 = false; - } - } - - returner = "'data:image/svg+xml" + (useBase64 ? ";base64" : "") + "," + returner + "'"; - return new(tree.URL)(new(tree.Anonymous)(returner)); - } -}; - -// Math - -var mathFunctions = { - // name, unit - ceil: null, - floor: null, - sqrt: null, - abs: null, - tan: "", - sin: "", - cos: "", - atan: "rad", - asin: "rad", - acos: "rad" -}; - -function _math(fn, unit, n) { - if (!(n instanceof tree.Dimension)) { - throw { type: "Argument", message: "argument must be a number" }; - } - if (unit == null) { - unit = n.unit; - } else { - n = n.unify(); - } - return new(tree.Dimension)(fn(parseFloat(n.value)), unit); -} - -// ~ End of Math - -// Color Blending -// ref: http://www.w3.org/TR/compositing-1 - -function colorBlend(mode, color1, color2) { - var ab = color1.alpha, cb, // backdrop - as = color2.alpha, cs, // source - ar, cr, r = []; // result - - ar = as + ab * (1 - as); - for (var i = 0; i < 3; i++) { - cb = color1.rgb[i] / 255; - cs = color2.rgb[i] / 255; - cr = mode(cb, cs); - if (ar) { - cr = (as * cs + ab * (cb - - as * (cb + cs - cr))) / ar; - } - r[i] = cr * 255; - } - - return new(tree.Color)(r, ar); -} - -var colorBlendMode = { - multiply: function(cb, cs) { - return cb * cs; - }, - screen: function(cb, cs) { - return cb + cs - cb * cs; - }, - overlay: function(cb, cs) { - cb *= 2; - return (cb <= 1) - ? colorBlendMode.multiply(cb, cs) - : colorBlendMode.screen(cb - 1, cs); - }, - softlight: function(cb, cs) { - var d = 1, e = cb; - if (cs > 0.5) { - e = 1; - d = (cb > 0.25) ? Math.sqrt(cb) - : ((16 * cb - 12) * cb + 4) * cb; - } - return cb - (1 - 2 * cs) * e * (d - cb); - }, - hardlight: function(cb, cs) { - return colorBlendMode.overlay(cs, cb); - }, - difference: function(cb, cs) { - return Math.abs(cb - cs); - }, - exclusion: function(cb, cs) { - return cb + cs - 2 * cb * cs; - }, - - // non-w3c functions: - average: function(cb, cs) { - return (cb + cs) / 2; - }, - negation: function(cb, cs) { - return 1 - Math.abs(cb + cs - 1); - } -}; - -// ~ End of Color Blending - -tree.defaultFunc = { - eval: function () { - var v = this.value_, e = this.error_; - if (e) { - throw e; - } - if (v != null) { - return v ? tree.True : tree.False; - } - }, - value: function (v) { - this.value_ = v; - }, - error: function (e) { - this.error_ = e; - }, - reset: function () { - this.value_ = this.error_ = null; - } -}; - -function initFunctions() { - var f; - - // math - for (f in mathFunctions) { - if (mathFunctions.hasOwnProperty(f)) { - functions[f] = _math.bind(null, Math[f], mathFunctions[f]); - } - } - - // color blending - for (f in colorBlendMode) { - if (colorBlendMode.hasOwnProperty(f)) { - functions[f] = colorBlend.bind(null, colorBlendMode[f]); - } - } - - // default - f = tree.defaultFunc; - functions["default"] = f.eval.bind(f); - -} initFunctions(); - -function hsla(color) { - return functions.hsla(color.h, color.s, color.l, color.a); -} - -function scaled(n, size) { - if (n instanceof tree.Dimension && n.unit.is('%')) { - return parseFloat(n.value * size / 100); - } else { - return number(n); - } -} - -function number(n) { - if (n instanceof tree.Dimension) { - return parseFloat(n.unit.is('%') ? n.value / 100 : n.value); - } else if (typeof(n) === 'number') { - return n; - } else { - throw { - error: "RuntimeError", - message: "color functions take numbers as parameters" - }; - } -} - -function clamp(val) { - return Math.min(1, Math.max(0, val)); -} - -tree.fround = function(env, value) { - var p = env && env.numPrecision; - //add "epsilon" to ensure numbers like 1.000000005 (represented as 1.000000004999....) are properly rounded... - return (p == null) ? value : Number((value + 2e-16).toFixed(p)); -}; - -tree.functionCall = function(env, currentFileInfo, environment) { - this.env = env; - this.environment = environment; - this.currentFileInfo = currentFileInfo; -}; - -tree.functionCall.prototype = functions; - -return functions; - -}; diff --git a/lib/less/functions/color-blending.js b/lib/less/functions/color-blending.js new file mode 100644 index 00000000..c24b549a --- /dev/null +++ b/lib/less/functions/color-blending.js @@ -0,0 +1,74 @@ +module.exports = function(functions, tree) { + + // Color Blending + // ref: http://www.w3.org/TR/compositing-1 + + function colorBlend(mode, color1, color2) { + var ab = color1.alpha, cb, // backdrop + as = color2.alpha, cs, // source + ar, cr, r = []; // result + + ar = as + ab * (1 - as); + for (var i = 0; i < 3; i++) { + cb = color1.rgb[i] / 255; + cs = color2.rgb[i] / 255; + cr = mode(cb, cs); + if (ar) { + cr = (as * cs + ab * (cb + - as * (cb + cs - cr))) / ar; + } + r[i] = cr * 255; + } + + return new(tree.Color)(r, ar); + } + + var colorBlendModeFunctions = { + multiply: function(cb, cs) { + return cb * cs; + }, + screen: function(cb, cs) { + return cb + cs - cb * cs; + }, + overlay: function(cb, cs) { + cb *= 2; + return (cb <= 1) + ? colorBlendModeFunctions.multiply(cb, cs) + : colorBlendModeFunctions.screen(cb - 1, cs); + }, + softlight: function(cb, cs) { + var d = 1, e = cb; + if (cs > 0.5) { + e = 1; + d = (cb > 0.25) ? Math.sqrt(cb) + : ((16 * cb - 12) * cb + 4) * cb; + } + return cb - (1 - 2 * cs) * e * (d - cb); + }, + hardlight: function(cb, cs) { + return colorBlendModeFunctions.overlay(cs, cb); + }, + difference: function(cb, cs) { + return Math.abs(cb - cs); + }, + exclusion: function(cb, cs) { + return cb + cs - 2 * cb * cs; + }, + + // non-w3c functions: + average: function(cb, cs) { + return (cb + cs) / 2; + }, + negation: function(cb, cs) { + return 1 - Math.abs(cb + cs - 1); + } + }; + + for (var f in colorBlendModeFunctions) { + if (colorBlendModeFunctions.hasOwnProperty(f)) { + colorBlend[f] = colorBlend.bind(null, colorBlendModeFunctions[f]); + } + } + + functions.functionRegistry.addMultiple(colorBlend); +}; diff --git a/lib/less/functions/color.js b/lib/less/functions/color.js new file mode 100644 index 00000000..1b493bce --- /dev/null +++ b/lib/less/functions/color.js @@ -0,0 +1,272 @@ +module.exports = function(functions, tree) { + function clamp(val) { + return Math.min(1, Math.max(0, val)); + } + function hsla(color) { + return colorFunctions.hsla(color.h, color.s, color.l, color.a); + } + function number(n) { + if (n instanceof tree.Dimension) { + return parseFloat(n.unit.is('%') ? n.value / 100 : n.value); + } else if (typeof(n) === 'number') { + return n; + } else { + throw { + error: "RuntimeError", + message: "color functions take numbers as parameters" + }; + } + } + function scaled(n, size) { + if (n instanceof tree.Dimension && n.unit.is('%')) { + return parseFloat(n.value * size / 100); + } else { + return number(n); + } + } + var colorFunctions = { + rgb: function (r, g, b) { + return colorFunctions.rgba(r, g, b, 1.0); + }, + rgba: function (r, g, b, a) { + var rgb = [r, g, b].map(function (c) { return scaled(c, 255); }); + a = number(a); + return new(tree.Color)(rgb, a); + }, + hsl: function (h, s, l) { + return colorFunctions.hsla(h, s, l, 1.0); + }, + hsla: function (h, s, l, a) { + function hue(h) { + h = h < 0 ? h + 1 : (h > 1 ? h - 1 : h); + if (h * 6 < 1) { return m1 + (m2 - m1) * h * 6; } + else if (h * 2 < 1) { return m2; } + else if (h * 3 < 2) { return m1 + (m2 - m1) * (2/3 - h) * 6; } + else { return m1; } + } + + h = (number(h) % 360) / 360; + s = clamp(number(s)); l = clamp(number(l)); a = clamp(number(a)); + + var m2 = l <= 0.5 ? l * (s + 1) : l + s - l * s; + var m1 = l * 2 - m2; + + return colorFunctions.rgba(hue(h + 1/3) * 255, + hue(h) * 255, + hue(h - 1/3) * 255, + a); + }, + + hsv: function(h, s, v) { + return colorFunctions.hsva(h, s, v, 1.0); + }, + + hsva: function(h, s, v, a) { + h = ((number(h) % 360) / 360) * 360; + s = number(s); v = number(v); a = number(a); + + var i, f; + i = Math.floor((h / 60) % 6); + f = (h / 60) - i; + + var vs = [v, + v * (1 - s), + v * (1 - f * s), + v * (1 - (1 - f) * s)]; + var perm = [[0, 3, 1], + [2, 0, 1], + [1, 0, 3], + [1, 2, 0], + [3, 1, 0], + [0, 1, 2]]; + + return colorFunctions.rgba(vs[perm[i][0]] * 255, + vs[perm[i][1]] * 255, + vs[perm[i][2]] * 255, + a); + }, + + hue: function (color) { + return new(tree.Dimension)(color.toHSL().h); + }, + saturation: function (color) { + return new(tree.Dimension)(color.toHSL().s * 100, '%'); + }, + lightness: function (color) { + return new(tree.Dimension)(color.toHSL().l * 100, '%'); + }, + hsvhue: function(color) { + return new(tree.Dimension)(color.toHSV().h); + }, + hsvsaturation: function (color) { + return new(tree.Dimension)(color.toHSV().s * 100, '%'); + }, + hsvvalue: function (color) { + return new(tree.Dimension)(color.toHSV().v * 100, '%'); + }, + red: function (color) { + return new(tree.Dimension)(color.rgb[0]); + }, + green: function (color) { + return new(tree.Dimension)(color.rgb[1]); + }, + blue: function (color) { + return new(tree.Dimension)(color.rgb[2]); + }, + alpha: function (color) { + return new(tree.Dimension)(color.toHSL().a); + }, + luma: function (color) { + return new(tree.Dimension)(color.luma() * color.alpha * 100, '%'); + }, + luminance: function (color) { + var luminance = + (0.2126 * color.rgb[0] / 255) + + (0.7152 * color.rgb[1] / 255) + + (0.0722 * color.rgb[2] / 255); + + return new(tree.Dimension)(luminance * color.alpha * 100, '%'); + }, + saturate: function (color, amount) { + // filter: saturate(3.2); + // should be kept as is, so check for color + if (!color.rgb) { + return null; + } + var hsl = color.toHSL(); + + hsl.s += amount.value / 100; + hsl.s = clamp(hsl.s); + return hsla(hsl); + }, + desaturate: function (color, amount) { + var hsl = color.toHSL(); + + hsl.s -= amount.value / 100; + hsl.s = clamp(hsl.s); + return hsla(hsl); + }, + lighten: function (color, amount) { + var hsl = color.toHSL(); + + hsl.l += amount.value / 100; + hsl.l = clamp(hsl.l); + return hsla(hsl); + }, + darken: function (color, amount) { + var hsl = color.toHSL(); + + hsl.l -= amount.value / 100; + hsl.l = clamp(hsl.l); + return hsla(hsl); + }, + fadein: function (color, amount) { + var hsl = color.toHSL(); + + hsl.a += amount.value / 100; + hsl.a = clamp(hsl.a); + return hsla(hsl); + }, + fadeout: function (color, amount) { + var hsl = color.toHSL(); + + hsl.a -= amount.value / 100; + hsl.a = clamp(hsl.a); + return hsla(hsl); + }, + fade: function (color, amount) { + var hsl = color.toHSL(); + + hsl.a = amount.value / 100; + hsl.a = clamp(hsl.a); + return hsla(hsl); + }, + spin: function (color, amount) { + var hsl = color.toHSL(); + var hue = (hsl.h + amount.value) % 360; + + hsl.h = hue < 0 ? 360 + hue : hue; + + return hsla(hsl); + }, + // + // Copyright (c) 2006-2009 Hampton Catlin, Nathan Weizenbaum, and Chris Eppstein + // http://sass-lang.com + // + mix: function (color1, color2, weight) { + if (!weight) { + weight = new(tree.Dimension)(50); + } + var p = weight.value / 100.0; + var w = p * 2 - 1; + var a = color1.toHSL().a - color2.toHSL().a; + + var w1 = (((w * a == -1) ? w : (w + a) / (1 + w * a)) + 1) / 2.0; + var w2 = 1 - w1; + + var rgb = [color1.rgb[0] * w1 + color2.rgb[0] * w2, + color1.rgb[1] * w1 + color2.rgb[1] * w2, + color1.rgb[2] * w1 + color2.rgb[2] * w2]; + + var alpha = color1.alpha * p + color2.alpha * (1 - p); + + return new(tree.Color)(rgb, alpha); + }, + greyscale: function (color) { + return colorFunctions.desaturate(color, new(tree.Dimension)(100)); + }, + contrast: function (color, dark, light, threshold) { + // filter: contrast(3.2); + // should be kept as is, so check for color + if (!color.rgb) { + return null; + } + if (typeof light === 'undefined') { + light = colorFunctions.rgba(255, 255, 255, 1.0); + } + if (typeof dark === 'undefined') { + dark = colorFunctions.rgba(0, 0, 0, 1.0); + } + //Figure out which is actually light and dark! + if (dark.luma() > light.luma()) { + var t = light; + light = dark; + dark = t; + } + if (typeof threshold === 'undefined') { + threshold = 0.43; + } else { + threshold = number(threshold); + } + if (color.luma() < threshold) { + return light; + } else { + return dark; + } + }, + argb: function (color) { + return new(tree.Anonymous)(color.toARGB()); + }, + color: function(c) { + if ((c instanceof tree.Quoted) && + (/^#([a-f0-9]{6}|[a-f0-9]{3})$/i.test(c.value))) { + return new(tree.Color)(c.value.slice(1)); + } + if ((c instanceof tree.Color) || (c = tree.Color.fromKeyword(c.value))) { + c.keyword = undefined; + return c; + } + throw { + type: "Argument", + message: "argument must be a color keyword or 3/6 digit hex e.g. #FFF" + }; + }, + tint: function(color, amount) { + return colorFunctions.mix(colorFunctions.rgb(255,255,255), color, amount); + }, + shade: function(color, amount) { + return colorFunctions.mix(colorFunctions.rgb(0, 0, 0), color, amount); + } + }; + functions.functionRegistry.addMultiple(colorFunctions); +}; diff --git a/lib/less/functions/data-uri.js b/lib/less/functions/data-uri.js new file mode 100644 index 00000000..c60be3aa --- /dev/null +++ b/lib/less/functions/data-uri.js @@ -0,0 +1,69 @@ +module.exports = function(functions, tree, less) { + functions.functionRegistry.add("data-uri", function(mimetypeNode, filePathNode) { + + if (!less.environment.supportsDataURI(this.env)) { + return new tree.URL(filePathNode || mimetypeNode, this.currentFileInfo).eval(this.env); + } + + var mimetype = mimetypeNode.value; + var filePath = (filePathNode && filePathNode.value); + + var useBase64 = false; + + if (arguments.length < 2) { + filePath = mimetype; + } + + var fragmentStart = filePath.indexOf('#'); + var fragment = ''; + if (fragmentStart!==-1) { + fragment = filePath.slice(fragmentStart); + filePath = filePath.slice(0, fragmentStart); + } + + if (this.env.isPathRelative(filePath)) { + if (this.currentFileInfo.relativeUrls) { + filePath = less.environment.join(this.currentFileInfo.currentDirectory, filePath); + } else { + filePath = less.environment.join(this.currentFileInfo.entryPath, filePath); + } + } + + // detect the mimetype if not given + if (arguments.length < 2) { + + mimetype = less.environment.mimeLookup(this.env, filePath); + + // use base 64 unless it's an ASCII or UTF-8 format + var charset = less.environment.charsetLookup(this.env, mimetype); + useBase64 = ['US-ASCII', 'UTF-8'].indexOf(charset) < 0; + if (useBase64) { mimetype += ';base64'; } + } + else { + useBase64 = /;base64$/.test(mimetype); + } + + var buf = less.environment.readFileSync(filePath); + + // IE8 cannot handle a data-uri larger than 32KB. If this is exceeded + // and the --ieCompat flag is enabled, return a normal url() instead. + var DATA_URI_MAX_KB = 32, + fileSizeInKB = parseInt((buf.length / 1024), 10); + if (fileSizeInKB >= DATA_URI_MAX_KB) { + + if (this.env.ieCompat !== false) { + if (!this.env.silent) { + console.warn("Skipped data-uri embedding of %s because its size (%dKB) exceeds IE8-safe %dKB!", filePath, fileSizeInKB, DATA_URI_MAX_KB); + } + + return new tree.URL(filePathNode || mimetypeNode, this.currentFileInfo).eval(this.env); + } + } + + buf = useBase64 ? buf.toString('base64') + : encodeURIComponent(buf); + + var uri = "\"data:" + mimetype + ',' + buf + fragment + "\""; + return new(tree.URL)(new(tree.Anonymous)(uri)); + }); +}; diff --git a/lib/less/functions/default.js b/lib/less/functions/default.js new file mode 100644 index 00000000..6aa78aaa --- /dev/null +++ b/lib/less/functions/default.js @@ -0,0 +1,25 @@ +module.exports = function(functions, tree) { + var defaultFunc = { + eval: function () { + var v = this.value_, e = this.error_; + if (e) { + throw e; + } + if (v != null) { + return v ? tree.True : tree.False; + } + }, + value: function (v) { + this.value_ = v; + }, + error: function (e) { + this.error_ = e; + }, + reset: function () { + this.value_ = this.error_ = null; + } + }; + + functions.functionRegistry.add("default", defaultFunc.eval.bind(defaultFunc)); + tree.defaultFunc = defaultFunc; +}; diff --git a/lib/less/functions/function-caller.js b/lib/less/functions/function-caller.js new file mode 100644 index 00000000..da86d513 --- /dev/null +++ b/lib/less/functions/function-caller.js @@ -0,0 +1,15 @@ +module.exports = function(functions) { +var functionCaller = function(name, env, currentFileInfo) { + this.name = name.toLowerCase(); + this.function = functions.functionRegistry.get(this.name); + this.env = env; + this.currentFileInfo = currentFileInfo; +}; +functionCaller.prototype.isValid = function() { + return Boolean(this.function); +}; +functionCaller.prototype.call = function(args) { + return this.function.apply(this, args); +}; +return functionCaller; +}; diff --git a/lib/less/functions/function-registry.js b/lib/less/functions/function-registry.js new file mode 100644 index 00000000..d39230d1 --- /dev/null +++ b/lib/less/functions/function-registry.js @@ -0,0 +1,18 @@ +module.exports = { + _data: {}, + add: function(name, func) { + if (this._data.hasOwnProperty(name)) { + //TODO warn + } + this._data[name] = func; + }, + addMultiple: function(functions) { + Object.keys(functions).forEach( + function(name) { + this.add(name, functions[name]); + }.bind(this)); + }, + get: function(name) { + return this._data[name]; + } +}; diff --git a/lib/less/functions/index.js b/lib/less/functions/index.js new file mode 100644 index 00000000..36d71676 --- /dev/null +++ b/lib/less/functions/index.js @@ -0,0 +1,18 @@ +module.exports = function(less, tree) { + var functions = {}; + functions.functionRegistry = require("./function-registry.js"); + functions.functionCaller = require("./function-caller.js")(functions); + + //register functions + require("./color.js")(functions, tree); + require("./color-blending.js")(functions, tree); + require("./data-uri.js")(functions, tree, less); + require("./default.js")(functions, tree); + require("./math.js")(functions, tree); + require("./number.js")(functions, tree); + require("./string.js")(functions, tree); + require("./svg.js")(functions, tree, less); + require("./types.js")(functions, tree); + + return functions; +}; diff --git a/lib/less/functions/math.js b/lib/less/functions/math.js new file mode 100644 index 00000000..4e56d8d2 --- /dev/null +++ b/lib/less/functions/math.js @@ -0,0 +1,42 @@ +module.exports = function(functions, tree) { + + var mathFunctions = { + // name, unit + ceil: null, + floor: null, + sqrt: null, + abs: null, + tan: "", + sin: "", + cos: "", + atan: "rad", + asin: "rad", + acos: "rad" + }; + + function _math(fn, unit, n) { + if (!(n instanceof tree.Dimension)) { + throw { type: "Argument", message: "argument must be a number" }; + } + if (unit == null) { + unit = n.unit; + } else { + n = n.unify(); + } + return new(tree.Dimension)(fn(parseFloat(n.value)), unit); + } + + for (var f in mathFunctions) { + if (mathFunctions.hasOwnProperty(f)) { + mathFunctions[f] = _math.bind(null, Math[f], mathFunctions[f]); + } + } + + mathFunctions.round = function (n, f) { + var fraction = typeof(f) === "undefined" ? 0 : f.value; + return _math(function(num) { return num.toFixed(fraction); }, null, n); + }; + + functions.functionRegistry.addMultiple(mathFunctions); + +}; diff --git a/lib/less/functions/number.js b/lib/less/functions/number.js new file mode 100644 index 00000000..dc6fdfd5 --- /dev/null +++ b/lib/less/functions/number.js @@ -0,0 +1,74 @@ +module.exports = function(functions, tree) { + var minMax = function (isMin, args) { + args = Array.prototype.slice.call(args); + switch(args.length) { + case 0: throw { type: "Argument", message: "one or more arguments required" }; + } + var i, j, current, currentUnified, referenceUnified, unit, unitStatic, unitClone, + order = [], // elems only contains original argument values. + values = {}; // key is the unit.toString() for unified tree.Dimension values, + // value is the index into the order array. + for (i = 0; i < args.length; i++) { + current = args[i]; + if (!(current instanceof tree.Dimension)) { + if(Array.isArray(args[i].value)) { + Array.prototype.push.apply(args, Array.prototype.slice.call(args[i].value)); + } + continue; + } + currentUnified = current.unit.toString() === "" && unitClone !== undefined ? new(tree.Dimension)(current.value, unitClone).unify() : current.unify(); + unit = currentUnified.unit.toString() === "" && unitStatic !== undefined ? unitStatic : currentUnified.unit.toString(); + unitStatic = unit !== "" && unitStatic === undefined || unit !== "" && order[0].unify().unit.toString() === "" ? unit : unitStatic; + unitClone = unit !== "" && unitClone === undefined ? current.unit.toString() : unitClone; + j = values[""] !== undefined && unit !== "" && unit === unitStatic ? values[""] : values[unit]; + if (j === undefined) { + if(unitStatic !== undefined && unit !== unitStatic) { + throw{ type: "Argument", message: "incompatible types" }; + } + values[unit] = order.length; + order.push(current); + continue; + } + referenceUnified = order[j].unit.toString() === "" && unitClone !== undefined ? new(tree.Dimension)(order[j].value, unitClone).unify() : order[j].unify(); + if ( isMin && currentUnified.value < referenceUnified.value || + !isMin && currentUnified.value > referenceUnified.value) { + order[j] = current; + } + } + if (order.length == 1) { + return order[0]; + } + args = order.map(function (a) { return a.toCSS(this.env); }).join(this.env.compress ? "," : ", "); + return new(tree.Anonymous)((isMin ? "min" : "max") + "(" + args + ")"); + }; + functions.functionRegistry.addMultiple({ + min: function () { + return minMax(true, arguments); + }, + max: function () { + return minMax(false, arguments); + }, + convert: function (val, unit) { + return val.convertTo(unit.value); + }, + pi: function () { + return new(tree.Dimension)(Math.PI); + }, + mod: function(a, b) { + return new(tree.Dimension)(a.value % b.value, a.unit); + }, + pow: function(x, y) { + if (typeof x === "number" && typeof y === "number") { + x = new(tree.Dimension)(x); + y = new(tree.Dimension)(y); + } else if (!(x instanceof tree.Dimension) || !(y instanceof tree.Dimension)) { + throw { type: "Argument", message: "arguments must be numbers" }; + } + + return new(tree.Dimension)(Math.pow(x.value, y.value), x.unit); + }, + percentage: function (n) { + return new(tree.Dimension)(n.value * 100, '%'); + } + }); +}; diff --git a/lib/less/functions/string.js b/lib/less/functions/string.js new file mode 100644 index 00000000..9851c891 --- /dev/null +++ b/lib/less/functions/string.js @@ -0,0 +1,30 @@ +module.exports = function(functions, tree) { + functions.functionRegistry.addMultiple({ + e: function (str) { + return new(tree.Anonymous)(str instanceof tree.JavaScript ? str.evaluated : str.value); + }, + escape: function (str) { + return new(tree.Anonymous)(encodeURI(str.value).replace(/=/g, "%3D").replace(/:/g, "%3A").replace(/#/g, "%23").replace(/;/g, "%3B").replace(/\(/g, "%28").replace(/\)/g, "%29")); + }, + replace: function (string, pattern, replacement, flags) { + var result = string.value; + + result = result.replace(new RegExp(pattern.value, flags ? flags.value : ''), replacement.value); + return new(tree.Quoted)(string.quote || '', result, string.escaped); + }, + '%': function (string /* arg, arg, ...*/) { + var args = Array.prototype.slice.call(arguments, 1), + result = string.value; + + for (var i = 0; i < args.length; i++) { + /*jshint loopfunc:true */ + result = result.replace(/%[sda]/i, function(token) { + var value = token.match(/s/i) ? args[i].value : args[i].toCSS(); + return token.match(/[A-Z]$/) ? encodeURIComponent(value) : value; + }); + } + result = result.replace(/%%/g, '%'); + return new(tree.Quoted)(string.quote || '', result, string.escaped); + } + }); +}; diff --git a/lib/less/functions/svg.js b/lib/less/functions/svg.js new file mode 100644 index 00000000..0cd71d57 --- /dev/null +++ b/lib/less/functions/svg.js @@ -0,0 +1,77 @@ +module.exports = function(functions, tree, less) { + functions.functionRegistry.add("svg-gradient", function(direction) { + + function throwArgumentDescriptor() { + throw { type: "Argument", message: "svg-gradient expects direction, start_color [start_position], [color position,]..., end_color [end_position]" }; + } + + if (arguments.length < 3) { + throwArgumentDescriptor(); + } + var stops = Array.prototype.slice.call(arguments, 1), + gradientDirectionSvg, + gradientType = "linear", + rectangleDimension = 'x="0" y="0" width="1" height="1"', + useBase64 = true, + renderEnv = {compress: false}, + returner, + directionValue = direction.toCSS(renderEnv), + i, color, position, positionValue, alpha; + + switch (directionValue) { + case "to bottom": + gradientDirectionSvg = 'x1="0%" y1="0%" x2="0%" y2="100%"'; + break; + case "to right": + gradientDirectionSvg = 'x1="0%" y1="0%" x2="100%" y2="0%"'; + break; + case "to bottom right": + gradientDirectionSvg = 'x1="0%" y1="0%" x2="100%" y2="100%"'; + break; + case "to top right": + gradientDirectionSvg = 'x1="0%" y1="100%" x2="100%" y2="0%"'; + break; + case "ellipse": + case "ellipse at center": + gradientType = "radial"; + gradientDirectionSvg = 'cx="50%" cy="50%" r="75%"'; + rectangleDimension = 'x="-50" y="-50" width="101" height="101"'; + break; + default: + throw { type: "Argument", message: "svg-gradient direction must be 'to bottom', 'to right', 'to bottom right', 'to top right' or 'ellipse at center'" }; + } + returner = '' + + '' + + '<' + gradientType + 'Gradient id="gradient" gradientUnits="userSpaceOnUse" ' + gradientDirectionSvg + '>'; + + for (i = 0; i < stops.length; i+= 1) { + if (stops[i].value) { + color = stops[i].value[0]; + position = stops[i].value[1]; + } else { + color = stops[i]; + position = undefined; + } + + if (!(color instanceof tree.Color) || (!((i === 0 || i+1 === stops.length) && position === undefined) && !(position instanceof tree.Dimension))) { + throwArgumentDescriptor(); + } + positionValue = position ? position.toCSS(renderEnv) : i === 0 ? "0%" : "100%"; + alpha = color.alpha; + returner += ''; + } + returner += '' + + ''; + + if (useBase64) { + try { + returner = less.environment.encodeBase64(this.env, returner); + } catch(e) { + useBase64 = false; + } + } + + returner = "'data:image/svg+xml" + (useBase64 ? ";base64" : "") + "," + returner + "'"; + return new(tree.URL)(new(tree.Anonymous)(returner)); + }); +}; diff --git a/lib/less/functions/types.js b/lib/less/functions/types.js new file mode 100644 index 00000000..802d8094 --- /dev/null +++ b/lib/less/functions/types.js @@ -0,0 +1,64 @@ +module.exports = function(functions, tree) { + var isa = function (n, Type) { + return (n instanceof Type) ? tree.True : tree.False; + }, + isunit = function (n, unit) { + return (n instanceof tree.Dimension) && n.unit.is(unit.value || unit) ? tree.True : tree.False; + }; + functions.functionRegistry.addMultiple({ + iscolor: function (n) { + return isa(n, tree.Color); + }, + isnumber: function (n) { + return isa(n, tree.Dimension); + }, + isstring: function (n) { + return isa(n, tree.Quoted); + }, + iskeyword: function (n) { + return isa(n, tree.Keyword); + }, + isurl: function (n) { + return isa(n, tree.URL); + }, + ispixel: function (n) { + return isunit(n, 'px'); + }, + ispercentage: function (n) { + return isunit(n, '%'); + }, + isem: function (n) { + return isunit(n, 'em'); + }, + isunit: isunit, + unit: function (val, unit) { + if(!(val instanceof tree.Dimension)) { + throw { type: "Argument", message: "the first argument to unit must be a number" + (val instanceof tree.Operation ? ". Have you forgotten parenthesis?" : "") }; + } + if (unit) { + if (unit instanceof tree.Keyword) { + unit = unit.value; + } else { + unit = unit.toCSS(); + } + } else { + unit = ""; + } + return new(tree.Dimension)(val.value, unit); + }, + "get-unit": function (n) { + return new(tree.Anonymous)(n.unit); + }, + extract: function(values, index) { + index = index.value - 1; // (1-based index) + // handle non-array values as an array of length 1 + // return 'undefined' if index is invalid + return Array.isArray(values.value) ? + values.value[index] : Array(values)[index]; + }, + length: function(values) { + var n = Array.isArray(values.value) ? values.value.length : 1; + return new tree.Dimension(n); + } + }); +}; diff --git a/lib/less/non-node-index.js b/lib/less/non-node-index.js index cacb207c..c93846df 100644 --- a/lib/less/non-node-index.js +++ b/lib/less/non-node-index.js @@ -5,10 +5,10 @@ var less = { } }; -less.tree = (require('./tree'))(less); +less.tree = (require('./tree'))(less, less.data); less.visitor = require('./visitor/index.js')(less, less.tree); less.Parser = (require('./parser'))(less, less.tree, less.visitor); -less.tree.functions = (require('./functions'))(less, less.tree); +less.functions = (require('./functions/index.js'))(less, less.tree); require('./env')(less.tree); less.tree.sourceMapOutput = require('./source-map-output.js')(less); diff --git a/lib/less/tree.js b/lib/less/tree.js index d771e3c4..b3d7b1c5 100644 --- a/lib/less/tree.js +++ b/lib/less/tree.js @@ -1,4 +1,4 @@ -module.exports = function (less) { +module.exports = function (less, data) { var tree = {}; @@ -97,7 +97,7 @@ tree.outputRuleset = function (env, output, rules) { }; tree.Alpha = require('./tree/alpha')(tree); -tree.Color = require('./tree/color')(less.data, tree); +tree.Color = require('./tree/color')(data, tree); tree.Directive = require('./tree/directive')(tree); tree.DetachedRuleset = require('./tree/detached-ruleset')(tree); tree.Operation = require('./tree/operation')(tree); @@ -113,7 +113,7 @@ tree.Selector = require('./tree/selector')(tree); tree.Quoted = require('./tree/quoted')(tree); tree.Expression = require('./tree/expression')(tree); tree.Rule = require('./tree/rule')(tree); -tree.Call = require('./tree/call')(tree); +tree.Call = require('./tree/call')(tree, less); tree.URL = require('./tree/url')(tree); tree.Import = require('./tree/import')(tree); tree.mixin = { @@ -133,6 +133,12 @@ tree.Negative = require('./tree/negative')(tree); tree.Extend = require('./tree/extend')(tree); tree.RulesetCall = require('./tree/ruleset-call')(tree); +tree.fround = function(env, value) { + var precision = env && env.numPrecision; + //add "epsilon" to ensure numbers like 1.000000005 (represented as 1.000000004999....) are properly rounded... + return (precision == null) ? value : Number((value + 2e-16).toFixed(precision)); +}; + return tree; }; diff --git a/lib/less/tree/call.js b/lib/less/tree/call.js index 951a0832..08dc9898 100644 --- a/lib/less/tree/call.js +++ b/lib/less/tree/call.js @@ -1,4 +1,4 @@ -module.exports = function (tree) { +module.exports = function (tree, less) { // // A function call node. @@ -18,7 +18,7 @@ Call.prototype = { }, // // When evaluating a function call, - // we either find the function in `tree.functions` [1], + // we either find the function in `less.functions` [1], // 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]. @@ -31,13 +31,11 @@ Call.prototype = { // eval: function (env) { var args = this.args.map(function (a) { return a.eval(env); }), - nameLC = this.name.toLowerCase(), - result, func; + result, funcCaller = new less.functions.functionCaller(this.name, env, this.currentFileInfo); - if (nameLC in tree.functions) { // 1. + if (funcCaller.isValid()) { // 1. try { - func = new tree.functionCall(env, this.currentFileInfo); - result = func[nameLC].apply(func, args); + result = funcCaller.call(args); if (result != null) { return result; } diff --git a/test/less-test.js b/test/less-test.js index 4bba05b2..ad046d1b 100644 --- a/test/less-test.js +++ b/test/less-test.js @@ -18,15 +18,17 @@ module.exports = function() { passedTests = 0; - less.tree.functions.add = function (a, b) { - return new(less.tree.Dimension)(a.value + b.value); - }; - less.tree.functions.increment = function (a) { - return new(less.tree.Dimension)(a.value + 1); - }; - less.tree.functions._color = function (str) { - if (str.value === "evil red") { return new(less.tree.Color)("600"); } - }; + less.functions.functionRegistry.addMultiple({ + add: function (a, b) { + return new(less.tree.Dimension)(a.value + b.value); + }, + increment: function (a) { + return new(less.tree.Dimension)(a.value + 1); + }, + _color: function (str) { + if (str.value === "evil red") { return new(less.tree.Color)("600"); } + } + }); function testSourcemap(name, err, compiledLess, doReplacements, sourcemap) { fs.readFile(path.join('test/', name) + '.json', 'utf8', function (e, expectedSourcemap) {