freemarker-notifications mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ddek...@apache.org
Subject [47/54] [partial] incubator-freemarker git commit: Unifying the o.a.f.core and o.a.f.core.ast
Date Thu, 23 Feb 2017 21:36:14 GMT
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/ASTExpNegateOrPlus.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/ASTExpNegateOrPlus.java b/src/main/java/org/apache/freemarker/core/ASTExpNegateOrPlus.java
new file mode 100644
index 0000000..753202b
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/ASTExpNegateOrPlus.java
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * 
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.model.impl.SimpleNumber;
+
+/**
+ * AST expression node: {@code -exp} or {@code +exp}.
+ */
+final class ASTExpNegateOrPlus extends ASTExpression {
+    
+    private final int TYPE_MINUS = 0;
+    private final int TYPE_PLUS = 1;
+
+    private final ASTExpression target;
+    private final boolean isMinus;
+    private static final Integer MINUS_ONE = Integer.valueOf(-1); 
+
+    ASTExpNegateOrPlus(ASTExpression target, boolean isMinus) {
+        this.target = target;
+        this.isMinus = isMinus;
+    }
+    
+    @Override
+    TemplateModel _eval(Environment env) throws TemplateException {
+        TemplateNumberModel targetModel = null;
+        TemplateModel tm = target.eval(env);
+        try {
+            targetModel = (TemplateNumberModel) tm;
+        } catch (ClassCastException cce) {
+            throw new NonNumericalException(target, tm, env);
+        }
+        if (!isMinus) {
+            return targetModel;
+        }
+        target.assertNonNull(targetModel, env);
+        Number n = targetModel.getAsNumber();
+        n = ArithmeticEngine.CONSERVATIVE_ENGINE.multiply(MINUS_ONE, n);
+        return new SimpleNumber(n);
+    }
+    
+    @Override
+    public String getCanonicalForm() {
+        String op = isMinus ? "-" : "+";
+        return op + target.getCanonicalForm();
+    }
+
+    @Override
+    String getNodeTypeSymbol() {
+        return isMinus ? "-..." : "+...";
+    }
+    
+    @Override
+    boolean isLiteral() {
+        return target.isLiteral();
+    }
+
+    @Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) {
+    	return new ASTExpNegateOrPlus(
+    	        target.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState),
+    	        isMinus);
+    }
+
+    @Override
+    int getParameterCount() {
+        return 2;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        switch (idx) {
+        case 0: return target;
+        case 1: return Integer.valueOf(isMinus ? TYPE_MINUS : TYPE_PLUS);
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        switch (idx) {
+        case 0: return ParameterRole.RIGHT_HAND_OPERAND;
+        case 1: return ParameterRole.AST_NODE_SUBTYPE;
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/ASTExpNot.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/ASTExpNot.java b/src/main/java/org/apache/freemarker/core/ASTExpNot.java
new file mode 100644
index 0000000..5aed612
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/ASTExpNot.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * 
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+/**
+ * AST expression node: {@code !exp}.
+ */
+final class ASTExpNot extends ASTExpBoolean {
+
+    private final ASTExpression target;
+
+    ASTExpNot(ASTExpression target) {
+        this.target = target;
+    }
+
+    @Override
+    boolean evalToBoolean(Environment env) throws TemplateException {
+        return (!target.evalToBoolean(env));
+    }
+
+    @Override
+    public String getCanonicalForm() {
+        return "!" + target.getCanonicalForm();
+    }
+ 
+    @Override
+    String getNodeTypeSymbol() {
+        return "!";
+    }
+    
+    @Override
+    boolean isLiteral() {
+        return target.isLiteral();
+    }
+
+    @Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) {
+    	return new ASTExpNot(
+    	        target.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState));
+    }
+
+    @Override
+    int getParameterCount() {
+        return 1;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        if (idx != 0) throw new IndexOutOfBoundsException();
+        return target;
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        if (idx != 0) throw new IndexOutOfBoundsException();
+        return ParameterRole.RIGHT_HAND_OPERAND;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/ASTExpNumberLiteral.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/ASTExpNumberLiteral.java b/src/main/java/org/apache/freemarker/core/ASTExpNumberLiteral.java
new file mode 100644
index 0000000..c60c34e
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/ASTExpNumberLiteral.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * 
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.model.impl.SimpleNumber;
+
+/**
+ * AST expression node: numerical literal
+ */
+final class ASTExpNumberLiteral extends ASTExpression implements TemplateNumberModel {
+
+    private final Number value;
+
+    public ASTExpNumberLiteral(Number value) {
+        this.value = value;
+    }
+    
+    @Override
+    TemplateModel _eval(Environment env) {
+        return new SimpleNumber(value);
+    }
+
+    @Override
+    public String evalAndCoerceToPlainText(Environment env) throws TemplateException {
+        return env.formatNumberToPlainText(this, this, false);
+    }
+
+    @Override
+    public Number getAsNumber() {
+        return value;
+    }
+    
+    String getName() {
+        return "the number: '" + value + "'";
+    }
+
+    @Override
+    public String getCanonicalForm() {
+        return value.toString();
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return getCanonicalForm();
+    }
+    
+    @Override
+    boolean isLiteral() {
+        return true;
+    }
+
+    @Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) {
+        return new ASTExpNumberLiteral(value);
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 0;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/ASTExpOr.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/ASTExpOr.java b/src/main/java/org/apache/freemarker/core/ASTExpOr.java
new file mode 100644
index 0000000..e01e875
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/ASTExpOr.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * 
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+/**
+ * AST expression node: {@code exp || exp}.
+ */
+final class ASTExpOr extends ASTExpBoolean {
+
+    private final ASTExpression lho;
+    private final ASTExpression rho;
+
+    ASTExpOr(ASTExpression lho, ASTExpression rho) {
+        this.lho = lho;
+        this.rho = rho;
+    }
+
+    @Override
+    boolean evalToBoolean(Environment env) throws TemplateException {
+        return lho.evalToBoolean(env) || rho.evalToBoolean(env);
+    }
+
+    @Override
+    public String getCanonicalForm() {
+        return lho.getCanonicalForm() + " || " + rho.getCanonicalForm();
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return "||";
+    }
+
+    @Override
+    boolean isLiteral() {
+        return constantValue != null || (lho.isLiteral() && rho.isLiteral());
+    }
+
+    @Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) {
+    	return new ASTExpOr(
+    	        lho.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState),
+    	        rho.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState));
+    }
+
+    @Override
+    int getParameterCount() {
+        return 2;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        switch (idx) {
+        case 0: return lho;
+        case 1: return rho;
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        return ParameterRole.forBinaryOperatorOperand(idx);
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/ASTExpParenthesis.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/ASTExpParenthesis.java b/src/main/java/org/apache/freemarker/core/ASTExpParenthesis.java
new file mode 100644
index 0000000..9803498
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/ASTExpParenthesis.java
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * 
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.model.TemplateModel;
+
+/**
+ * AST expression node: {@code (exp)}.
+ */
+final class ASTExpParenthesis extends ASTExpression {
+
+    private final ASTExpression nested;
+
+    ASTExpParenthesis(ASTExpression nested) {
+        this.nested = nested;
+    }
+
+    @Override
+    boolean evalToBoolean(Environment env) throws TemplateException {
+        return nested.evalToBoolean(env);
+    }
+
+    @Override
+    public String getCanonicalForm() {
+        return "(" + nested.getCanonicalForm() + ")";
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return "(...)";
+    }
+    
+    @Override
+    TemplateModel _eval(Environment env) throws TemplateException {
+        return nested.eval(env);
+    }
+    
+    @Override
+    public boolean isLiteral() {
+        return nested.isLiteral();
+    }
+    
+    ASTExpression getNestedExpression() {
+        return nested;
+    }
+
+    @Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) {
+        return new ASTExpParenthesis(
+                nested.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState));
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 1;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        if (idx != 0) throw new IndexOutOfBoundsException();
+        return nested;
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        if (idx != 0) throw new IndexOutOfBoundsException();
+        return ParameterRole.ENCLOSED_OPERAND;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/ASTExpRange.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/ASTExpRange.java b/src/main/java/org/apache/freemarker/core/ASTExpRange.java
new file mode 100644
index 0000000..0910517
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/ASTExpRange.java
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * 
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.util.BugException;
+
+/**
+ * AST expression node: {@code exp .. exp}, {@code exp ..< exp} (or {@code exp ..! exp}), {@code exp ..* exp}.
+ */
+final class ASTExpRange extends ASTExpression {
+
+    static final int END_INCLUSIVE = 0; 
+    static final int END_EXCLUSIVE = 1; 
+    static final int END_UNBOUND = 2; 
+    static final int END_SIZE_LIMITED = 3; 
+    
+    final ASTExpression lho;
+    final ASTExpression rho;
+    final int endType;
+
+    ASTExpRange(ASTExpression lho, ASTExpression rho, int endType) {
+        this.lho = lho;
+        this.rho = rho;
+        this.endType = endType;
+    }
+    
+    int getEndType() {
+        return endType;
+    }
+
+    @Override
+    TemplateModel _eval(Environment env) throws TemplateException {
+        final int begin = lho.evalToNumber(env).intValue();
+        if (endType != END_UNBOUND) {
+            final int lhoValue = rho.evalToNumber(env).intValue();
+            return new BoundedRangeModel(
+                    begin, endType != END_SIZE_LIMITED ? lhoValue : begin + lhoValue,
+                    endType == END_INCLUSIVE, endType == END_SIZE_LIMITED); 
+        } else {
+            return new ListableRightUnboundedRangeModel(begin);
+        }
+    }
+    
+    // Surely this way we can tell that it won't be a boolean without evaluating the range, but why was this important?
+    @Override
+    boolean evalToBoolean(Environment env) throws TemplateException {
+        throw new NonBooleanException(this, new BoundedRangeModel(0, 0, false, false), env);
+    }
+
+    @Override
+    public String getCanonicalForm() {
+        String rhs = rho != null ? rho.getCanonicalForm() : "";
+        return lho.getCanonicalForm() + getNodeTypeSymbol() + rhs;
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        switch (endType) {
+        case END_EXCLUSIVE: return "..<";
+        case END_INCLUSIVE: return "..";
+        case END_UNBOUND: return "..";
+        case END_SIZE_LIMITED: return "..*";
+        default: throw new BugException(endType);
+        }
+    }
+    
+    @Override
+    boolean isLiteral() {
+        boolean rightIsLiteral = rho == null || rho.isLiteral();
+        return constantValue != null || (lho.isLiteral() && rightIsLiteral);
+    }
+    
+    @Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) {
+        return new ASTExpRange(
+                lho.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState),
+                rho.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState),
+                endType);
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 2;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        switch (idx) {
+        case 0: return lho;
+        case 1: return rho;
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        return ParameterRole.forBinaryOperatorOperand(idx);
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/ASTExpStringLiteral.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/ASTExpStringLiteral.java b/src/main/java/org/apache/freemarker/core/ASTExpStringLiteral.java
new file mode 100644
index 0000000..b7c8960
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/ASTExpStringLiteral.java
@@ -0,0 +1,208 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * 
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import java.io.StringReader;
+import java.util.List;
+
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.apache.freemarker.core.model.impl.SimpleScalar;
+import org.apache.freemarker.core.util.FTLUtil;
+
+/**
+ * AST expression node: string literal
+ */
+final class ASTExpStringLiteral extends ASTExpression implements TemplateScalarModel {
+    
+    private final String value;
+    
+    /** {@link List} of {@link String}-s and {@link ASTInterpolation}-s. */
+    private List<Object> dynamicValue;
+    
+    ASTExpStringLiteral(String value) {
+        this.value = value;
+    }
+    
+    /**
+     * @param parentTkMan
+     *            The token source of the template that contains this string literal. As of this writing, we only need
+     *            this to share the {@code namingConvetion} with that.
+     */
+    void parseValue(FMParserTokenManager parentTkMan, OutputFormat outputFormat) throws ParseException {
+        // The way this works is incorrect (the literal should be parsed without un-escaping),
+        // but we can't fix this backward compatibly.
+        if (value.length() > 3 && (value.indexOf("${") >= 0 || value.indexOf("#{") >= 0)) {
+            
+            Template parentTemplate = getTemplate();
+            ParserConfiguration pcfg = parentTemplate.getParserConfiguration();
+
+            try {
+                SimpleCharStream simpleCharacterStream = new SimpleCharStream(
+                        new StringReader(value),
+                        beginLine, beginColumn + 1,
+                        value.length());
+                simpleCharacterStream.setTabSize(pcfg.getTabSize());
+                
+                FMParserTokenManager tkMan = new FMParserTokenManager(
+                        simpleCharacterStream);
+                
+                FMParser parser = new FMParser(parentTemplate, false, tkMan, pcfg,
+                        TemplateSpecifiedEncodingHandler.DEFAULT);
+                // We continue from the parent parser's current state:
+                parser.setupStringLiteralMode(parentTkMan, outputFormat);
+                try {
+                    dynamicValue = parser.StaticTextAndInterpolations();
+                } finally {
+                    // The parent parser continues from this parser's current state:
+                    parser.tearDownStringLiteralMode(parentTkMan);
+                }
+            } catch (ParseException e) {
+                e.setTemplateName(parentTemplate.getSourceName());
+                throw e;
+            }
+            constantValue = null;
+        }
+    }
+    
+    @Override
+    TemplateModel _eval(Environment env) throws TemplateException {
+        if (dynamicValue == null) {
+            return new SimpleScalar(value);
+        } else {
+            // This should behave like concatenating the values with `+`. Thus, an interpolated expression that
+            // returns markup promotes the result of the whole expression to markup.
+            
+            // Exactly one of these is non-null, depending on if the result will be plain text or markup, which can
+            // change during evaluation, depending on the result of the interpolations:
+            StringBuilder plainTextResult = null;
+            TemplateMarkupOutputModel<?> markupResult = null;
+            
+            for (Object part : dynamicValue) {
+                Object calcedPart =
+                        part instanceof String ? part
+                        : ((ASTInterpolation) part).calculateInterpolatedStringOrMarkup(env);
+                if (markupResult != null) {
+                    TemplateMarkupOutputModel<?> partMO = calcedPart instanceof String
+                            ? markupResult.getOutputFormat().fromPlainTextByEscaping((String) calcedPart)
+                            : (TemplateMarkupOutputModel<?>) calcedPart;
+                    markupResult = EvalUtil.concatMarkupOutputs(this, markupResult, partMO);
+                } else { // We are using `plainTextOutput` (or nothing yet)
+                    if (calcedPart instanceof String) {
+                        String partStr = (String) calcedPart;
+                        if (plainTextResult == null) {
+                            plainTextResult = new StringBuilder(partStr);
+                        } else {
+                            plainTextResult.append(partStr);
+                        }
+                    } else { // `calcedPart` is TemplateMarkupOutputModel
+                        TemplateMarkupOutputModel<?> moPart = (TemplateMarkupOutputModel<?>) calcedPart;
+                        if (plainTextResult != null) {
+                            TemplateMarkupOutputModel<?> leftHandMO = moPart.getOutputFormat()
+                                    .fromPlainTextByEscaping(plainTextResult.toString());
+                            markupResult = EvalUtil.concatMarkupOutputs(this, leftHandMO, moPart);
+                            plainTextResult = null;
+                        } else {
+                            markupResult = moPart;
+                        }
+                    }
+                }
+            } // for each part
+            return markupResult != null ? markupResult
+                    : plainTextResult != null ? new SimpleScalar(plainTextResult.toString())
+                    : SimpleScalar.EMPTY_STRING;
+        }
+    }
+
+    @Override
+    public String getAsString() {
+        return value;
+    }
+    
+    /**
+     * Tells if this is something like <tt>"${foo}"</tt>, which is usually a user mistake.
+     */
+    boolean isSingleInterpolationLiteral() {
+        return dynamicValue != null && dynamicValue.size() == 1
+                && dynamicValue.get(0) instanceof ASTInterpolation;
+    }
+    
+    @Override
+    public String getCanonicalForm() {
+        if (dynamicValue == null) {
+            return FTLUtil.toStringLiteral(value);
+        } else {
+            StringBuilder sb = new StringBuilder();
+            sb.append('"');
+            for (Object child : dynamicValue) {
+                if (child instanceof ASTInterpolation) {
+                    sb.append(((ASTInterpolation) child).getCanonicalFormInStringLiteral());
+                } else {
+                    sb.append(FTLUtil.escapeStringLiteralPart((String) child, '"'));
+                }
+            }
+            sb.append('"');
+            return sb.toString();
+        }
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return dynamicValue == null ? getCanonicalForm() : "dynamic \"...\"";
+    }
+    
+    @Override
+    boolean isLiteral() {
+        return dynamicValue == null;
+    }
+
+    @Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) {
+        ASTExpStringLiteral cloned = new ASTExpStringLiteral(value);
+        // FIXME: replacedIdentifier should be searched inside interpolatedOutput too:
+        cloned.dynamicValue = dynamicValue;
+        return cloned;
+    }
+
+    @Override
+    int getParameterCount() {
+        return dynamicValue == null ? 0 : dynamicValue.size();
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        checkIndex(idx);
+        return dynamicValue.get(idx);
+    }
+
+    private void checkIndex(int idx) {
+        if (dynamicValue == null || idx >= dynamicValue.size()) {
+            throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        checkIndex(idx);
+        return ParameterRole.VALUE_PART;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/ASTExpVariable.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/ASTExpVariable.java b/src/main/java/org/apache/freemarker/core/ASTExpVariable.java
new file mode 100644
index 0000000..b3cfbc2
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/ASTExpVariable.java
@@ -0,0 +1,105 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * 
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * AST expression node: Reference to a "top-level" (local, current namespace, global, data-model) variable
+ */
+final class ASTExpVariable extends ASTExpression {
+
+    private final String name;
+
+    ASTExpVariable(String name) {
+        this.name = name;
+    }
+
+    @Override
+    TemplateModel _eval(Environment env) throws TemplateException {
+        try {
+            return env.getVariable(name);
+        } catch (NullPointerException e) {
+            if (env == null) {
+                throw new _MiscTemplateException(
+                        "Variables are not available (certainly you are in a parse-time executed directive). "
+                        + "The name of the variable you tried to read: ", name);
+            } else {
+                throw e;
+            }
+        }
+    }
+
+    @Override
+    public String getCanonicalForm() {
+        return _StringUtil.toFTLTopLevelIdentifierReference(name);
+    }
+    
+    /**
+     * The name of the identifier without any escaping or other syntactical distortions. 
+     */
+    String getName() {
+        return name;
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return getCanonicalForm();
+    }
+
+    @Override
+    boolean isLiteral() {
+        return false;
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 0;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+
+    @Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) {
+        if (name.equals(replacedIdentifier)) {
+            if (replacementState.replacementAlreadyInUse) {
+                ASTExpression clone = replacement.deepCloneWithIdentifierReplaced(null, null, replacementState);
+                clone.copyLocationFrom(replacement);
+                return clone;
+            } else {
+                replacementState.replacementAlreadyInUse = true;
+                return replacement;
+            }
+        } else {
+            return new ASTExpVariable(name);
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/ASTExpression.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/ASTExpression.java b/src/main/java/org/apache/freemarker/core/ASTExpression.java
new file mode 100644
index 0000000..692f1bb
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/ASTExpression.java
@@ -0,0 +1,207 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * 
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.model.TemplateBooleanModel;
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateHashModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+import org.apache.freemarker.core.model.impl.beans.BeanModel;
+
+/**
+ * AST expression node superclass
+ */
+abstract class ASTExpression extends ASTNode {
+
+    /**
+     * @param env might be {@code null}, if this kind of expression can be evaluated during parsing (as opposed to
+     *     during template execution).
+     */
+    abstract TemplateModel _eval(Environment env) throws TemplateException;
+    
+    abstract boolean isLiteral();
+
+    // Used to store a constant return value for this expression. Only if it
+    // is possible, of course.
+    
+    TemplateModel constantValue;
+
+    // Hook in here to set the constant value if possible.
+    
+    @Override
+    void setLocation(Template template, int beginColumn, int beginLine, int endColumn, int endLine) {
+        super.setLocation(template, beginColumn, beginLine, endColumn, endLine);
+        if (isLiteral()) {
+            try {
+                constantValue = _eval(null);
+            } catch (Exception e) {
+            // deliberately ignore.
+            }
+        }
+    }
+
+    final TemplateModel getAsTemplateModel(Environment env) throws TemplateException {
+        return eval(env);
+    }
+    
+    final TemplateModel eval(Environment env) throws TemplateException {
+        return constantValue != null ? constantValue : _eval(env);
+    }
+    
+    String evalAndCoerceToPlainText(Environment env) throws TemplateException {
+        return EvalUtil.coerceModelToPlainText(eval(env), this, null, env);
+    }
+
+    /**
+     * @param seqTip Tip to display if the value type is not coercable, but it's sequence or collection.
+     */
+    String evalAndCoerceToPlainText(Environment env, String seqTip) throws TemplateException {
+        return EvalUtil.coerceModelToPlainText(eval(env), this, seqTip, env);
+    }
+
+    Object evalAndCoerceToStringOrMarkup(Environment env) throws TemplateException {
+        return EvalUtil.coerceModelToStringOrMarkup(eval(env), this, null, env);
+    }
+
+    /**
+     * @param seqTip Tip to display if the value type is not coercable, but it's sequence or collection.
+     */
+    Object evalAndCoerceToStringOrMarkup(Environment env, String seqTip) throws TemplateException {
+        return EvalUtil.coerceModelToStringOrMarkup(eval(env), this, seqTip, env);
+    }
+    
+    String evalAndCoerceToStringOrUnsupportedMarkup(Environment env) throws TemplateException {
+        return EvalUtil.coerceModelToStringOrUnsupportedMarkup(eval(env), this, null, env);
+    }
+
+    /**
+     * @param seqTip Tip to display if the value type is not coercable, but it's sequence or collection.
+     */
+    String evalAndCoerceToStringOrUnsupportedMarkup(Environment env, String seqTip) throws TemplateException {
+        return EvalUtil.coerceModelToStringOrUnsupportedMarkup(eval(env), this, seqTip, env);
+    }
+    
+    Number evalToNumber(Environment env) throws TemplateException {
+        TemplateModel model = eval(env);
+        return modelToNumber(model, env);
+    }
+
+    Number modelToNumber(TemplateModel model, Environment env) throws TemplateException {
+        if (model instanceof TemplateNumberModel) {
+            return EvalUtil.modelToNumber((TemplateNumberModel) model, this);
+        } else {
+            throw new NonNumericalException(this, model, env);
+        }
+    }
+    
+    boolean evalToBoolean(Environment env) throws TemplateException {
+        return evalToBoolean(env, null);
+    }
+
+    boolean evalToBoolean(Configuration cfg) throws TemplateException {
+        return evalToBoolean(null, cfg);
+    }
+
+    TemplateModel evalToNonMissing(Environment env) throws TemplateException {
+        TemplateModel result = eval(env);
+        assertNonNull(result, env);
+        return result;
+    }
+    
+    private boolean evalToBoolean(Environment env, Configuration cfg) throws TemplateException {
+        TemplateModel model = eval(env);
+        return modelToBoolean(model, env, cfg);
+    }
+    
+    boolean modelToBoolean(TemplateModel model, Environment env) throws TemplateException {
+        return modelToBoolean(model, env, null);
+    }
+
+    boolean modelToBoolean(TemplateModel model, Configuration cfg) throws TemplateException {
+        return modelToBoolean(model, null, cfg);
+    }
+    
+    private boolean modelToBoolean(TemplateModel model, Environment env, Configuration cfg) throws TemplateException {
+        if (model instanceof TemplateBooleanModel) {
+            return ((TemplateBooleanModel) model).getAsBoolean();
+        } else {
+            throw new NonBooleanException(this, model, env);
+        }
+    }
+    
+    final ASTExpression deepCloneWithIdentifierReplaced(
+            String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) {
+        ASTExpression clone = deepCloneWithIdentifierReplaced_inner(replacedIdentifier, replacement, replacementState);
+        if (clone.beginLine == 0) {
+            clone.copyLocationFrom(this);
+        }
+        return clone;
+    }
+    
+    static class ReplacemenetState {
+        /**
+         * If the replacement expression is not in use yet, we don't have to clone it.
+         */
+        boolean replacementAlreadyInUse; 
+    }
+
+    /**
+     * This should return an equivalent new expression object (or an identifier replacement expression).
+     * The position need not be filled, unless it will be different from the position of what we were cloning. 
+     */
+    protected abstract ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState);
+
+    static boolean isEmpty(TemplateModel model) throws TemplateModelException {
+        if (model instanceof BeanModel) {
+            return ((BeanModel) model).isEmpty();
+        } else if (model instanceof TemplateSequenceModel) {
+            return ((TemplateSequenceModel) model).size() == 0;
+        } else if (model instanceof TemplateScalarModel) {
+            String s = ((TemplateScalarModel) model).getAsString();
+            return (s == null || s.length() == 0);
+        } else if (model == null) {
+            return true;
+        } else if (model instanceof TemplateMarkupOutputModel) { // Note: happens just after FTL string check
+            TemplateMarkupOutputModel mo = (TemplateMarkupOutputModel) model;
+            return mo.getOutputFormat().isEmpty(mo);
+        } else if (model instanceof TemplateCollectionModel) {
+            return !((TemplateCollectionModel) model).iterator().hasNext();
+        } else if (model instanceof TemplateHashModel) {
+            return ((TemplateHashModel) model).isEmpty();
+        } else if (model instanceof TemplateNumberModel
+                || model instanceof TemplateDateModel
+                || model instanceof TemplateBooleanModel) {
+            return false;
+        } else {
+            return true;
+        }
+    }
+    
+    void assertNonNull(TemplateModel model, Environment env) throws InvalidReferenceException {
+        if (model == null) throw InvalidReferenceException.getInstance(this, env);
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/ASTHashInterpolation.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/ASTHashInterpolation.java b/src/main/java/org/apache/freemarker/core/ASTHashInterpolation.java
new file mode 100644
index 0000000..93a95c5
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/ASTHashInterpolation.java
@@ -0,0 +1,172 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * 
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.text.NumberFormat;
+import java.util.Locale;
+
+import org.apache.freemarker.core.util.FTLUtil;
+
+/**
+ * AST interpolation node: <tt>#{exp}</tt>
+ */
+final class ASTHashInterpolation extends ASTInterpolation {
+
+    private final ASTExpression expression;
+    private final boolean hasFormat;
+    private final int minFracDigits;
+    private final int maxFracDigits;
+    /** For OutputFormat-based auto-escaping */
+    private final MarkupOutputFormat autoEscapeOutputFormat;
+    private volatile FormatHolder formatCache; // creating new NumberFormat is slow operation
+
+    ASTHashInterpolation(ASTExpression expression, MarkupOutputFormat autoEscapeOutputFormat) {
+        this.expression = expression;
+        hasFormat = false;
+        minFracDigits = 0;
+        maxFracDigits = 0;
+        this.autoEscapeOutputFormat = autoEscapeOutputFormat;
+    }
+
+    ASTHashInterpolation(ASTExpression expression,
+            int minFracDigits, int maxFracDigits,
+            MarkupOutputFormat autoEscapeOutputFormat) {
+        this.expression = expression;
+        hasFormat = true;
+        this.minFracDigits = minFracDigits;
+        this.maxFracDigits = maxFracDigits;
+        this.autoEscapeOutputFormat = autoEscapeOutputFormat;
+    }
+
+    @Override
+    _ASTElement[] accept(Environment env) throws TemplateException, IOException {
+        String s = calculateInterpolatedStringOrMarkup(env);
+        Writer out = env.getOut();
+        if (autoEscapeOutputFormat != null) {
+            autoEscapeOutputFormat.output(s, out);
+        } else {
+            out.write(s);
+        }
+        return null;
+    }
+
+    @Override
+    protected String calculateInterpolatedStringOrMarkup(Environment env) throws TemplateException {
+        Number num = expression.evalToNumber(env);
+        
+        FormatHolder fmth = formatCache;  // atomic sampling
+        if (fmth == null || !fmth.locale.equals(env.getLocale())) {
+            synchronized (this) {
+                fmth = formatCache;
+                if (fmth == null || !fmth.locale.equals(env.getLocale())) {
+                    NumberFormat fmt = NumberFormat.getNumberInstance(env.getLocale());
+                    if (hasFormat) {
+                        fmt.setMinimumFractionDigits(minFracDigits);
+                        fmt.setMaximumFractionDigits(maxFracDigits);
+                    } else {
+                        fmt.setMinimumFractionDigits(0);
+                        fmt.setMaximumFractionDigits(50);
+                    }
+                    fmt.setGroupingUsed(false);
+                    formatCache = new FormatHolder(fmt, env.getLocale());
+                    fmth = formatCache;
+                }
+            }
+        }
+        // We must use Format even if hasFormat == false.
+        // Some locales may use non-Arabic digits, thus replacing the
+        // decimal separator in the result of toString() is not enough.
+        String s = fmth.format.format(num);
+        return s;
+    }
+
+    @Override
+    protected String dump(boolean canonical, boolean inStringLiteral) {
+        StringBuilder buf = new StringBuilder("#{");
+        final String exprCF = expression.getCanonicalForm();
+        buf.append(inStringLiteral ? FTLUtil.escapeStringLiteralPart(exprCF, '"') : exprCF);
+        if (hasFormat) {
+            buf.append(" ; ");
+            buf.append("m");
+            buf.append(minFracDigits);
+            buf.append("M");
+            buf.append(maxFracDigits);
+        }
+        buf.append("}");
+        return buf.toString();
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return "#{...}";
+    }
+
+    @Override
+    boolean heedsOpeningWhitespace() {
+        return true;
+    }
+
+    @Override
+    boolean heedsTrailingWhitespace() {
+        return true;
+    }
+    
+    private static class FormatHolder {
+        final NumberFormat format;
+        final Locale locale;
+        
+        FormatHolder(NumberFormat format, Locale locale) {
+            this.format = format;
+            this.locale = locale;
+        }
+    }
+
+    @Override
+    int getParameterCount() {
+        return 3;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        switch (idx) {
+        case 0: return expression;
+        case 1: return Integer.valueOf(minFracDigits);
+        case 2: return Integer.valueOf(maxFracDigits);
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        switch (idx) {
+        case 0: return ParameterRole.CONTENT;
+        case 1: return ParameterRole.MINIMUM_DECIMALS;
+        case 2: return ParameterRole.MAXIMUM_DECIMALS;
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        return false;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/ASTImplicitParent.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/ASTImplicitParent.java b/src/main/java/org/apache/freemarker/core/ASTImplicitParent.java
new file mode 100644
index 0000000..63b0f77
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/ASTImplicitParent.java
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * 
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import java.io.IOException;
+
+/**
+ * AST directive-like node, used where there's no other parent for a list of {@link _ASTElement}-s. Most often occurs as
+ * the root node of the AST.
+ */
+final class ASTImplicitParent extends _ASTElement {
+
+    ASTImplicitParent() { }
+    
+    @Override
+    _ASTElement postParseCleanup(boolean stripWhitespace)
+        throws ParseException {
+        super.postParseCleanup(stripWhitespace);
+        return getChildCount() == 1 ? getChild(0) : this;
+    }
+
+    /**
+     * Processes the contents of the internal <tt>_ASTElement</tt> list,
+     * and outputs the resulting text.
+     */
+    @Override
+    _ASTElement[] accept(Environment env)
+        throws TemplateException, IOException {
+        return getChildBuffer();
+    }
+
+    @Override
+    protected String dump(boolean canonical) {
+        if (canonical) {
+            return getChildrenCanonicalForm();
+        } else {
+            if (getParentElement() == null) {
+                return "root";
+            }
+            return getNodeTypeSymbol(); // ASTImplicitParent is uninteresting in a stack trace.
+        }
+    }
+
+    @Override
+    protected boolean isOutputCacheable() {
+        int ln = getChildCount();
+        for (int i = 0; i < ln; i++) {
+            if (!getChild(i).isOutputCacheable()) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    @Override
+    String getNodeTypeSymbol() {
+        return "#mixed_content";
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 0;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+    
+    @Override
+    boolean isIgnorable(boolean stripWhitespace) {
+        return getChildCount() == 0;
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        return false;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/ASTInterpolation.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/ASTInterpolation.java b/src/main/java/org/apache/freemarker/core/ASTInterpolation.java
new file mode 100644
index 0000000..d303642
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/ASTInterpolation.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * 
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.freemarker.core;
+
+/**
+ * AST interpolation node superclass.
+ */
+abstract class ASTInterpolation extends _ASTElement {
+
+    protected abstract String dump(boolean canonical, boolean inStringLiteral);
+
+    @Override
+    protected final String dump(boolean canonical) {
+        return dump(canonical, false);
+    }
+    
+    final String getCanonicalFormInStringLiteral() {
+        return dump(true, true);
+    }
+
+    /**
+     * Returns the already type-converted value that this interpolation will insert into the output.
+     * 
+     * @return A {@link String} or {@link TemplateMarkupOutputModel}. Not {@code null}.
+     */
+    protected abstract Object calculateInterpolatedStringOrMarkup(Environment env) throws TemplateException;
+
+    @Override
+    boolean isShownInStackTrace() {
+        return true;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/ASTNode.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/ASTNode.java b/src/main/java/org/apache/freemarker/core/ASTNode.java
new file mode 100644
index 0000000..19dd62d
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/ASTNode.java
@@ -0,0 +1,233 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * 
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+/**
+ * AST node: The superclass of all AST nodes
+ */
+abstract class ASTNode {
+    
+    private Template template;
+    int beginColumn, beginLine, endColumn, endLine;
+    
+    /** This is needed for an ?eval hack; the expression AST nodes will be the descendants of the template, however,
+     *  we can't give their position in the template, only in the dynamic string that's evaluated. That's signaled
+     *  by a negative line numbers, starting from this constant as line 1. */
+    static final int RUNTIME_EVAL_LINE_DISPLACEMENT = -1000000000;  
+
+    final void setLocation(Template template, Token begin, Token end) {
+        setLocation(template, begin.beginColumn, begin.beginLine, end.endColumn, end.endLine);
+    }
+
+    final void setLocation(Template template, Token tagBegin, Token tagEnd, TemplateElements children) {
+        _ASTElement lastChild = children.getLast();
+        if (lastChild != null) {
+            // [<#if exp>children]<#else>
+            setLocation(template, tagBegin, lastChild);
+        } else {
+            // [<#if exp>]<#else>
+            setLocation(template, tagBegin, tagEnd);
+        }
+    }
+    
+    final void setLocation(Template template, Token begin, ASTNode end) {
+        setLocation(template, begin.beginColumn, begin.beginLine, end.endColumn, end.endLine);
+    }
+    
+    final void setLocation(Template template, ASTNode begin, Token end) {
+        setLocation(template, begin.beginColumn, begin.beginLine, end.endColumn, end.endLine);
+    }
+
+    final void setLocation(Template template, ASTNode begin, ASTNode end) {
+        setLocation(template, begin.beginColumn, begin.beginLine, end.endColumn, end.endLine);
+    }
+
+    void setLocation(Template template, int beginColumn, int beginLine, int endColumn, int endLine) {
+        this.template = template;
+        this.beginColumn = beginColumn;
+        this.beginLine = beginLine;
+        this.endColumn = endColumn;
+        this.endLine = endLine;
+    }
+    
+    public final int getBeginColumn() {
+        return beginColumn;
+    }
+
+    public final int getBeginLine() {
+        return beginLine;
+    }
+
+    public final int getEndColumn() {
+        return endColumn;
+    }
+
+    public final int getEndLine() {
+        return endLine;
+    }
+
+    /**
+     * Returns a string that indicates
+     * where in the template source, this object is.
+     */
+    public String getStartLocation() {
+        return MessageUtil.formatLocationForEvaluationError(template, beginLine, beginColumn);
+    }
+
+    /**
+     * As of 2.3.20. the same as {@link #getStartLocation}. Meant to be used where there's a risk of XSS
+     * when viewing error messages.
+     */
+    public String getStartLocationQuoted() {
+        return getStartLocation();
+    }
+
+    public String getEndLocation() {
+        return MessageUtil.formatLocationForEvaluationError(template, endLine, endColumn);
+    }
+
+    /**
+     * As of 2.3.20. the same as {@link #getEndLocation}. Meant to be used where there's a risk of XSS
+     * when viewing error messages.
+     */
+    public String getEndLocationQuoted() {
+        return getEndLocation();
+    }
+    
+    public final String getSource() {
+        String s;
+        if (template != null) {
+            s = template.getSource(beginColumn, beginLine, endColumn, endLine);
+        } else {
+            s = null;
+        }
+
+        // Can't just return null for backward-compatibility... 
+        return s != null ? s : getCanonicalForm();
+    }
+
+    @Override
+    public String toString() {
+        String s;
+    	try {
+    		s = getSource();
+    	} catch (Exception e) { // REVISIT: A bit of a hack? (JR)
+    	    s = null;
+    	}
+    	return s != null ? s : getCanonicalForm();
+    }
+
+    /**
+     * @return whether the point in the template file specified by the 
+     * column and line numbers is contained within this template object.
+     */
+    public boolean contains(int column, int line) {
+        if (line < beginLine || line > endLine) {
+            return false;
+        }
+        if (line == beginLine) {
+            if (column < beginColumn) {
+                return false;
+            }
+        }
+        if (line == endLine) {
+            if (column > endColumn) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    public Template getTemplate() {
+        return template;
+    }
+    
+    ASTNode copyLocationFrom(ASTNode from) {
+        template = from.template;
+        beginColumn = from.beginColumn;
+        beginLine = from.beginLine;
+        endColumn = from.endColumn;
+        endLine = from.endLine;
+        return this;
+    }    
+
+    /**
+     * FTL generated from the AST of the node, which must be parseable to an AST that does the same as the original
+     * source, assuming we turn off automatic white-space removal when parsing the canonical form.
+     * 
+     * @see _ASTElement#getDescription()
+     * @see #getNodeTypeSymbol()
+     */
+    abstract public String getCanonicalForm();
+    
+    /**
+     * A very sort single-line string that describes what kind of AST node this is, without describing any 
+     * embedded expression or child element. Examples: {@code "#if"}, {@code "+"}, <tt>"${...}</tt>. These values should
+     * be suitable as tree node labels in a tree view. Yet, they should be consistent and complete enough so that an AST
+     * that is equivalent with the original could be reconstructed from the tree view. Thus, for literal values that are
+     * leaf nodes the symbols should be the canonical form of value.
+     * 
+     * Note that {@link _ASTElement#getDescription()} has similar role, only it doesn't go under the element level
+     * (i.e. down to the expression level), instead it always prints the embedded expressions itself.
+     * 
+     * @see #getCanonicalForm()
+     * @see _ASTElement#getDescription()
+     */
+    abstract String getNodeTypeSymbol();
+    
+    /**
+     * Returns highest valid parameter index + 1. So one should scan indexes with {@link #getParameterValue(int)}
+     * starting from 0 up until but excluding this. For example, for the binary "+" operator this will give 2, so the
+     * legal indexes are 0 and 1. Note that if a parameter is optional in a template-object-type and happens to be
+     * omitted in an instance, this will still return the same value and the value of that parameter will be
+     * {@code null}.
+     */
+    abstract int getParameterCount();
+    
+    /**
+     * Returns the value of the parameter identified by the index. For example, the binary "+" operator will have an
+     * LHO {@link ASTExpression} at index 0, and and RHO {@link ASTExpression} at index 1. Or, the binary "." operator will
+     * have an LHO {@link ASTExpression} at index 0, and an RHO {@link String}(!) at index 1. Or, the {@code #include}
+     * directive will have a path {@link ASTExpression} at index 0, a "parse" {@link ASTExpression} at index 1, etc.
+     * 
+     * <p>The index value doesn't correspond to the source-code location in general. It's an arbitrary identifier
+     * that corresponds to the role of the parameter instead. This also means that when a parameter is omitted, the
+     * index of the other parameters won't shift.
+     *
+     *  @return {@code null} or any kind of {@link Object}, very often an {@link ASTExpression}. However, if there's
+     *      a {@link ASTNode} stored inside the returned value, it must itself be be a {@link ASTNode}
+     *      too, otherwise the AST couldn't be (easily) fully traversed. That is, non-{@link ASTNode} values
+     *      can only be used for leafs. 
+     *  
+     *  @throws IndexOutOfBoundsException if {@code idx} is less than 0 or not less than {@link #getParameterCount()}. 
+     */
+    abstract Object getParameterValue(int idx);
+
+    /**
+     *  Returns the role of the parameter at the given index, like {@link ParameterRole#LEFT_HAND_OPERAND}.
+     *  
+     *  As of this writing (2013-06-17), for directive parameters it will always give {@link ParameterRole#UNKNOWN},
+     *  because there was no need to be more specific so far. This should be improved as need.
+     *  
+     *  @throws IndexOutOfBoundsException if {@code idx} is less than 0 or not less than {@link #getParameterCount()}. 
+     */
+    abstract ParameterRole getParameterRole(int idx);
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/ASTStaticText.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/ASTStaticText.java b/src/main/java/org/apache/freemarker/core/ASTStaticText.java
new file mode 100644
index 0000000..2d68e30
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/ASTStaticText.java
@@ -0,0 +1,408 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * 
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import java.io.IOException;
+
+import org.apache.freemarker.core.util._CollectionUtil;
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * AST node representing static text.
+ */
+final class ASTStaticText extends _ASTElement {
+    
+    // We're using char[] instead of String for storing the text block because
+    // Writer.write(String) involves copying the String contents to a char[] 
+    // using String.getChars(), and then calling Writer.write(char[]). By
+    // using Writer.write(char[]) directly, we avoid array copying on each 
+    // write. 
+    private char[] text;
+    private final boolean unparsed;
+
+    public ASTStaticText(String text) {
+        this(text, false);
+    }
+
+    public ASTStaticText(String text, boolean unparsed) {
+        this(text.toCharArray(), unparsed);
+    }
+
+    ASTStaticText(char[] text, boolean unparsed) {
+        this.text = text;
+        this.unparsed = unparsed;
+    }
+    
+    void replaceText(String text) {
+        this.text = text.toCharArray();
+    }
+
+    /**
+     * Simply outputs the text.
+     * 
+     * @deprecated This is an internal API; don't call or override it.
+     */
+    @Deprecated
+    @Override
+    public _ASTElement[] accept(Environment env)
+    throws IOException {
+        env.getOut().write(text);
+        return null;
+    }
+
+    @Override
+    protected String dump(boolean canonical) {
+        if (canonical) {
+            String text = new String(this.text);
+            if (unparsed) {
+                return "<#noparse>" + text + "</#noparse>";
+            }
+            return text;
+        } else {
+            return "text " + _StringUtil.jQuote(new String(text));
+        }
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return "#text";
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 1;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        if (idx != 0) throw new IndexOutOfBoundsException();
+        return new String(text);
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        if (idx != 0) throw new IndexOutOfBoundsException();
+        return ParameterRole.CONTENT;
+    }
+
+    @Override
+    _ASTElement postParseCleanup(boolean stripWhitespace) {
+        if (text.length == 0) return this;
+        int openingCharsToStrip = 0, trailingCharsToStrip = 0;
+        boolean deliberateLeftTrim = deliberateLeftTrim();
+        boolean deliberateRightTrim = deliberateRightTrim();
+        if (!stripWhitespace || text.length == 0 ) {
+            return this;
+        }
+        _ASTElement parentElement = getParentElement();
+        if (isTopLevelTextIfParentIs(parentElement) && previousSibling() == null) {
+            return this;
+        }
+        if (!deliberateLeftTrim) {
+            trailingCharsToStrip = trailingCharsToStrip();
+        }
+        if (!deliberateRightTrim) {
+            openingCharsToStrip = openingCharsToStrip();
+        }
+        if (openingCharsToStrip == 0 && trailingCharsToStrip == 0) {
+            return this;
+        }
+        text = substring(text, openingCharsToStrip, text.length - trailingCharsToStrip);
+        if (openingCharsToStrip > 0) {
+            beginLine++;
+            beginColumn = 1;
+        }
+        if (trailingCharsToStrip > 0) {
+            endColumn = 0;
+        }
+        return this;
+    }
+    
+    /**
+     * Scans forward the nodes on the same line to see whether there is a 
+     * deliberate left trim in effect. Returns true if the left trim was present.
+     */
+    private boolean deliberateLeftTrim() {
+        boolean result = false;
+        for (_ASTElement elem = nextTerminalNode();
+             elem != null && elem.beginLine == endLine;
+             elem = elem.nextTerminalNode()) {
+            if (elem instanceof ASTDirTOrTrOrTl) {
+                ASTDirTOrTrOrTl ti = (ASTDirTOrTrOrTl) elem;
+                if (!ti.left && !ti.right) {
+                    result = true;
+                }
+                if (ti.left) {
+                    result = true;
+                    int lastNewLineIndex = lastNewLineIndex();
+                    if (lastNewLineIndex >= 0  || beginColumn == 1) {
+                        char[] firstPart = substring(text, 0, lastNewLineIndex + 1);
+                        char[] lastLine = substring(text, 1 + lastNewLineIndex); 
+                        if (_StringUtil.isTrimmableToEmpty(lastLine)) {
+                            text = firstPart;
+                            endColumn = 0;
+                        } else {
+                            int i = 0;
+                            while (Character.isWhitespace(lastLine[i])) {
+                                i++;
+                            }
+                            char[] printablePart = substring(lastLine, i);
+                            text = concat(firstPart, printablePart);
+                        }
+                    }
+                }
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Checks for the presence of a t or rt directive on the 
+     * same line. Returns true if the right trim directive was present.
+     */
+    private boolean deliberateRightTrim() {
+        boolean result = false;
+        for (_ASTElement elem = prevTerminalNode();
+             elem != null && elem.endLine == beginLine;
+             elem = elem.prevTerminalNode()) {
+            if (elem instanceof ASTDirTOrTrOrTl) {
+                ASTDirTOrTrOrTl ti = (ASTDirTOrTrOrTl) elem;
+                if (!ti.left && !ti.right) {
+                    result = true;
+                }
+                if (ti.right) {
+                    result = true;
+                    int firstLineIndex = firstNewLineIndex() + 1;
+                    if (firstLineIndex == 0) {
+                        return false;
+                    }
+                    if (text.length > firstLineIndex 
+                        && text[firstLineIndex - 1] == '\r' 
+                        && text[firstLineIndex] == '\n') {
+                        firstLineIndex++;
+                    }
+                    char[] trailingPart = substring(text, firstLineIndex);
+                    char[] openingPart = substring(text, 0, firstLineIndex);
+                    if (_StringUtil.isTrimmableToEmpty(openingPart)) {
+                        text = trailingPart;
+                        beginLine++;
+                        beginColumn = 1;
+                    } else {
+                        int lastNonWS = openingPart.length - 1;
+                        while (Character.isWhitespace(text[lastNonWS])) {
+                            lastNonWS--;
+                        }
+                        char[] printablePart = substring(text, 0, lastNonWS + 1);
+                        if (_StringUtil.isTrimmableToEmpty(trailingPart)) {
+                        // THIS BLOCK IS HEINOUS! THERE MUST BE A BETTER WAY! REVISIT (JR)
+                            boolean trimTrailingPart = true;
+                            for (_ASTElement te = nextTerminalNode();
+                                 te != null && te.beginLine == endLine;
+                                 te = te.nextTerminalNode()) {
+                                if (te.heedsOpeningWhitespace()) {
+                                    trimTrailingPart = false;
+                                }
+                                if (te instanceof ASTDirTOrTrOrTl && ((ASTDirTOrTrOrTl) te).left) {
+                                    trimTrailingPart = true;
+                                    break;
+                                }
+                            }
+                            if (trimTrailingPart) trailingPart = _CollectionUtil.EMPTY_CHAR_ARRAY;
+                        }
+                        text = concat(printablePart, trailingPart);
+                    }
+                }
+            }
+        }
+        return result;
+    }
+    
+    private int firstNewLineIndex() {
+        char[] text = this.text;
+        for (int i = 0; i < text.length; i++) {
+            char c = text[i];
+            if (c == '\r' || c == '\n' ) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    private int lastNewLineIndex() {
+        char[] text = this.text;
+        for (int i = text.length - 1; i >= 0; i--) {
+            char c = text[i];
+            if (c == '\r' || c == '\n' ) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    /**
+     * figures out how many opening whitespace characters to strip
+     * in the post-parse cleanup phase.
+     */
+    private int openingCharsToStrip() {
+        int newlineIndex = firstNewLineIndex();
+        if (newlineIndex == -1 && beginColumn != 1) {
+            return 0;
+        }
+        ++newlineIndex;
+        if (text.length > newlineIndex) {
+            if (newlineIndex > 0 && text[newlineIndex - 1] == '\r' && text[newlineIndex] == '\n') {
+                ++newlineIndex;
+            }
+        }
+        if (!_StringUtil.isTrimmableToEmpty(text, 0, newlineIndex)) {
+            return 0;
+        }
+        // We look at the preceding elements on the line to see if we should
+        // strip the opening newline and any whitespace preceding it.
+        for (_ASTElement elem = prevTerminalNode();
+             elem != null && elem.endLine == beginLine;
+             elem = elem.prevTerminalNode()) {
+            if (elem.heedsOpeningWhitespace()) {
+                return 0;
+            }
+        }
+        return newlineIndex;
+    }
+
+    /**
+     * figures out how many trailing whitespace characters to strip
+     * in the post-parse cleanup phase.
+     */
+    private int trailingCharsToStrip() {
+        int lastNewlineIndex = lastNewLineIndex();
+        if (lastNewlineIndex == -1 && beginColumn != 1) {
+            return 0;
+        }
+        if (!_StringUtil.isTrimmableToEmpty(text, lastNewlineIndex + 1)) {
+            return 0;
+        }
+        // We look at the elements afterward on the same line to see if we should
+        // strip any whitespace after the last newline
+        for (_ASTElement elem = nextTerminalNode();
+             elem != null && elem.beginLine == endLine;
+             elem = elem.nextTerminalNode()) {
+            if (elem.heedsTrailingWhitespace()) {
+                return 0;
+            }
+        }
+        return text.length - (lastNewlineIndex + 1);
+    }
+
+    @Override
+    boolean heedsTrailingWhitespace() {
+        if (isIgnorable(true)) {
+            return false;
+        }
+        for (char c : text) {
+            if (c == '\n' || c == '\r') {
+                return false;
+            }
+            if (!Character.isWhitespace(c)) {
+                return true;
+            }
+        }
+        return true;
+    }
+
+    @Override
+    boolean heedsOpeningWhitespace() {
+        if (isIgnorable(true)) {
+            return false;
+        }
+        for (int i = text.length - 1; i >= 0; i--) {
+            char c = text[i];
+            if (c == '\n' || c == '\r') {
+                return false;
+            }
+            if (!Character.isWhitespace(c)) {
+                return true;
+            }
+        }
+        return true;
+    }
+
+    @Override
+    boolean isIgnorable(boolean stripWhitespace) {
+        if (text == null || text.length == 0) {
+            return true;
+        }
+        if (stripWhitespace) {
+            if (!_StringUtil.isTrimmableToEmpty(text)) {
+                return false;
+            }
+            _ASTElement parentElement = getParentElement();
+            boolean atTopLevel = isTopLevelTextIfParentIs(parentElement);
+            _ASTElement prevSibling = previousSibling();
+            _ASTElement nextSibling = nextSibling();
+            return ((prevSibling == null && atTopLevel) || nonOutputtingType(prevSibling))
+                    && ((nextSibling == null && atTopLevel) || nonOutputtingType(nextSibling));
+        } else {
+            return false;
+        }
+    }
+
+    private boolean isTopLevelTextIfParentIs(_ASTElement parentElement) {
+        return parentElement == null
+                || parentElement.getParentElement() == null && parentElement instanceof ASTImplicitParent;
+    }
+    
+
+    private boolean nonOutputtingType(_ASTElement element) {
+        return (element instanceof ASTDirMacro ||
+                element instanceof ASTDirAssignment || 
+                element instanceof ASTDirAssignmentsContainer ||
+                element instanceof ASTDirSetting ||
+                element instanceof ASTDirImport ||
+                element instanceof ASTComment);
+    }
+
+    private static char[] substring(char[] c, int from, int to) {
+        char[] c2 = new char[to - from];
+        System.arraycopy(c, from, c2, 0, c2.length);
+        return c2;
+    }
+    
+    private static char[] substring(char[] c, int from) {
+        return substring(c, from, c.length);
+    }
+    
+    private static char[] concat(char[] c1, char[] c2) {
+        char[] c = new char[c1.length + c2.length];
+        System.arraycopy(c1, 0, c, 0, c1.length);
+        System.arraycopy(c2, 0, c, c1.length, c2.length);
+        return c;
+    }
+    
+    @Override
+    boolean isOutputCacheable() {
+        return true;
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        return false;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/AliasTargetTemplateValueFormatException.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/AliasTargetTemplateValueFormatException.java b/src/main/java/org/apache/freemarker/core/AliasTargetTemplateValueFormatException.java
new file mode 100644
index 0000000..705346a
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/AliasTargetTemplateValueFormatException.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * 
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.freemarker.core;
+
+/**
+ * Can't create a template format that the template format refers to (typically thrown by alias template formats).
+ * 
+ * @since 2.3.24
+ */
+class AliasTargetTemplateValueFormatException extends TemplateValueFormatException {
+
+    public AliasTargetTemplateValueFormatException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public AliasTargetTemplateValueFormatException(String message) {
+        super(message);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/AliasTemplateDateFormatFactory.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/AliasTemplateDateFormatFactory.java b/src/main/java/org/apache/freemarker/core/AliasTemplateDateFormatFactory.java
new file mode 100644
index 0000000..2334680
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/AliasTemplateDateFormatFactory.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * 
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.freemarker.core;
+
+import java.util.Locale;
+import java.util.Map;
+import java.util.TimeZone;
+
+import org.apache.freemarker.core.util._StringUtil;
+import org.apache.freemarker.core.util._LocaleUtil;
+
+/**
+ * Creates an alias to another format, so that the format can be referred to with a simple name in the template, rather
+ * than as a concrete pattern or other kind of format string.
+ * 
+ * @since 2.3.24
+ */
+public final class AliasTemplateDateFormatFactory extends TemplateDateFormatFactory {
+
+    private final String defaultTargetFormatString;
+    private final Map<Locale, String> localizedTargetFormatStrings;
+
+    /**
+     * @param targetFormatString
+     *            The format string this format will be an alias to.
+     */
+    public AliasTemplateDateFormatFactory(String targetFormatString) {
+        defaultTargetFormatString = targetFormatString;
+        localizedTargetFormatStrings = null;
+    }
+
+    /**
+     * @param defaultTargetFormatString
+     *            The format string this format will be an alias to if there's no locale-specific format string for the
+     *            requested locale in {@code localizedTargetFormatStrings}
+     * @param localizedTargetFormatStrings
+     *            Maps {@link Locale}-s to format strings. If the desired locale doesn't occur in the map, a less
+     *            specific locale is tried, repeatedly until only the language part remains. For example, if locale is
+     *            {@code new Locale("en", "US", "Linux")}, then these keys will be attempted untol a match is found, in
+     *            this order: {@code new Locale("en", "US", "Linux")}, {@code new Locale("en", "US")},
+     *            {@code new Locale("en")}. If there's still no matching key, the value of the
+     *            {@code targetFormatString} will be used.
+     */
+    public AliasTemplateDateFormatFactory(
+            String defaultTargetFormatString, Map<Locale, String> localizedTargetFormatStrings) {
+        this.defaultTargetFormatString = defaultTargetFormatString;
+        this.localizedTargetFormatStrings = localizedTargetFormatStrings;
+    }
+    
+    @Override
+    public TemplateDateFormat get(String params, int dateType, Locale locale, TimeZone timeZone, boolean zonelessInput,
+            Environment env) throws TemplateValueFormatException {
+        TemplateFormatUtil.checkHasNoParameters(params);
+        try {
+            String targetFormatString;
+            if (localizedTargetFormatStrings != null) {
+                Locale lookupLocale = locale;
+                targetFormatString = localizedTargetFormatStrings.get(lookupLocale);
+                while (targetFormatString == null
+                        && (lookupLocale = _LocaleUtil.getLessSpecificLocale(lookupLocale)) != null) {
+                    targetFormatString = localizedTargetFormatStrings.get(lookupLocale);
+                }
+            } else {
+                targetFormatString = null;
+            }
+            if (targetFormatString == null) {
+                targetFormatString = defaultTargetFormatString;
+            }
+            return env.getTemplateDateFormat(targetFormatString, dateType, locale, timeZone, zonelessInput);
+        } catch (TemplateValueFormatException e) {
+            throw new AliasTargetTemplateValueFormatException("Failed to create format based on target format string,  "
+                    + _StringUtil.jQuote(params) + ". Reason given: " + e.getMessage(), e);
+        }
+    }
+
+}


Mime
View raw message