Skip to content

Commit

Permalink
Support for string interpolation #20.
Browse files Browse the repository at this point in the history
  • Loading branch information
meri committed Dec 12, 2012
1 parent 426b927 commit b516a55
Show file tree
Hide file tree
Showing 16 changed files with 282 additions and 61 deletions.
42 changes: 2 additions & 40 deletions src/main/antlr3/com/github/sommeri/less4j/core/parser/Less.g
Expand Up @@ -350,31 +350,6 @@ ruleset_body
//If we remove LBRACE from the tree, a ruleset with an empty selector will report wrong line number in the warning.
-> ^(BODY LBRACE $a*);

/*TODO add to documentation
This does not create correct structure for selectors, but neither did the
original http://www.antlr.org/grammar/1240941192304/css21.g

The problem is that whitespaces are hidden and therefore following inputs:
* "div :not(:enabled) :not(:disabled)"
* "div:not(:enabled):not(:disabled)"

turn into exactly the same token stream. Which is unfortunate, because they mean
two different things. The first one is equivalent to "div *:not(:enabled) *:not(:disabled)"
while the second not.

Therefore we decided to just parse the thing into as simple structure as possible and solve
the rest in ASTSwitchBuilder. Again, it would be possible to add an action to the rule to
modify the tree in the parser, but it is unnecessary given that we are translating ANTLR
tree into another one.
*/
//TODO: Nested and top level selectors are different: top level one does NOT allow appenders
//selector
//@init {enterRule(retval, RULE_SELECTOR);}
// : ((nestedAppender)=>a+=nestedAppender | ) ((a+=combinator ) (a+=elementName | a+=elementSubsequent | a+=nestedAppender) )*
// -> ^(SELECTOR $a* )
// ;
//finally { leaveRule(); }

selector
@init {enterRule(retval, RULE_SELECTOR);}
: ((combinator)=>a+=combinator | )
Expand All @@ -388,8 +363,8 @@ selector
finally { leaveRule(); }

