1

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)?
Community
  • 1
  • 1
Robert Koritnik
  • 103,639
  • 52
  • 277
  • 404

2 Answers2

2

LESS 1.4.0 Solution

The following is a solution that requires LESS 1.4.0 (currently in beta as of this writing) as it utilizes the new extract() function. It also assumes that the color will always be the first value in the set of values for each argument (which should be the case I believe with proper syntax for the linear-gradient css).

By using the extract() function of LESS 1.4.0 we can allow the arguments to be passed as non-strings, which alleviates the string to color conversion issue you were having in your other question that we discussed (still a good question, however) while also giving access to individual values.

LESS

.gradient(...) {

  //setting up your argument definition string
  @def: ~`"@{arguments}".replace(/[\[\]]/g,"")`;

  //getting the number of arguments 
  //(weeding out nested commas in parenthesis to do it)
  @numStops: unit(`"@{arguments}".replace(/\([^)]*\)/g,"").split(',').length`);

  //extracting raw form of first and last arguments
  //which may be more than just a single color
  @rawFirst: extract(@arguments,1);
  @rawLast: extract(@arguments,@numStops);

  //mixins to recursively evaluate the raw arguments down to a color
  .setFirstColor(@color) when (isColor(@color)) {
     @FirstColor: @color;
  }
  .setFirstColor(@color) when not (isColor(@color)) {
     .setFirstColor(extract(@color,1));
  }
  .setLastColor(@color) when (isColor(@color)) {
     @LastColor: @color;
  }
  .setLastColor(@color) when not (isColor(@color)) {
     .setLastColor(extract(@color,1));
  }

  //call mixins to get the first & last color from first & last arguments
  .setFirstColor(@rawFirst);
  .setLastColor(@rawLast);

  //set your css using the extracted and formatted info
  background-color: mix(@FirstColor, @LastColor);
  background-image: linear-gradient(to bottom, @def);
}

.myClass1 {
  .gradient(#000, #fff); 
}

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

CSS Output

.myClass1 {
  background-color: #808080;
  background-image: linear-gradient(to bottom, #000000, #ffffff);
}
.myClass2 {
  background-color: rgba(179, 179, 179, 0.7);
  background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.5) 25%, #cccccc 50%, rgba(255, 255, 255, 0.9) 80%);
}
Community
  • 1
  • 1
ScottS
  • 71,703
  • 13
  • 126
  • 146
  • Huh I wish I could write an immediately executing javascript function that would do a similar thing. Because this really seems to be exactly what I need. – Robert Koritnik Mar 28 '13 at 19:27
  • To my knowledge there is no way to write a single javascript function that will produce the LESS output you desire. Of course, I'm not a javascript expert (I can get by, but am not a wiz), so I could be wrong. – ScottS Mar 29 '13 at 01:09
  • anything's possible, believe me. – Robert Koritnik Mar 29 '13 at 07:24
  • I've also added my own answer as I've now resolved my problem. I posted it for all those that need it before 1.4 comes out or before Web Essentials Visual Studio addin gets updated for the same. I'm in the last group. But I'll upvote your answer because it's also helpful. In the near future. – Robert Koritnik Mar 29 '13 at 09:58
1

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}")
Robert Koritnik
  • 103,639
  • 52
  • 277
  • 404
  • 1
    +1 for finding an answer for an earlier version of LESS and putting the mix all in a single function. – ScottS Mar 29 '13 at 13:11
  • You should look at helping the [LESS group](https://github.com/cloudhead/less.js) fix the issue with their color function so that a string really could be passed and return a color valid for use in the other color functions. Do so would simplify this and resolve your other question. – ScottS Mar 29 '13 at 13:16
  • It would yes. It's a rather simple change to `color` fuction to make it work with `#xxx`, `#xxxxxx`, `rgb` and `rgba` strings. I'm not sure whether it would make sense to include others like `hsl` or not, but the first four would make it a much better experience. Maybe I should fork it and make a pull request. :) – Robert Koritnik Mar 30 '13 at 12:30