Wednesday, May 1, 2013

Less CSS - Expressions and Traps

In an average case, less expressions are easy to use, very useful and behave the same way as expressions in any other programming language. Unfortunately, their interactions with the rest of css syntax can occasionally cause somewhat quirky behavior. Adding to that, next less language version will change in backward incompatible way and expressions are affected too.

This post shows when and why quirky behavior happens, which problems are going to be solved by planned changes and which traps will still remain there.

It starts with short expressions overview. Less expressions have two differences against what is intuitive and both are explained in second chapter. Third chapter is about backward incompatible changes. There are also two of them and both have major consequences.

Previous posts in this series:

Table of Contents

Warning

Some aspects of planned backward compatible changes are still under discussion. This post describes proposed changes as implemented in third beta version of less-1.4.0.js.

Since the discussion started after this post was written, I decided to release the post anyway and keep it as is. People interested in upcoming changes may still find it useful and it may take some time until all discussions are concluded.

List of active discussions will be updated as new issues are opened and closed:
Update: The discussion have been concluded in the meantime. The required parentheses backward incompatible change will be postponed. Next less.js release 1.4.0 will not have it. The change will be released in less 2.0. The rest of post have been updated.

Overview

Expressions are used to perform basic mathematical operations directly inside less style sheets. They can evaluate four basic arithmetical operators (+, -, * and /), know how to handle parentheses (), can reference variables @variable-name and even support several build-in functions.

Example:
@variable: 3;
#selector {
  arithmetical-operators: (3*10+5);
  parentheses: (3*(10+5));
  reference-variable: (@variable-2);
  /* note: sine function will be available in upcoming less.js 1.4.0 */
  built-in-function-sine: sin(1); 
}

compiles into:
#selector {
  arithmetical-operators: 35;
  parentheses: 45;
  reference-variable: 1;
  built-in-function-sine: 0.8414709848078965;
}

Expressions and Ambiguity

As we wrote in the overview, interactions between expressions and css syntax lead to two less language quirks. While both are easy to deal with, both may be also surprising for someone who run into them for the first time.

Ambiguous situations:
Problems with double meaning of slash / symbol occurred only rarely. Unfortunately, they have been impossible to solve and thus triggered the biggest backward incompatible change less 2.0.0 will have. The second issue, double meaning of space, is something almost every less user will run into.

Division vs Ration
Less uses slash / to express division and css uses it to denote ratio in fonts and media queries. Look at following less style:
@baseLineHeight: 10;
@media (aspect-ratio: 16/10){
  div {
    margin-top: (@baseLineHeight/2);
  }
}

The above less contains two slashes:
  • media query: aspect-ratio: 16/10,
  • declaration: margin-top: (@baseLineHeight/2).

The first slash is media query ratio and should compile into aspect-ratio: 16/10 while the second slash is division and should compile into margin-top: 5. The whole less should compile into following css:
@media (aspect-ratio: 16/10){
  div {
    margin-top: 5;
  }
}

The problem: symbol slash / is ambiguous and means different things in different contexts. This ambiguity is solved differently by 1.3.x and 2.0.0 versions.

Note: this change was originally planned for less.js 1.4.0. It was postponed to give users more time before the change.

Less 1.3.x Way
Ratios are used mainly in media queries and fonts, so expressions evaluation in media queries and fonts have been turned off. This prevents misinterpreted ratio slashes in correct css, but has one huge obvious drawback - you can not use expressions in them.

Any slash in font declaration or media query is treated as ratio and will be unmodified:
#selector {
  font: 4/2; 
}
@media (aspect-ratio: 16/9) { ... }

compiles into:
#selector {
  font: 4/2; 
}
@media (aspect-ratio: 16/9) { ... }

Expressions in font declaration or media query are not supported and the outcome is either syntax error or undefined:
#selector {
  /* expression not evaluated: undefined result */
  font: 4+2; 
}
/* expression not evaluated: syntax error */
@media (aspect-ratio: (16/9)) { ... }
/* expression not evaluated: syntax error */
@media (min-width: 2+3) { ... }

Expressions on any other place are treated as math operations and evaluated. All slashes other then those in fonts and media queries are treated as division:
#selector {
  margin: 4/2; 
}

compiles into:
#selector {
  margin: 2; 
}

Less 2.x.x Way
Less 2.0.0 changed expression evaluation to work the same way everywhere. There will be no difference between media queries, fonts and everything else.

