From b516a55a44a19cfc710714d38f9e1470979305a0 Mon Sep 17 00:00:00 2001 From: meri Date: Wed, 12 Dec 2012 11:20:50 +0100 Subject: [PATCH] Support for string interpolation #20. --- .../github/sommeri/less4j/core/parser/Less.g | 42 +------- .../less4j/core/ThreadUnsafeLessCompiler.java | 7 +- .../expressions/ExpressionEvaluator.java | 17 ++- .../expressions/StringInterpolator.java | 100 ++++++++++++++++++ .../compiler/stages/ReferencesSolver.java | 13 +++ .../sommeri/less4j/utils/CssPrinter.java | 23 ++-- .../less4j/utils/InStringCssPrinter.java | 26 +++++ .../sommeri/less4j/utils/ReadmeExample.java | 1 - .../sommeri/less4j/AbstractFileBasedTest.java | 3 +- .../less4j/compiler/ErrorReportingTest.java | 1 - src/test/resources/command-line/errors.css | 4 + .../command-line/errorsandwarnings.css | 4 + src/test/resources/command-line/one.css | 3 + src/test/resources/command-line/warnings.css | 3 + .../variables-string-interpolation.css | 35 ++++++ .../variables-string-interpolation.less | 61 +++++++++++ 16 files changed, 282 insertions(+), 61 deletions(-) create mode 100644 src/main/java/com/github/sommeri/less4j/core/compiler/expressions/StringInterpolator.java create mode 100644 src/main/java/com/github/sommeri/less4j/utils/InStringCssPrinter.java create mode 100644 src/test/resources/command-line/errors.css create mode 100644 src/test/resources/command-line/errorsandwarnings.css create mode 100644 src/test/resources/command-line/one.css create mode 100644 src/test/resources/command-line/warnings.css create mode 100644 src/test/resources/compile-basic-features/variables/variables-string-interpolation.css create mode 100644 src/test/resources/compile-basic-features/variables/variables-string-interpolation.less diff --git a/src/main/antlr3/com/github/sommeri/less4j/core/parser/Less.g b/src/main/antlr3/com/github/sommeri/less4j/core/parser/Less.g index 7d348487..33d2f8f5 100644 --- a/src/main/antlr3/com/github/sommeri/less4j/core/parser/Less.g +++ b/src/main/antlr3/com/github/sommeri/less4j/core/parser/Less.g @@ -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 | ) @@ -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*) ; @@ -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. diff --git a/src/main/java/com/github/sommeri/less4j/core/ThreadUnsafeLessCompiler.java b/src/main/java/com/github/sommeri/less4j/core/ThreadUnsafeLessCompiler.java index 5555c5f0..7efe7a87 100644 --- a/src/main/java/com/github/sommeri/less4j/core/ThreadUnsafeLessCompiler.java +++ b/src/main/java/com/github/sommeri/less4j/core/ThreadUnsafeLessCompiler.java @@ -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()) { @@ -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(); } } diff --git a/src/main/java/com/github/sommeri/less4j/core/compiler/expressions/ExpressionEvaluator.java b/src/main/java/com/github/sommeri/less4j/core/compiler/expressions/ExpressionEvaluator.java index 466453de..170a999b 100644 --- a/src/main/java/com/github/sommeri/less4j/core/compiler/expressions/ExpressionEvaluator.java +++ b/src/main/java/com/github/sommeri/less4j/core/compiler/expressions/ExpressionEvaluator.java @@ -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; @@ -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); @@ -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; @@ -213,7 +222,7 @@ public Expression evaluate(SignedExpression input) { negation.setExpliciteSign(false); return negation; } - + problemsHandler.nonNumberNegation(input); return new FaultyExpression(input); } diff --git a/src/main/java/com/github/sommeri/less4j/core/compiler/expressions/StringInterpolator.java b/src/main/java/com/github/sommeri/less4j/core/compiler/expressions/StringInterpolator.java new file mode 100644 index 00000000..a2bfe7c0 --- /dev/null +++ b/src/main/java/com/github/sommeri/less4j/core/compiler/expressions/StringInterpolator.java @@ -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 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 findMatches(String originalValue) { + List result = new ArrayList(); + 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; + } + +} diff --git a/src/main/java/com/github/sommeri/less4j/core/compiler/stages/ReferencesSolver.java b/src/main/java/com/github/sommeri/less4j/core/compiler/stages/ReferencesSolver.java index 639eb4f0..5aa3e767 100644 --- a/src/main/java/com/github/sommeri/less4j/core/compiler/stages/ReferencesSolver.java +++ b/src/main/java/com/github/sommeri/less4j/core/compiler/stages/ReferencesSolver.java @@ -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; @@ -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; @@ -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; @@ -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) { @@ -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 sameNameMixins = scope.getNearestMixins(reference); return resolveReferencedMixins(reference, scope, sameNameMixins); diff --git a/src/main/java/com/github/sommeri/less4j/utils/CssPrinter.java b/src/main/java/com/github/sommeri/less4j/utils/CssPrinter.java index 8257a88e..3e81314c 100644 --- a/src/main/java/com/github/sommeri/less4j/utils/CssPrinter.java +++ b/src/main/java/com/github/sommeri/less4j/utils/CssPrinter.java @@ -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; } /** @@ -208,7 +207,7 @@ private boolean appendNth(Nth node) { return true; } - private void appendComments(List comments, boolean ensureSeparator) { + protected void appendComments(List comments, boolean ensureSeparator) { if (comments == null || comments.isEmpty()) return; @@ -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; } @@ -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; @@ -532,10 +531,6 @@ private boolean appendNumberExpression(NumberExpression node) { return true; } - private String format(Double valueAsDouble) { - return FORMATTER.format(valueAsDouble); - } - public void appendSelectors(List selectors) { Iterator iterator = selectors.iterator(); while (iterator.hasNext()) { @@ -612,4 +607,12 @@ private void appendAllChilds(ASTCssNode node) { } } + private String format(Double valueAsDouble) { + return FORMATTER.format(valueAsDouble); + } + + public String toString() { + return builder.toString(); + } + } diff --git a/src/main/java/com/github/sommeri/less4j/utils/InStringCssPrinter.java b/src/main/java/com/github/sommeri/less4j/utils/InStringCssPrinter.java new file mode 100644 index 00000000..b09c2137 --- /dev/null +++ b/src/main/java/com/github/sommeri/less4j/utils/InStringCssPrinter.java @@ -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 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; + } +} diff --git a/src/main/java/com/github/sommeri/less4j/utils/ReadmeExample.java b/src/main/java/com/github/sommeri/less4j/utils/ReadmeExample.java index c994d548..fde04008 100644 --- a/src/main/java/com/github/sommeri/less4j/utils/ReadmeExample.java +++ b/src/main/java/com/github/sommeri/less4j/utils/ReadmeExample.java @@ -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(); diff --git a/src/test/java/com/github/sommeri/less4j/AbstractFileBasedTest.java b/src/test/java/com/github/sommeri/less4j/AbstractFileBasedTest.java index f0154867..42c9b793 100644 --- a/src/test/java/com/github/sommeri/less4j/AbstractFileBasedTest.java +++ b/src/test/java/com/github/sommeri/less4j/AbstractFileBasedTest.java @@ -69,9 +69,10 @@ private String generateErrorReport(Less4jException error) { ByteArrayOutputStream errContent = new ByteArrayOutputStream(); CommandLinePrint printer = new CommandLinePrint(new PrintStream(outContent), new PrintStream(errContent)); + printer.printToSysout(error.getPartialResult(), testName); printer.reportErrorsAndWarnings(error, testName); - String completeErrorReport = errContent.toString(); + String completeErrorReport = outContent.toString() + errContent.toString(); return completeErrorReport; } diff --git a/src/test/java/com/github/sommeri/less4j/compiler/ErrorReportingTest.java b/src/test/java/com/github/sommeri/less4j/compiler/ErrorReportingTest.java index d086371e..03dd2c1c 100644 --- a/src/test/java/com/github/sommeri/less4j/compiler/ErrorReportingTest.java +++ b/src/test/java/com/github/sommeri/less4j/compiler/ErrorReportingTest.java @@ -7,7 +7,6 @@ import com.github.sommeri.less4j.utils.TestFileUtils; -//FIXME: add tests for parser errors and lexer errors public class ErrorReportingTest extends AbstractErrorReportingTest { private static final String cases = "src/test/resources/error-handling/"; diff --git a/src/test/resources/command-line/errors.css b/src/test/resources/command-line/errors.css new file mode 100644 index 00000000..1b154af5 --- /dev/null +++ b/src/test/resources/command-line/errors.css @@ -0,0 +1,4 @@ +.test h4 { + declaration: !#error#!; + padding: 2 2 2 2; +} diff --git a/src/test/resources/command-line/errorsandwarnings.css b/src/test/resources/command-line/errorsandwarnings.css new file mode 100644 index 00000000..89106ce2 --- /dev/null +++ b/src/test/resources/command-line/errorsandwarnings.css @@ -0,0 +1,4 @@ +{ + declaration: !#error#!; + padding: 2 2 2 2; +} diff --git a/src/test/resources/command-line/one.css b/src/test/resources/command-line/one.css new file mode 100644 index 00000000..d027b44b --- /dev/null +++ b/src/test/resources/command-line/one.css @@ -0,0 +1,3 @@ +.test h4 { + declaration: one; +} diff --git a/src/test/resources/command-line/warnings.css b/src/test/resources/command-line/warnings.css new file mode 100644 index 00000000..a3d1aec8 --- /dev/null +++ b/src/test/resources/command-line/warnings.css @@ -0,0 +1,3 @@ +{ + padding: 2 2 2 2; +} diff --git a/src/test/resources/compile-basic-features/variables/variables-string-interpolation.css b/src/test/resources/compile-basic-features/variables/variables-string-interpolation.css new file mode 100644 index 00000000..000d756d --- /dev/null +++ b/src/test/resources/compile-basic-features/variables/variables-string-interpolation.css @@ -0,0 +1,35 @@ +#workingChain { + text: "inner"; +} +#faultyChain { + text: "@{bbb}"; + text2: "@{bbb} ccc"; +} +#trick { + text: "@{bbb}"; +} +#variousQuoting { + text1: "outer"; + text2: 'outer'; +} +#malformed { + text1: "@ {variable}"; + text2: "@{ variable}"; + text3: "@{variable }"; + text4: "@\{variable}"; + text5: "@/{variable}"; + text6: "@{variable"; +} +#multiple { + text1: "outer outer outer"; + text2: "prefix outer after first outer after second outer suffix"; +} +#nonString { + plus: "15"; + sum: "15"; + named: "#008000"; + color: "#114488"; +} +#useUnderdefinedMixin { + text: 'Hi DoNotForget :-)'; +} \ No newline at end of file diff --git a/src/test/resources/compile-basic-features/variables/variables-string-interpolation.less b/src/test/resources/compile-basic-features/variables/variables-string-interpolation.less new file mode 100644 index 00000000..c9bd211b --- /dev/null +++ b/src/test/resources/compile-basic-features/variables/variables-string-interpolation.less @@ -0,0 +1,61 @@ +@chained: "outer"; +@different: "@{chained}"; +@variable: "@{different}"; +#workingChain { + @chained: "inner"; + text: "@{variable}"; +} + +#faultyChain { + @bbb: "ccc"; + @aaa: "bbb"; + text: "@{@{aaa}}"; + text2: "@{@{aaa}} @{bbb}"; +} + +#trick { + @ccc: "ddd"; + @bbb: "ccc"; + @aaa: "@{"; + text: "@{aaa}bbb}"; +} + +#variousQuoting { + text1: "@{variable}"; + text2: '@{variable}'; +} + +#malformed { + text1: "@ {variable}"; + text2: "@{ variable}"; + text3: "@{variable }"; + text4: "@\{variable}"; + text5: "@/{variable}"; + text6: "@{variable"; +} + +#multiple { + text1: "@{chained} @{different} @{variable}"; + text2: "prefix @{chained} after first @{different} after second @{variable} suffix"; +} + +#nonString { + @plus: 5 + 10; + plus: "@{plus}"; + @a: 5; + @sum: @a + 10; + sum: "@{sum}"; + @namedColor: green; + named: "@{namedColor}"; + @color: #114488; + color: "@{color}"; +} + +.underdefinedMixin() { + text: 'Hi @{callerDefined} :-)'; +} + +#useUnderdefinedMixin { + @callerDefined: "DoNotForget"; + .underdefinedMixin(); +} \ No newline at end of file