Skip to content

Extending Less Language

Mária Jurčovičová edited this page Sep 16, 2015 · 68 revisions

Less4j can be extended with custom functions and embedded scripts. Embedded scripts are nothing more then slightly more complicated special case of functions. First chapter explains how to create custom function and second chapter explains how to add embedded scripting into less.

Real world example is inside less4j-javascript which adds embedded javascript support to less4j.

Custom Functions

Custom functions are called before build-in functions and may overwrite them. They must implement LessFunction interface:

public interface LessFunction {
  public boolean canEvaluate(FunctionExpression call, List<Expression> parameters);
  public Expression evaluate(FunctionExpression call, List<Expression> parameters, Expression evaluatedParameter, LessProblems problems);
}

The canEvaluate method analysis less function call supplied in input parameters. It returns true only if it should be used to calculate the result. Second evaluate method is called only if the canEvaluate returned true.

Custom functions are passed to less4j using addCustomFunction and addCustomFunctions methods of the Configuration object:

// configure custom function
Configuration configuration = new Configuration();
configuration.addCustomFunction(new CustomFunction());

// use configuration to compile input less file
CompilationResult compilationResult = compiler.compile(inputLessFile, configuration);

Example

We will create function able evaluate only constant() function calls with no parameters. It will always returns the same result, an identifier fixed. Test input:

div {
  property: constant();
}

should produce:

div {
  property: fixed;
}

Custom function implementation:

class ConstantFunction implements LessFunction {

  @Override
  public boolean canEvaluate(FunctionExpression call, List<Expression> parameters) {
    return call.getName().equals("constant") && parameters.isEmpty();
  }

  @Override
  public Expression evaluate(FunctionExpression call, List<Expression> parameters, Expression evaluatedParameter, LessProblems problems) {
    return new IdentifierExpression(call.getUnderlyingStructure(), "fixed");
  }
  
}

Use the Configuration object to pass your new function to the compiler:

Configuration configuration = new Configuration();
configuration.addCustomFunction(new ConstantFunction());

LessCompiler compiler = new DefaultLessCompiler();
CompilationResult compilationResult = compiler.compile(inputLessFile, configuration);

Writing Custom Functions

Parameters

The LessFunction interface has two methods:

public boolean canEvaluate(FunctionExpression call, List<Expression> parameters);
public Expression evaluate(FunctionExpression call, List<Expression> parameters, Expression evaluatedParameter, LessProblems problems);
  • The FunctionExpression call contains abstract syntax tree node corresponding to function call. Its most important method is getName() method which returns the name of called function.

  • The List<Expression> parameters contains list of all expressions used as less function call parameters. Parameter expressions comes in evaluated e.g., variables are already replaced by their values and nested functions/operations are calculated. For example, less function call add(3, 4 + 1) will result in two elements long list. Its first element will correspond to 3 and second element will correspond to 5.

  • The Expression evaluatedParameter contains the same information as previous parameter, but in different form. It contains evaluated parameters as a single abstract syntax tree node. It will be needed only rarely.

  • The last LessProblems problems parameter is used to report errors and warnings. Use it to generate user friendly errors whenever your function encounters wrong or suspicious input.

Output Expression

The evaluate method must return valid instance of Expression. It must not return null. The Expression and all its sub-classes are nodes in abstract syntax tree. Returned value must be a correct abstract syntax tree:

  • Each node can return list of its childs and knows its own parent. Use the 'setParent' method on childs to make the hierarchy consistent.
  • A node can have only one parent and nodes in the returned tree can not repeat. If you need to use the same node twice, the clone() method creates deep clones.
  • underlyingStructure property of each node must be non-null. It is used for error reporting and source map generation. It references original less file elements. If you do not know what to put there, use call.getUnderlyingStructure().

Error Handling

Use the LessProblems parameter to report errors and warnings. If an error is reported, generated css is considered incorrect and will not be generated. Warnings are shown to user, but css is generated as usually.

It generates user friendly errors. Error/warnings reporting methods have two parameters:

  • ASTCssNode node - ast node that caused the problem. It is used to generate line number and column number preceding error description.
  • String description - description of encountered problem.

For example, modified ConstantFunction example:

public Expression evaluate(FunctionExpression call, List<Expression> parameters, Expression evaluatedParameter, LessProblems problems) {
  problems.addWarning(call, "Constant is deprecated.");
  return new IdentifierExpression(call.getUnderlyingStructure(), "fixed");
}

generates following warning:

WARNING 2:13 Constant is deprecated.

Embedded and Escaped Scripts

Embedded and escaped scripts are code snippets closed inside ticks ` `. Escaped code is preceded by tilde ~`code` , embedded has ticks only `code`.

Implement Custom Functions

Less4j treats both embedded and escaped scripts as function calls. Embedded script is presented as function named ` and escaped script as function named ~`. Both obtain one parameter, an instance of EmbeddedScript class. Its getType method returns EMBEDDED_SCRIPT.

Example:

public class EscapedScript implements LessFunction {

  public boolean canEvaluate(FunctionExpression call, List<Expression> parameters) {
    if (!call.getName().equals("~`"))
      return false;

    return parameters.size() == 1 && parameters.get(0).getType() == ASTCssNodeType.EMBEDDED_SCRIPT;
  }

  public Expression evaluate(FunctionExpression call, List<Expression> parameters, Expression evaluatedParameter, LessProblems problems) {
    EmbeddedScript js = (EmbeddedScript) parameters.get(0);

    /* ... implementation goes here ... */    
  }

}

Embedded Script Interpolation

Unless configured otherwise, embedded scripts are interpolated the same way as strings. That may not be always practical and is incompatible with the way less.js interpolates embedded javascripts.

Example: Less.js Interpolation

Less.js embedded script interpolation translates variables values into javascript before replacing them. For example, @variable defined in following script:

@variable: 1 2 3 4;
script: `"translated list: @{variable}"`;

would be translated into its javascript equivalent [1, 2, 3, 4]. Whole script would be compiled exactly the same was as:

script: `"translated list: [1, 2, 3, 4]"`;

The output would be:

script: "translated list: [1, 2, 3, 4]"

Less4j API

Implement EmbeddedScriptGenerator interface to customize less to script translation:

public interface EmbeddedScriptGenerator {
  String toScript(Expression value, LessProblems problemsHandler);
}

Default behavior is implemented in EmbeddedLessGenerator class. You can use it to fall back to default behavior whenever comfortable:

public class JavascriptGenerator implements EmbeddedScriptGenerator {

  private EmbeddedLessGenerator defaultScripting = new EmbeddedLessGenerator();
  
  public String toScript(Expression value, LessProblems problemsHandler) {
    if (value.getType()==ASTCssNodeType.LIST_EXPRESSION) {
      return specialListHandling((ListExpression) value, problemsHandler);
    }
    
    return defaultScripting.toScript(value, problemsHandler);
  }

  /* ... implementation ... */
}

Configuration

Embedded script interpolation is configured using setEmbeddedScriptGenerator method of less4j configuration object. Embedded and escaped script functions are configured the same way as all other functions:

//create new less4j configuration object
Configuration configuration = new Configuration();
//add escaped and embedded script implementation into it
configuration.addCustomFunction(new EscapedScript());
configuration.addCustomFunction(new EmbeddedScript());
//customize interpolation
configuration.setEmbeddedScriptGenerator(new ScriptGenerator());

//compile files with embedded scripting
LessCompiler compiler = new DefaultLessCompiler();
CompilationResult result = compiler.compile(new File(less), configuration);