Instead, the expression must be enclosed into parentheses in order to be evaluated. If it is not inside parentheses, then it is going to be copied into output as is. Details are described in backward incompatible changes chapter, so this section shows only few simple examples.

Expressions in fonts and other properties are evaluated exactly the same way:
#fonts {
  font: 4/2; // no evaluation: compiles into "font: 4/2"
  font: 4+2; // no evaluation: compiles into "font: 4+2"
  font: (4/2); // evaluation: compiles into "font: 2"
  font: (4+2); // evaluation: compiles into "font: 6"
}
#margins {
  margin: 4/2; // no evaluation: compiles into "margin: 4/2"
  margin: 4+2; // no evaluation: compiles into "margin: 4+2"
  margin: (4/2); // evaluation: compiles into "margin: 2"
  margin: (4+2); // evaluation: compiles into "margin: 6"
}

Media queries evaluation is also the same:
// no evaluation: compiles into "aspect-ratio: 16/9"
@media (aspect-ratio: 16/9) { 
  h1{ color:green;}
}

// evaluation: compiles into "aspect-ratio: 1.7777"
@media (aspect-ratio: (16/9)) {
  h1{ color:green;}
}

// no evaluation: compiles into "min-width: 2+3"
@media (min-width: 2+3) { 
  h1{ color:green;}
}

// evaluation: compiles into "min-width: 5"
@media (min-width: (2+3)) {
  h1{ color:green;}
}

Lists vs Math Operations
Css list members are separated either by comma or space. While comma causes no problems, space as list separator can lead to ambiguities.

Consider following declaration:
margin: 2 -1;

The expression 2 -1 could be interpreted either as subtraction with result 1 or as list with two members 2 and -1. If it is subtraction, then compiled style sheet should be equivalent to single margin-top declaration:
margin-top: 1;

If it is list, then compiled style sheet should be equivalent to one margin-top and one margin-right declaration:
margin-top: 2;
margin-right: -1;

The problem: space inside an expression is ambiguous. All less.js versions solve this ambiguity in the same way. The only difference between them is planned for 2.0.0 and is caused by the expressions must be enclosed in parentheses backward incompatible change.

Breakdown
First of all, there is no ambiguity in case of two numbers separated with two math operators:
  • clear expression: 3 - -1,
  • clear expression: 3- -1,
  • clear expression: 3- +1,
  • clear expression: 3-+1.

There is no ambiguity in case of two numbers separated with operator other then plus/minus:
  • clear expression: 3 *1,
  • clear expression: 3*1.

There is no ambiguity in case of two numbers separated by space only:
  • clear list: 3 1.

On the other hand, two numbers separated with exactly one plus or minus e.g., 1 -2, are ambiguous. Their evaluation depends on exact spaces location. Spaces matter a lot if there is exactly one plus or minus between two numbers:
  • ambiguous - space does matter: 3 -1,
  • ambiguous - space does matter: 3 +1,
  • ambiguous - space does matter: 3-1,
  • ambiguous - space does matter: 3+ 1,
  • ambiguous - space does matter: 3 + 1.

Ambiguity Solution
If there is exactly one plus or minus between two numbers, less checks spaces. Only following sequence is evaluated as a list: number, space, operator, number. Anything else is treated as a math expression:
  • expression: 3-1,
  • expression: 3 - 1,
  • expression: 3- 1,
  • list: 3 -1,
  • list: 3 +1.

When compiler encounters complicated expression, it checks whether the expression is a list first. If it is a list, parser splits it into list members and evaluates each one separately.

For example, the input 5 - 2 +3 is first split into a list with two members: 5 - 2 and +3. Each one is evaluated separately, so the result is another list with two members 3 +3.

Otherwise said, 5 - 2 +3 is effectively equivalent to 5 - 2, +3.

Less 1.3.x Details
Old less.js versions make no difference between operations inside parentheses and outside of them. Without parentheses:
#no-parentheses {
  margin: 5 -2; // list: compiles into "margin: 5 -2;"
  margin: 5 - 2; // expression: compiles into "margin: 3;"
  margin: 5 - 2 +3; // expression in list: compiles into "margin: 3 +3;"
}

Inside parentheses:
#inside-parentheses {
  margin: (5 -2); // list: compiles into "margin: 5 -2;"
  margin: (5 - 2); // expression: compiles into "margin: 3;"
  margin: (5 - 2 +3); // expression in list: compiles into "margin: 3 +3;"
  margin: 5 - (2 +3); // list in expression: compiles into "margin: NaN;"
}

