Search code examples
argumentslessmixins

How do I get particular anonymous argument in LESS mixin


Note: I don't want to provide parameters using ~"parameters" notation as it's not user friendly and developers always have to think of that. I'm using this trick with javascript evaluation to get arguments in correct format and can be seen below in my mixin.

I have a LESS mixin

.gradient(...) {
    @def = ~`"@{arguments}".replace(/[\[\]]/g,"")`;

    background-color: mix(/* first and last argument color */)
    background-image: linear-gradient(to bottom, @def);
}

As gradient can have several colour stop definitions I defined it to support an arbitrary number of arguments.

Now. What I want to get is first and last parameter's colour definition only. The problem is that these can be provided in different ways some simple to extract others complicated:

.gradient(#000, #fff); // easy
.gradient(fade(#000, 50) 25%, #ccc 50%, fade(#fff, 90) 80%); // complicated

Questions

  1. Is it possible to access individual arguments from @arguments without resorting to string conversion?
  2. 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 rbga values)?

Solution

  • 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}")