simpleSelector
: ( (a+=elementName ( ({!predicates.onEmptyCombinator(input)}?)=>a+=elementSubsequent)*)
| (a+=elementSubsequent ( ({!predicates.onEmptyCombinator(input)}?)=>a+=elementSubsequent)*)
: ( (a+=elementName ( {!predicates.onEmptyCombinator(input)}?=>a+=elementSubsequent)*)
| (a+=elementSubsequent ( {!predicates.onEmptyCombinator(input)}?=>a+=elementSubsequent)*)
)
-> ^(SIMPLE_SELECTOR $a*)
;
Expand All @@ -409,19 +384,6 @@ elementSubsequent
| attribOrPseudo
;

//TODO Document: a class name can be also a number e.g., .56 or .5cm
//if that is the case, then the lexer spits out some kind of number instead of IDENT
//this could be solved by a semantic predicate, but if I do this:
//cssClass
// : {1==2}?=>(a=. -> ^(CSS_CLASS $a))
// | ((DOT) => DOT IDENT -> ^(CSS_CLASS IDENT))
// ;
//then predicates.isNthPseudoClass($a) from pseudoclass starts to be copied all over the
//place including places where variable a is not accessible. End result: he parser stops
//being compilable.

// YEP, I was confused about predicates when I wrote the above. Maybe write some post about predicates?

//A class name can be also a number e.g., .56 or .5cm or anything else that starts with ..
//unfortunately, those can be turned into numbers by lexer. This feels like an ugly hack,
//but I do not know how to solve the problem otherwise.
Expand Down
Expand Up @@ -31,8 +31,6 @@ public CompilationResult compile(String lessContent) throws Less4jException {
}

private String doCompile(String lessContent) throws Less4jException {
// FIXME: ugly, clean hierarchy and dependencies
ExtendedStringBuilder stringBuilder = new ExtendedStringBuilder("");

ANTLRParser.ParseResult result = parser.parseStyleSheet(lessContent);
if (result.hasErrors()) {
Expand All @@ -42,9 +40,10 @@ private String doCompile(String lessContent) throws Less4jException {
StyleSheet lessStyleSheet = astBuilder.parse(result.getTree());
ASTCssNode cssStyleSheet = compiler.compileToCss(lessStyleSheet);

CssPrinter builder = new CssPrinter(stringBuilder);

CssPrinter builder = new CssPrinter();
builder.append(cssStyleSheet);
return stringBuilder.toString();
return builder.toString();
}

}
Expand Up @@ -36,7 +36,7 @@
import com.github.sommeri.less4j.core.problems.ProblemsHandler;

public class ExpressionEvaluator {

private final Scope scope;
private final ProblemsHandler problemsHandler;
private ArithmeticCalculator arithmeticCalculator;
Expand Down Expand Up @@ -88,13 +88,22 @@ public Expression evaluate(Variable input) {
return evaluate(value);
}

public Expression evaluateIfPresent(Variable input) {
Expression value = scope.getValue(input);
if (value == null) {
return null;
}

return evaluate(value);
}

public Expression evaluate(IndirectVariable input) {
Expression value = scope.getValue(input);
if (!(value instanceof CssString)) {
problemsHandler.nonStringIndirection(input);
return new FaultyExpression(input);
}

CssString realName = (CssString) value;
String realVariableName = "@" + realName.getValue();
value = scope.getValue(realVariableName);
Expand Down Expand Up @@ -129,10 +138,10 @@ public Expression evaluate(Expression input) {
return ((NamedExpression) input).getExpression();

//the value is already there, nothing to evaluate
case STRING_EXPRESSION:
case IDENTIFIER_EXPRESSION:
case COLOR_EXPRESSION:
case NUMBER:
case STRING_EXPRESSION:
case FAULTY_EXPRESSION:
return input;

Expand Down Expand Up @@ -213,7 +222,7 @@ public Expression evaluate(SignedExpression input) {
negation.setExpliciteSign(false);
return negation;
}

problemsHandler.nonNumberNegation(input);
return new FaultyExpression(input);
}
Expand Down
@@ -0,0 +1,100 @@
package com.github.sommeri.less4j.core.compiler.expressions;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.github.sommeri.less4j.core.ast.CssString;
import com.github.sommeri.less4j.core.ast.Expression;
import com.github.sommeri.less4j.core.ast.Variable;
import com.github.sommeri.less4j.core.parser.HiddenTokenAwareTree;
import com.github.sommeri.less4j.utils.InStringCssPrinter;

public class StringInterpolator {

private final static Pattern STR_INTERPOLATION = Pattern.compile("@\\{([^\\{\\}@])*\\}");

public String replaceInterpolatedVariables(String originalValue, ExpressionEvaluator expressionEvaluator, HiddenTokenAwareTree technicalUnderlying) {
StringBuilder result = new StringBuilder();
List<MatchRange> matches = findMatches(originalValue);
int lastEnd = 0;
for (MatchRange matchRange : matches) {
// add everything from the last end to match from
result.append(originalValue.substring(lastEnd, matchRange.getFrom()));
// replace match by value
result.append(evaluate(expressionEvaluator, technicalUnderlying, matchRange));
// update last end
lastEnd = matchRange.getTo();
}
// add everything from the last end to end of string
if (lastEnd<originalValue.length())
result.append(originalValue.substring(lastEnd));

return result.toString();
}

private String evaluate(ExpressionEvaluator expressionEvaluator, HiddenTokenAwareTree technicalUnderlying, MatchRange matchRange) {
Expression value = expressionEvaluator.evaluateIfPresent(new Variable(technicalUnderlying, matchRange.getVariableName()));
if (value!=null && (value instanceof CssString)) {
CssString string = (CssString) value;
return replaceInterpolatedVariables(string.getValue(), expressionEvaluator, technicalUnderlying);
} else if (value==null) {
return matchRange.getFullMatch();
} else {
InStringCssPrinter builder = new InStringCssPrinter();
builder.append(value);
String replacement = builder.toString();
return replacement;
}
}

private List<MatchRange> findMatches(String originalValue) {
List<MatchRange> result = new ArrayList<MatchRange>();
Matcher matcher = STR_INTERPOLATION.matcher(originalValue);
while (matcher.find()) {
result.add(createMatchRange(matcher));
}

return result;
}

private MatchRange createMatchRange(Matcher matcher) {
String group = matcher.group();
return new MatchRange(matcher.start(), matcher.end(), "@"+group.substring(2, group.length()-1), group);
}

}

class MatchRange {

private final int from;
private final int to;
private final String variableName;
private final String fullMatch;

public MatchRange(int from, int to, String variableName, String fullMatch) {
super();
this.from = from;
this.to = to;
this.variableName = variableName;
this.fullMatch = fullMatch;
}

public int getFrom() {
return from;
}

public int getTo() {
return to;
}

public String getVariableName() {
return variableName;
}

public String getFullMatch() {
return fullMatch;
}

}
Expand Up @@ -7,6 +7,7 @@
import com.github.sommeri.less4j.core.ast.ASTCssNode;
import com.github.sommeri.less4j.core.ast.ASTCssNodeType;
import com.github.sommeri.less4j.core.ast.ArgumentDeclaration;
import com.github.sommeri.less4j.core.ast.CssString;
import com.github.sommeri.less4j.core.ast.Declaration;
import com.github.sommeri.less4j.core.ast.Expression;
import com.github.sommeri.less4j.core.ast.IndirectVariable;
Expand All @@ -16,6 +17,7 @@
import com.github.sommeri.less4j.core.ast.RuleSetsBody;
import com.github.sommeri.less4j.core.ast.Variable;
import com.github.sommeri.less4j.core.compiler.expressions.ExpressionEvaluator;
import com.github.sommeri.less4j.core.compiler.expressions.StringInterpolator;
import com.github.sommeri.less4j.core.compiler.scopes.FullMixinDefinition;
import com.github.sommeri.less4j.core.compiler.scopes.IteratedScope;
import com.github.sommeri.less4j.core.compiler.scopes.Scope;
Expand All @@ -27,6 +29,7 @@ public class ReferencesSolver {
public static final String ALL_ARGUMENTS = "@arguments";
private ASTManipulator manipulator = new ASTManipulator();
private final ProblemsHandler problemsHandler;
private StringInterpolator stringInterpolator = new StringInterpolator();

public ReferencesSolver(ProblemsHandler problemsHandler) {
this.problemsHandler = problemsHandler;
Expand Down Expand Up @@ -66,6 +69,11 @@ private void doSolveReferences(ASTCssNode node, IteratedScope scope) {
manipulator.replaceInBody(namespaceReference, replacement.getChilds());
break;
}
case STRING_EXPRESSION: {
CssString replacement = replaceInString((CssString) node, expressionEvaluator);
manipulator.replace(node, replacement);
break;
}
}

if (node.getType() != ASTCssNodeType.NAMESPACE_REFERENCE) {
Expand All @@ -80,6 +88,11 @@ private void doSolveReferences(ASTCssNode node, IteratedScope scope) {
}
}

private CssString replaceInString(CssString input, ExpressionEvaluator expressionEvaluator) {
String value = stringInterpolator.replaceInterpolatedVariables(input.getValue(), expressionEvaluator, input.getUnderlyingStructure());
return new CssString(input.getUnderlyingStructure(), value, input.getQuoteType());
}

private RuleSetsBody resolveMixinReference(MixinReference reference, Scope scope) {
List<FullMixinDefinition> sameNameMixins = scope.getNearestMixins(reference);
return resolveReferencedMixins(reference, scope, sameNameMixins);
Expand Down
23 changes: 13 additions & 10 deletions src/main/java/com/github/sommeri/less4j/utils/CssPrinter.java
Expand Up @@ -46,12 +46,11 @@

public class CssPrinter {

private final ExtendedStringBuilder builder;
protected final ExtendedStringBuilder builder = new ExtendedStringBuilder("");
private static final DecimalFormat FORMATTER = new DecimalFormat("#.##################");

public CssPrinter(ExtendedStringBuilder builder) {
public CssPrinter() {
super();
this.builder = builder;
}

/**
Expand Down Expand Up @@ -208,7 +207,7 @@ private boolean appendNth(Nth node) {
return true;
}

private void appendComments(List<Comment> comments, boolean ensureSeparator) {
protected void appendComments(List<Comment> comments, boolean ensureSeparator) {
if (comments == null || comments.isEmpty())
return;

Expand Down Expand Up @@ -484,7 +483,7 @@ public boolean appendExpressionOperator(ExpressionOperator operator) {

public boolean appendCssString(CssString expression) {
String quoteType = expression.getQuoteType();
builder.append(quoteType + expression.getValue() + quoteType);
builder.append(quoteType).append(expression.getValue()).append(quoteType);

return true;
}
Expand All @@ -495,7 +494,7 @@ public boolean appendIdentifierExpression(IdentifierExpression expression) {
return true;
}

private boolean appendColorExpression(ColorExpression expression) {
protected boolean appendColorExpression(ColorExpression expression) {
// if it is named color expression, write out the name
if (expression instanceof NamedColorExpression) {
NamedColorExpression named = (NamedColorExpression) expression;
Expand Down Expand Up @@ -532,10 +531,6 @@ private boolean appendNumberExpression(NumberExpression node) {
return true;
}

private String format(Double valueAsDouble) {
return FORMATTER.format(valueAsDouble);
}

public void appendSelectors(List<Selector> selectors) {
Iterator<Selector> iterator = selectors.iterator();
while (iterator.hasNext()) {
Expand Down Expand Up @@ -612,4 +607,12 @@ private void appendAllChilds(ASTCssNode node) {
}
}

private String format(Double valueAsDouble) {
return FORMATTER.format(valueAsDouble);
}

public String toString() {
return builder.toString();
}

}
@@ -0,0 +1,26 @@
package com.github.sommeri.less4j.utils;

import java.util.List;

import com.github.sommeri.less4j.core.ast.ColorExpression;
import com.github.sommeri.less4j.core.ast.Comment;
import com.github.sommeri.less4j.core.ast.CssString;

public class InStringCssPrinter extends CssPrinter {

@Override
protected void appendComments(List<Comment> comments, boolean ensureSeparator) {
}

@Override
public boolean appendCssString(CssString expression) {
builder.append(expression.getValue());
return true;
}

@Override
protected boolean appendColorExpression(ColorExpression expression) {
builder.append(expression.getValue());
return true;
}
}
Expand Up @@ -6,7 +6,6 @@
import com.github.sommeri.less4j.LessCompiler.Problem;
import com.github.sommeri.less4j.core.ThreadUnsafeLessCompiler;

//FIXME: !!!! change readme
public class ReadmeExample {
public static void main(String[] args) throws Less4jException {
LessCompiler compiler = new ThreadUnsafeLessCompiler();
Expand Down

0 comments on commit b516a55

Please sign in to comment.