A detailed blog post about this solution can be found here.
To answer my own question and offer a workable solution
Is it possible to access individual arguments from @arguments
without resorting to string conversion?
Without LESS 1.4 (currently in beta) it doesn't seem to be possible to do this directly and get actual arguments without resorting to string conversion and then manipulate those.
If string conversion has to be done (as ~'"@{arguments}"'
) how do I split individual parameters to ignore commas within parentheses (upper complex example converts those to rgba
values)?
The answer is immediately executing anonymous function that returns desired result. Let's take example from the question:
.gradient(fade(#000, 50) 25%, #ccc 50%, fade(#fff, 90) 80%);
After executing the first line within mixin @def
is assigned this value:
@def: "rgba(0, 0, 0, 0.5) 25%, #ccc 50%, rgba(255, 255, 255, 0.9) 80%";
Now what we have to do is to replace those commas that shouldn't be split. And those are commas within parentheses. That's quite easily detectable using lookahead regular expression. So we replace those commas with semi-colons and then split on commas that were left:
val.replace(/,\s+(?=[\d ,.]+\))/gi, ";").split(/,\s*/g)
Which results in this array of strings or individual gradient parameters
["rgba(0;0;0;0.5) 25%", "#ccc 50%", "rgba(255;255;255;0.9) 80%"]
Now we have data we can work with. As it's also not possible to provide LESS mix
arguments that aren't color
objects we have to do the mixing manually.
And this is resulting .gradient
mixin that outputs #xxxxxx as a result of first and last gradient colour:
.gradient (...) {
@all: ~`"@{arguments}".replace(/[\[\]]/g,"")`;
@mix: ~`(function(a){a=a.replace(/,\s+(?=[\d ,.]+\))/gi,";").split(/,\s*/g);var f=a[0].split(/\s+/g)[0];var l=a.pop().split(/\s+/g)[0];var c=function(c){var r=[];/rgb/i.test(c)&&c.replace(/[\d.]+/g,function(i){r.push(1*i);return"";});/#.{3}$/.test(c)&&c.replace(/[\da-f]/ig,function(i){r.push(parseInt(i+i,16));return"";});/#.{6}/.test(c)&&c.replace(/[\da-f]{2}/ig,function(i){r.push(parseInt(i,16));return"";});if(r.length)return r;return[100,0,0];};var p=function(v){return("0"+v.toString(16)).match(/.{2}$/)[0];};f=c(f);l=c(l);var r={r:((f.shift()+l.shift())/2)|0,g:((f.shift()+l.shift())/2)|0,b:((f.shift()+l.shift())/2)|0};return"#"+p(r.r)+p(r.g)+p(r.b);})("@{arguments}")`;
background-color: @mix;
background-image: -webkit-linear-gradient(top, @all);
background-image: -moz-linear-gradient(top, @all);
background-image: -o-linear-gradient(top, @all);
background-image: linear-gradient(to bottom, @all);
}
We could of course complicate this even further and calculate average of all gradient colours but for my needs this is enough. The following is the function that does the trick of parsing arguments as well as calculating the mix of first and last colour in the gradient and is minified in the upper @mix
variable:
(function(args) {
args = args.replace(/,\s+(?=[\d ,.]+\))/gi, ";").split(/,\s*/g);
var first = args[0].split(/\s+/g)[0];
var last = args.pop().split(/\s+/g)[0];
var calculateValues = function(color) {
var result = [];
/rgb/i.test(color) && color.replace(/[\d.]+/g, function(i) {
result.push(1*i);
return "";
});
/#.{3}$/.test(color) && color.replace(/[\da-f]/ig, function(i) {
result.push(parseInt(i+i, 16));
return "";
});
/#.{6}/.test(color) && color.replace(/[\da-f]{2}/ig, function(i) {
result.push(parseInt(i, 16));
return "";
});
if (result.length) return result;
return [100,0,0];
};
var padZero = function(val) {
return ("0" + val.toString(16)).match(/.{2}$/)[0];
};
first = calculateValues(first);
last = calculateValues(last);
var result = {
r: ((first.shift() + last.shift()) / 2) | 0,
g: ((first.shift() + last.shift()) / 2) | 0,
b: ((first.shift() + last.shift()) / 2) | 0
};
return "#"+ padZero(result.r) + padZero(result.g) + padZero(result.b);
})("@{arguments}")