Operations over expression inside parentheses:
#multiply-parentheses {
  margin: 2*(1 -2); // multiply list: compiles into "margin: NaN;"
  margin: 2*(1 - 2); // multiply expression: compiles into "margin: -2;"
}

Less 2.0.0 Details
Less 2.0.0 will use exactly the same parse rules, the algorithm to distinguish between lists and expressions did not changed. All differences are caused by new only expressions are allowed inside parentheses rule.

Content inside parentheses is evaluated as math. If it happens to be list, syntax error is thrown. The declaration that was accepted and compiled by 1.3.x version may suddenly start throwing errors:
#inside-parentheses {
  margin: (5 -2); // list in parentheses: syntax error
  margin: (5 - 2); // expression: compiles into "margin: 3;"
  margin: (5 - 2 +3); // list in parentheses: syntax error
  margin: 5 - (2 +3); // list in parentheses: syntax error
}

Backward Incompatible Changes

Expression related backward incompatible changes are not the only planned changes, but they are definitely the most important ones. There are two planned changes and both have major consequences for existing less style sheets:
Required Parentheses
Less 2.0.0 does not evaluate arithmetical operations unless they are be enclosed in parentheses. In addition, any expression enclosed in parentheses is expected to be math and less will attempt to evaluate it as a math.

Basic Examples
If it is outside of parentheses, then it is not math and will not be evaluated:
#no-parentheses {
  margin: 1 - 2; // no evaluation: compiles into "margin: 1 - 2;"
  margin: 1 -2; // no evaluation: compiles into "margin: 1 -2;"
  margin: 2*(1 - 2); // no evaluation: compiles into "margin: 2*(1 - 2);"
}

Mathematical expressions inside parentheses are evaluated:
#math-inside-parentheses {
  margin: (1 - 2); // expression: compiles into "margin: -1;"
}

Non-mathematical expressions inside parentheses are evaluated and cause syntax errors:
#math-inside-parentheses {
  margin: (left); // identifier inside parentheses - syntax error
  margin: (1 -2); // list inside parentheses - syntax error
}

Mixins
Mixins parameters are treated the same way. The parameter is evaluated if and only if it is closed inside parentheses. It does not matter whether the parentheses is located inside the mixin declaration, inside the mixin call or somewhere else. If it is present somewhere, the expression is evaluated. Otherwise it is not.

Less files with parentheses inside mixins body, inside mixin call and elsewhere:
/* Inside Body */
.mixin(@param) {
  declaration: (@param); 
}
#evaluated {
  .mixin(3 - 1);
}
 
/* Inside Call */
.mixin(@param) {
  declaration: @param; 
}
#evaluated {
  .mixin((3 - 1));
}
 
/* Elsewhere */
.mixin(@param) {
  declaration: @param; 
}
#evaluated {
  @variable: (3 - 1);
  .mixin(@variable);
}

Compilation result is the same in all three cases:
/* Compiled css */
#evaluated {
  declaration: 2;
}

If the value inside parentheses happens to be list or other non-expression, syntax error is thrown. All three less files throw compilation error:
/* Inside Body */
.mixin(@param) {
  declaration: (@param); 
}
#evaluated {
  /* !list! */
  .mixin(3 -1);
}
 
/* Inside Call */
.mixin(@param) {
  declaration: @param; 
}
#evaluated {
  /* !list! */
  .mixin((3 -1)); 
}
 
/* Elsewhere */
.mixin(@param) {
  declaration: @param; 
}
#evaluated {
  /* !list! */
  @variable: (3 -1);
  .mixin(@variable);
}

If there is no parentheses, expression will not be evaluated. Both input files compile into the same css:
/* Less input 1 */
.mixin(@param) {
  declaration: @param; 
}
#not-evaluated {
  .mixin(3 - 1);
}
 
/* Less input 2 */
.mixin(@param) {
  declaration: @param; 
}
#not-evaluated {
  @variable: 3 - 1;
  .mixin(@variable);
}
/* Compiled css */
#not-evaluated {
  declaration: 3 - 1; 
}
 
 
 
 

Variables
Variables are treated with exactly the same logic. Three less files with parentheses on three different places:
/* Less 1 */
#evaluated {
  @var1: (3 - 1);
  @var2: @var1;
  declaration: @var2; 
}
/* Less 2 */
#evaluated {
  @var1: 3 - 1;
  @var2: (@var1);
  declaration: @var2; 
}
/* Less 3 */
#evaluated {
  @var1: 3 - 1;
  @var2: @var1;
  declaration: (@var2); 
}

Compilation result is the same in all three cases:
/* Compiled css */
#evaluated {
  declaration: 2;
}

No parentheses - no evaluation:
/* Less input */
#not-evaluated {
  @var1: 3 - 1;
  @var2: @var1;
  declaration: @var2; 
}
/* Compiled css */
#not-evaluated {
  declaration: 3 - 1; 
}
 
 

Functions
Built-in functions are evaluated even without parentheses around them. However, if you want to use an expression as a function parameter, the expression must be enclosed in parentheses.

Expression as a function parameter and compilation result:
/* Less input */
#function {
  declaration: sqrt((36-11));
}
/* Compiled css */
#function {
  declaration: 5;
}

Expression without parentheses causes syntax errors:
/* Syntax error */
#function {
  declaration: sqrt(36-11); // syntax error
}

Expressions and Units
The old way of treating units inside expressions is very simple. Less 1.3.x takes numbers as they are, calculates the result and then assigns leftmost explicitly stated unit type to the result.

The old way:
#selector {
  property: 2 + 5cm - 3mm; // compiles into "property: 4cm"
  property: (1 + 5cm) - 3%; // compiles into "property: 3cm"
}

Less 1.4.0 treats units in a bit more interesting way. While division and multiplication have been left intact, plus and minus operations gained new ability:
  • plus + and minus - operations convert between compatible number types.

Unit Conversions for Plus and Minus
Plus + and minus - operations convert between compatible number types. Numbers without units are not converted:
#selector-compiles {
  /* less converts 3mm into 0.3cm before the operation */
  property: (5cm - 3mm); // compiles into "property: 4.7cm;"
  property: (2 + 5cm - 3mm); // compiles into "property: 6.7cm;"
}

If you add or subtract incompatible numbers, the result inherits left side unit. Right side unit is ignored and behaves as if it would be unspecified:
#selector-wrong {
  /* % is incompatible with cm and ignored */
  property-zero: (5cm - 3%); // compiles into "2cm;"
  /* % is incompatible with cm and ignored */
  property-one: (5cm - 3% - 1mm); // compiles into "1.9cm;"
  /* % is incompatible with cm and ignored */
  property-two: ((1 + 5cm) - 3% - 1mm); // compiles into "2.9cm;"
}

Notes on Division and Multiplication
Unlike plus and minus, division and multiplication are not able to convert between unit types. Just as in the old version, they take numbers as they are, calculate the result and then assign leftmost explicitly stated unit type to the result:
#simplified-expressions {
  property-one: (4cm/2cm); // css: "property-one: 2cm;"
  property-two: (4cm*2mm); // css: "property-two: 8cm;"
  property-three: (1/1mm); // css: "property-three: 1mm;"

Plus and minus over divided or multiplicated expressions still convert units:
#syntax-errors {
  property-1: (2cm/1mm - 1mm/1cm); // css: "1.9cm"
  property-2: (2cm*1mm + 1mm*1cm); // css: "2.1cm"
}

Preview Availability

If you want to try the new syntax, less.js already released fourth beta of upcoming 1.4.0. It is not meant to be used in production, but you can try it out anyway or maybe even start to migrate old style sheets. And if you are writing new style sheet, it is probably good idea to make it new syntax compatible from the start.

However, fourth beta does not require parentheses around math expressions. That change was included in previous beta three and then postponed. So, if you want to play around required parentheses, you have three options:
  • get it from npm: npm install -g less@1.4.0-b3
  • get it from history on Github,
  • wait for first 2.0.0 beta release.

Conclusion

That would be all about expressions, quirks and their incoming changes. Outside of what was described in this post, expressions in less work the same way as expressions in any other language.

In short, all you need to remember is this:
  • Place all expressions into parentheses.
  • If expressions return unexpected results, check out units.
  • If it throws unexpected errors or generates lists where it should not, check out spaces in expressions.

1 comments:

Anonymous said...

Thanks for posting this. As someone trying to learn some of the deeper parts of LESS, get into the nitty-gritty of it and what it's capable of, I appreciate in-depth posts like this. It was very enlightening. Same with your other LESS posts.

Post a Comment