freemarker-notifications mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ddek...@apache.org
Subject [49/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:16 GMT
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/ASTDirOutputFormat.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/ASTDirOutputFormat.java b/src/main/java/org/apache/freemarker/core/ASTDirOutputFormat.java
new file mode 100644
index 0000000..162ec1e
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/ASTDirOutputFormat.java
@@ -0,0 +1,85 @@
+/*
+ * 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 node: {@code #outputformat}.
+ */
+final class ASTDirOutputFormat extends _ASTElement {
+    
+    private final ASTExpression paramExp;
+
+    ASTDirOutputFormat(TemplateElements children, ASTExpression paramExp) { 
+        this.paramExp = paramExp; 
+        setChildren(children);
+    }
+
+    @Override
+    _ASTElement[] accept(Environment env) throws TemplateException, IOException {
+        return getChildBuffer();
+    }
+
+    @Override
+    protected String dump(boolean canonical) {
+        if (canonical) {
+            return "<" + getNodeTypeSymbol() + " \"" + paramExp.getCanonicalForm() + "\">"
+                    + getChildrenCanonicalForm() + "</" + getNodeTypeSymbol() + ">";
+        } else {
+            return getNodeTypeSymbol();
+        }
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return "#outputformat";
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 1;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        if (idx == 0) return paramExp;
+        else
+            throw new IndexOutOfBoundsException();
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        if (idx == 0) return ParameterRole.VALUE;
+        else
+            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/ASTDirRecover.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/ASTDirRecover.java b/src/main/java/org/apache/freemarker/core/ASTDirRecover.java
new file mode 100644
index 0000000..33ef932
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/ASTDirRecover.java
@@ -0,0 +1,75 @@
+/*
+ * 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 node: {@code #recover}.
+ */
+final class ASTDirRecover extends _ASTElement {
+    
+    ASTDirRecover(TemplateElements children) {
+        setChildren(children);
+    }
+
+    @Override
+    _ASTElement[] accept(Environment env) throws TemplateException, IOException {
+        return getChildBuffer();
+    }
+
+    @Override
+    protected String dump(boolean canonical) {
+        if (canonical) {
+            StringBuilder buf = new StringBuilder();
+            buf.append('<').append(getNodeTypeSymbol()).append('>');
+            buf.append(getChildrenCanonicalForm());            
+            return buf.toString();
+        } else {
+            return getNodeTypeSymbol();
+        }
+    }
+
+    @Override
+    String getNodeTypeSymbol() {
+        return "#recover";
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 0;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        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/ASTDirRecurse.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/ASTDirRecurse.java b/src/main/java/org/apache/freemarker/core/ASTDirRecurse.java
new file mode 100644
index 0000000..8597e92
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/ASTDirRecurse.java
@@ -0,0 +1,131 @@
+/*
+ * 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.model.TemplateHashModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateNodeModel;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+import org.apache.freemarker.core.model.impl.SimpleSequence;
+
+
+/**
+ * AST directive node: {@code #recurse}.
+ */
+final class ASTDirRecurse extends _ASTElement {
+    
+    ASTExpression targetNode, namespaces;
+    
+    ASTDirRecurse(ASTExpression targetNode, ASTExpression namespaces) {
+        this.targetNode = targetNode;
+        this.namespaces = namespaces;
+    }
+
+    @Override
+    _ASTElement[] accept(Environment env) throws IOException, TemplateException {
+        TemplateModel node = targetNode == null ? null : targetNode.eval(env);
+        if (node != null && !(node instanceof TemplateNodeModel)) {
+            throw new NonNodeException(targetNode, node, "node", env);
+        }
+        
+        TemplateModel nss = namespaces == null ? null : namespaces.eval(env);
+        if (namespaces instanceof ASTExpStringLiteral) {
+            nss = env.importLib(((TemplateScalarModel) nss).getAsString(), null);
+        } else if (namespaces instanceof ASTExpListLiteral) {
+            nss = ((ASTExpListLiteral) namespaces).evaluateStringsToNamespaces(env);
+        }
+        if (nss != null) {
+            if (nss instanceof TemplateHashModel) {
+                SimpleSequence ss = new SimpleSequence(1);
+                ss.add(nss);
+                nss = ss;
+            } else if (!(nss instanceof TemplateSequenceModel)) {
+                if (namespaces != null) {
+                    throw new NonSequenceException(namespaces, nss, env);
+                } else {
+                    // Should not occur
+                    throw new _MiscTemplateException(env, "Expecting a sequence of namespaces after \"using\"");
+                }
+            }
+        }
+        
+        env.recurse((TemplateNodeModel) node, (TemplateSequenceModel) nss);
+        return null;
+    }
+
+    @Override
+    protected String dump(boolean canonical) {
+        StringBuilder sb = new StringBuilder();
+        if (canonical) sb.append('<');
+        sb.append(getNodeTypeSymbol());
+        if (targetNode != null) {
+            sb.append(' ');
+            sb.append(targetNode.getCanonicalForm());
+        }
+        if (namespaces != null) {
+            sb.append(" using ");
+            sb.append(namespaces.getCanonicalForm());
+        }
+        if (canonical) sb.append("/>");
+        return sb.toString();
+    }
+
+    @Override
+    String getNodeTypeSymbol() {
+        return "#recurse";
+    }
+
+    @Override
+    int getParameterCount() {
+        return 2;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        switch (idx) {
+        case 0: return targetNode;
+        case 1: return namespaces;
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        switch (idx) {
+        case 0: return ParameterRole.NODE;
+        case 1: return ParameterRole.NAMESPACE;
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        return false;
+    }
+    
+    @Override
+    boolean isShownInStackTrace() {
+        return true;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/ASTDirReturn.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/ASTDirReturn.java b/src/main/java/org/apache/freemarker/core/ASTDirReturn.java
new file mode 100644
index 0000000..afa7d56
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/ASTDirReturn.java
@@ -0,0 +1,91 @@
+/*
+ * 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 directive node: {@code #return}.
+ */
+public final class ASTDirReturn extends _ASTElement {
+
+    private ASTExpression exp;
+
+    ASTDirReturn(ASTExpression exp) {
+        this.exp = exp;
+    }
+
+    @Override
+    _ASTElement[] accept(Environment env) throws TemplateException {
+        if (exp != null) {
+            env.setLastReturnValue(exp.eval(env));
+        }
+        if (nextSibling() == null && getParentElement() instanceof ASTDirMacro) {
+            // Avoid unnecessary exception throwing 
+            return null;
+        }
+        throw Return.INSTANCE;
+    }
+
+    @Override
+    protected String dump(boolean canonical) {
+        StringBuilder sb = new StringBuilder();
+        if (canonical) sb.append('<');
+        sb.append(getNodeTypeSymbol());
+        if (exp != null) {
+            sb.append(' ');
+            sb.append(exp.getCanonicalForm());
+        }
+        if (canonical) sb.append("/>");
+        return sb.toString();
+    }
+
+    @Override
+    String getNodeTypeSymbol() {
+        return "#return";
+    }
+    
+    public static class Return extends RuntimeException {
+        static final Return INSTANCE = new Return();
+        private Return() {
+        }
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 1;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        if (idx != 0) throw new IndexOutOfBoundsException();
+        return exp;
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        if (idx != 0) throw new IndexOutOfBoundsException();
+        return ParameterRole.VALUE;
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        return false;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/ASTDirSep.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/ASTDirSep.java b/src/main/java/org/apache/freemarker/core/ASTDirSep.java
new file mode 100644
index 0000000..fe181ab
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/ASTDirSep.java
@@ -0,0 +1,89 @@
+/*
+ * 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.ASTDirList.IterationContext;
+
+/**
+ * AST directive node: {@code #sep}.
+ */
+class ASTDirSep extends _ASTElement {
+
+    public ASTDirSep(TemplateElements children) {
+        setChildren(children);
+    }
+
+    @Override
+    _ASTElement[] accept(Environment env) throws TemplateException, IOException {
+        final IterationContext iterCtx = ASTDirList.findEnclosingIterationContext(env, null);
+        if (iterCtx == null) {
+            // The parser should prevent this situation
+            throw new _MiscTemplateException(env,
+                    getNodeTypeSymbol(), " without iteration in context");
+        }
+        
+        if (iterCtx.hasNext()) {
+            return getChildBuffer();
+        }
+        return null;
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        return false;
+    }
+
+    @Override
+    protected String dump(boolean canonical) {
+        StringBuilder sb = new StringBuilder();
+        if (canonical) sb.append('<');
+        sb.append(getNodeTypeSymbol());
+        if (canonical) {
+            sb.append('>');
+            sb.append(getChildrenCanonicalForm());
+            sb.append("</");
+            sb.append(getNodeTypeSymbol());
+            sb.append('>');
+        }
+        return sb.toString();
+    }
+
+    @Override
+    String getNodeTypeSymbol() {
+        return "#sep";
+    }
+
+    @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/ASTDirSetting.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/ASTDirSetting.java b/src/main/java/org/apache/freemarker/core/ASTDirSetting.java
new file mode 100644
index 0000000..5554599
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/ASTDirSetting.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.util.Arrays;
+
+import org.apache.freemarker.core.model.TemplateBooleanModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * AST directive node: {@code #setting}.
+ */
+final class ASTDirSetting extends _ASTElement {
+
+    private final String key;
+    private final ASTExpression value;
+    
+    static final String[] SETTING_NAMES = new String[] {
+            // Must be sorted alphabetically!
+            Configurable.BOOLEAN_FORMAT_KEY_CAMEL_CASE,
+            Configurable.BOOLEAN_FORMAT_KEY_SNAKE_CASE,
+            Configurable.DATE_FORMAT_KEY_CAMEL_CASE,
+            Configurable.DATE_FORMAT_KEY_SNAKE_CASE,
+            Configurable.DATETIME_FORMAT_KEY_CAMEL_CASE,
+            Configurable.DATETIME_FORMAT_KEY_SNAKE_CASE,
+            Configurable.LOCALE_KEY,
+            Configurable.NUMBER_FORMAT_KEY_CAMEL_CASE,
+            Configurable.NUMBER_FORMAT_KEY_SNAKE_CASE,
+            Configurable.OUTPUT_ENCODING_KEY_CAMEL_CASE,
+            Configurable.OUTPUT_ENCODING_KEY_SNAKE_CASE,
+            Configurable.SQL_DATE_AND_TIME_TIME_ZONE_KEY_CAMEL_CASE,
+            Configurable.SQL_DATE_AND_TIME_TIME_ZONE_KEY,
+            Configurable.TIME_FORMAT_KEY_CAMEL_CASE,
+            Configurable.TIME_ZONE_KEY_CAMEL_CASE,
+            Configurable.TIME_FORMAT_KEY_SNAKE_CASE,
+            Configurable.TIME_ZONE_KEY_SNAKE_CASE,
+            Configurable.URL_ESCAPING_CHARSET_KEY_CAMEL_CASE,
+            Configurable.URL_ESCAPING_CHARSET_KEY_SNAKE_CASE
+    };
+
+    ASTDirSetting(Token keyTk, FMParserTokenManager tokenManager, ASTExpression value, Configuration cfg)
+            throws ParseException {
+        String key = keyTk.image;
+        if (Arrays.binarySearch(SETTING_NAMES, key) < 0) {
+            StringBuilder sb = new StringBuilder();
+            if (_TemplateAPI.getConfigurationSettingNames(cfg, true).contains(key)
+                    || _TemplateAPI.getConfigurationSettingNames(cfg, false).contains(key)) {
+                sb.append("The setting name is recognized, but changing this setting from inside a template isn't "
+                        + "supported.");                
+            } else {
+                sb.append("Unknown setting name: ");
+                sb.append(_StringUtil.jQuote(key)).append(".");
+                sb.append(" The allowed setting names are: ");
+
+                int shownNamingConvention;
+                {
+                    int namingConvention = tokenManager.namingConvention;
+                    shownNamingConvention = namingConvention != Configuration.AUTO_DETECT_NAMING_CONVENTION
+                            ? namingConvention : Configuration.LEGACY_NAMING_CONVENTION /* [2.4] CAMEL_CASE */; 
+                }
+                
+                boolean first = true;
+                for (String correctName : SETTING_NAMES) {
+                    int correctNameNamingConvention = _StringUtil.getIdentifierNamingConvention(correctName);
+                    if (shownNamingConvention == Configuration.CAMEL_CASE_NAMING_CONVENTION
+                            ? correctNameNamingConvention != Configuration.LEGACY_NAMING_CONVENTION
+                            : correctNameNamingConvention != Configuration.CAMEL_CASE_NAMING_CONVENTION) {
+                        if (first) {
+                            first = false;
+                        } else {
+                            sb.append(", ");
+                        }
+
+                        sb.append(correctName);
+                    }
+                }
+            }
+            throw new ParseException(sb.toString(), null, keyTk);
+        }
+        
+        this.key = key;
+        this.value = value;
+    }
+
+    @Override
+    _ASTElement[] accept(Environment env) throws TemplateException {
+        TemplateModel mval = value.eval(env);
+        String strval;
+        if (mval instanceof TemplateScalarModel) {
+            strval = ((TemplateScalarModel) mval).getAsString();
+        } else if (mval instanceof TemplateBooleanModel) {
+            strval = ((TemplateBooleanModel) mval).getAsBoolean() ? "true" : "false";
+        } else if (mval instanceof TemplateNumberModel) {
+            strval = ((TemplateNumberModel) mval).getAsNumber().toString();
+        } else {
+            strval = value.evalAndCoerceToStringOrUnsupportedMarkup(env);
+        }
+        try {
+            env.setSetting(key, strval);
+        } catch (ConfigurationException e) {
+            throw new _MiscTemplateException(env, e.getMessage(), e.getCause());
+        }
+        return null;
+    }
+    
+    @Override
+    protected String dump(boolean canonical) {
+        StringBuilder sb = new StringBuilder();
+        if (canonical) sb.append('<');
+        sb.append(getNodeTypeSymbol());
+        sb.append(' ');
+        sb.append(_StringUtil.toFTLTopLevelTragetIdentifier(key));
+        sb.append('=');
+        sb.append(value.getCanonicalForm());
+        if (canonical) sb.append("/>");
+        return sb.toString();
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return "#setting";
+    }
+
+    @Override
+    int getParameterCount() {
+        return 2;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        switch (idx) {
+        case 0: return key;
+        case 1: return value;
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        switch (idx) {
+        case 0: return ParameterRole.ITEM_KEY;
+        case 1: return ParameterRole.ITEM_VALUE;
+        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/ASTDirStop.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/ASTDirStop.java b/src/main/java/org/apache/freemarker/core/ASTDirStop.java
new file mode 100644
index 0000000..54397b4
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/ASTDirStop.java
@@ -0,0 +1,81 @@
+/*
+ * 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 directive node: {@code #stop}.
+ */
+final class ASTDirStop extends _ASTElement {
+
+    private ASTExpression exp;
+
+    ASTDirStop(ASTExpression exp) {
+        this.exp = exp;
+    }
+
+    @Override
+    _ASTElement[] accept(Environment env) throws TemplateException {
+        if (exp == null) {
+            throw new StopException(env);
+        }
+        throw new StopException(env, exp.evalAndCoerceToPlainText(env));
+    }
+
+    @Override
+    protected String dump(boolean canonical) {
+        StringBuilder sb = new StringBuilder();
+        if (canonical) sb.append('<');
+        sb.append(getNodeTypeSymbol());
+        if (exp != null) {
+            sb.append(' ');
+            sb.append(exp.getCanonicalForm());
+        }
+        if (canonical) sb.append("/>");
+        return sb.toString();
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return "#stop";
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 1;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        if (idx != 0) throw new IndexOutOfBoundsException();
+        return exp;
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        if (idx != 0) throw new IndexOutOfBoundsException();
+        return ParameterRole.MESSAGE;
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        return false;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/ASTDirSwitch.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/ASTDirSwitch.java b/src/main/java/org/apache/freemarker/core/ASTDirSwitch.java
new file mode 100644
index 0000000..de56bf3
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/ASTDirSwitch.java
@@ -0,0 +1,127 @@
+/*
+ * 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 node: {@code #switch}.
+ */
+final class ASTDirSwitch extends _ASTElement {
+
+    private ASTDirCase defaultCase;
+    private final ASTExpression searched;
+
+    /**
+     * @param searched the expression to be tested.
+     */
+    ASTDirSwitch(ASTExpression searched) {
+        this.searched = searched;
+        setChildBufferCapacity(4);
+    }
+
+    void addCase(ASTDirCase cas) {
+        if (cas.condition == null) {
+            defaultCase = cas;
+        }
+        addChild(cas);
+    }
+
+    @Override
+    _ASTElement[] accept(Environment env)
+        throws TemplateException, IOException {
+        boolean processedCase = false;
+        int ln = getChildCount();
+        try {
+            for (int i = 0; i < ln; i++) {
+                ASTDirCase cas = (ASTDirCase) getChild(i);
+                boolean processCase = false;
+
+                // Fall through if a previous case tested true.
+                if (processedCase) {
+                    processCase = true;
+                } else if (cas.condition != null) {
+                    // Otherwise, if this case isn't the default, test it.
+                    processCase = EvalUtil.compare(
+                            searched,
+                            EvalUtil.CMP_OP_EQUALS, "case==", cas.condition, cas.condition, env);
+                }
+                if (processCase) {
+                    env.visit(cas);
+                    processedCase = true;
+                }
+            }
+
+            // If we didn't process any nestedElements, and we have a default,
+            // process it.
+            if (!processedCase && defaultCase != null) {
+                env.visit(defaultCase);
+            }
+        } catch (ASTDirBreak.Break br) {}
+        return null;
+    }
+
+    @Override
+    protected String dump(boolean canonical) {
+        StringBuilder buf = new StringBuilder();
+        if (canonical) buf.append('<');
+        buf.append(getNodeTypeSymbol());
+        buf.append(' ');
+        buf.append(searched.getCanonicalForm());
+        if (canonical) {
+            buf.append('>');
+            int ln = getChildCount();
+            for (int i = 0; i < ln; i++) {
+                ASTDirCase cas = (ASTDirCase) getChild(i);
+                buf.append(cas.getCanonicalForm());
+            }
+            buf.append("</").append(getNodeTypeSymbol()).append('>');
+        }
+        return buf.toString();
+    }
+
+    @Override
+    String getNodeTypeSymbol() {
+        return "#switch";
+    }
+
+    @Override
+    int getParameterCount() {
+        return 1;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        if (idx != 0) throw new IndexOutOfBoundsException();
+        return searched;
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        if (idx != 0) throw new IndexOutOfBoundsException();
+        return ParameterRole.VALUE;
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        return false;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/ASTDirTOrTrOrTl.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/ASTDirTOrTrOrTl.java b/src/main/java/org/apache/freemarker/core/ASTDirTOrTrOrTl.java
new file mode 100644
index 0000000..c5f2182
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/ASTDirTOrTrOrTl.java
@@ -0,0 +1,109 @@
+/*
+ * 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 directive node: {@code #t}, {@code #tr}, {@code #tl}.
+ */
+final class ASTDirTOrTrOrTl extends _ASTElement {
+    
+    private final int TYPE_T = 0;  
+    private final int TYPE_LT = 1;  
+    private final int TYPE_RT = 2;  
+    private final int TYPE_NT = 3;  
+
+    final boolean left, right;
+
+    ASTDirTOrTrOrTl(boolean left, boolean right) {
+        this.left = left;
+        this.right = right;
+    }
+
+    @Override
+    _ASTElement[] accept(Environment env) {
+        // This instruction does nothing at render-time, only parse-time.
+        return null;
+    }
+
+    @Override
+    protected String dump(boolean canonical) {
+        StringBuilder sb = new StringBuilder();
+        if (canonical) sb.append('<');
+        sb.append(getNodeTypeSymbol());
+        if (canonical) sb.append("/>");
+        return sb.toString();
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        if (left && right) {
+            return "#t";
+        } else if (left) {
+            return "#lt";
+        } else if (right) {
+            return "#rt";
+        } else {
+            return "#nt";
+        }
+    }
+    
+    @Override
+    boolean isIgnorable(boolean stripWhitespace) {
+        return true;
+    }
+
+    @Override
+    int getParameterCount() {
+        return 1;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        if (idx != 0) throw new IndexOutOfBoundsException();
+        int type;
+        if (left && right) {
+            type = TYPE_T;
+        } else if (left) {
+            type = TYPE_LT;
+        } else if (right) {
+            type = TYPE_RT;
+        } else {
+            type = TYPE_NT;
+        }
+        return Integer.valueOf(type);
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        if (idx != 0) throw new IndexOutOfBoundsException();
+        return ParameterRole.AST_NODE_SUBTYPE;
+    }
+
+    @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/ASTDirTransform.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/ASTDirTransform.java b/src/main/java/org/apache/freemarker/core/ASTDirTransform.java
new file mode 100644
index 0000000..cd22b97
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/ASTDirTransform.java
@@ -0,0 +1,162 @@
+/*
+ * 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.lang.ref.Reference;
+import java.lang.ref.SoftReference;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateTransformModel;
+
+/**
+ * AST directive node: {@code #transform}.
+ */
+final class ASTDirTransform extends _ASTElement {
+
+    private ASTExpression transformExpression;
+    Map namedArgs;
+    private transient volatile SoftReference/*List<Map.Entry<String,ASTExpression>>*/ sortedNamedArgsCache;
+
+    ASTDirTransform(ASTExpression transformExpression,
+                   Map namedArgs,
+                   TemplateElements children) {
+        this.transformExpression = transformExpression;
+        this.namedArgs = namedArgs;
+        setChildren(children);
+    }
+
+    @Override
+    _ASTElement[] accept(Environment env)
+    throws TemplateException, IOException {
+        TemplateTransformModel ttm = env.getTransform(transformExpression);
+        if (ttm != null) {
+            Map args;
+            if (namedArgs != null && !namedArgs.isEmpty()) {
+                args = new HashMap();
+                for (Iterator it = namedArgs.entrySet().iterator(); it.hasNext(); ) {
+                    Map.Entry entry = (Map.Entry) it.next();
+                    String key = (String) entry.getKey();
+                    ASTExpression valueExp = (ASTExpression) entry.getValue();
+                    TemplateModel value = valueExp.eval(env);
+                    args.put(key, value);
+                }
+            } else {
+                args = Collections.emptyMap();
+            }
+            env.visitAndTransform(getChildBuffer(), ttm, args);
+        } else {
+            TemplateModel tm = transformExpression.eval(env);
+            throw new UnexpectedTypeException(
+                    transformExpression, tm,
+                    "transform", new Class[] { TemplateTransformModel.class }, env);
+        }
+        return null;
+    }
+
+    @Override
+    protected String dump(boolean canonical) {
+        StringBuilder sb = new StringBuilder();
+        if (canonical) sb.append('<');
+        sb.append(getNodeTypeSymbol());
+        sb.append(' ');
+        sb.append(transformExpression);
+        if (namedArgs != null) {
+            for (Iterator it = getSortedNamedArgs().iterator(); it.hasNext(); ) {
+                Map.Entry entry = (Map.Entry) it.next();
+                sb.append(' ');
+                sb.append(entry.getKey());
+                sb.append('=');
+                MessageUtil.appendExpressionAsUntearable(sb, (ASTExpression) entry.getValue());
+            }
+        }
+        if (canonical) {
+            sb.append(">");
+            sb.append(getChildrenCanonicalForm());
+            sb.append("</").append(getNodeTypeSymbol()).append('>');
+        }
+        return sb.toString();
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return "#transform";
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 1/*nameExp*/ + (namedArgs != null ? namedArgs.size() * 2 : 0);
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        if (idx == 0) {
+            return transformExpression;
+        } else if (namedArgs != null && idx - 1 < namedArgs.size() * 2) {
+            Map.Entry namedArg = (Map.Entry) getSortedNamedArgs().get((idx - 1) / 2);
+            return (idx - 1) % 2 == 0 ? namedArg.getKey() : namedArg.getValue();
+        } else {
+            throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        if (idx == 0) {
+            return ParameterRole.CALLEE;
+        } else if (idx - 1 < namedArgs.size() * 2) {
+                return (idx - 1) % 2 == 0 ? ParameterRole.ARGUMENT_NAME : ParameterRole.ARGUMENT_VALUE;
+        } else {
+            throw new IndexOutOfBoundsException();
+        }
+    }
+
+    /**
+     * Returns the named args by source-code order; it's not meant to be used during template execution, too slow for
+     * that!
+     */
+    private List/*<Map.Entry<String, ASTExpression>>*/ getSortedNamedArgs() {
+        Reference ref = sortedNamedArgsCache;
+        if (ref != null) {
+            List res = (List) ref.get();
+            if (res != null) return res;
+        }
+        
+        List res = MiscUtil.sortMapOfExpressions(namedArgs);
+        sortedNamedArgsCache = new SoftReference(res);
+        return res;
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        return false;
+    }
+
+    @Override
+    boolean isShownInStackTrace() {
+        return true;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/ASTDirUserDefined.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/ASTDirUserDefined.java b/src/main/java/org/apache/freemarker/core/ASTDirUserDefined.java
new file mode 100644
index 0000000..3eaa6bb
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/ASTDirUserDefined.java
@@ -0,0 +1,344 @@
+/*
+ * 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.lang.ref.Reference;
+import java.lang.ref.SoftReference;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.freemarker.core.model.TemplateDirectiveModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateTransformModel;
+import org.apache.freemarker.core.util.ObjectFactory;
+import org.apache.freemarker.core.util._StringUtil;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
+/**
+ * AST directive node: {@code <@exp .../>} or {@code <@exp ...>...</@...>}. Calls an user-defined directive (like a
+ * macro).
+ */
+final class ASTDirUserDefined extends _ASTElement implements DirectiveCallPlace {
+
+    private ASTExpression nameExp;
+    private Map namedArgs;
+    private List positionalArgs, bodyParameterNames;
+    boolean legacySyntax;
+    private transient volatile SoftReference/*List<Map.Entry<String,ASTExpression>>*/ sortedNamedArgsCache;
+    private CustomDataHolder customDataHolder;
+
+    ASTDirUserDefined(ASTExpression nameExp,
+         Map namedArgs,
+         TemplateElements children,
+         List bodyParameterNames) {
+        this.nameExp = nameExp;
+        this.namedArgs = namedArgs;
+        setChildren(children);
+        this.bodyParameterNames = bodyParameterNames;
+    }
+
+    ASTDirUserDefined(ASTExpression nameExp,
+         List positionalArgs,
+         TemplateElements children,
+         List bodyParameterNames) {
+        this.nameExp = nameExp;
+        this.positionalArgs = positionalArgs;
+        setChildren(children);
+        this.bodyParameterNames = bodyParameterNames;
+    }
+
+    @Override
+    _ASTElement[] accept(Environment env) throws TemplateException, IOException {
+        TemplateModel tm = nameExp.eval(env);
+        if (tm == ASTDirMacro.DO_NOTHING_MACRO) return null; // shortcut here.
+        if (tm instanceof ASTDirMacro) {
+            ASTDirMacro macro = (ASTDirMacro) tm;
+            if (macro.isFunction() && !legacySyntax) {
+                throw new _MiscTemplateException(env,
+                        "Routine ", new _DelayedJQuote(macro.getName()), " is a function, not a directive. "
+                        + "Functions can only be called from expressions, like in ${f()}, ${x + f()} or ",
+                        "<@someDirective someParam=f() />", ".");
+            }    
+            env.invoke(macro, namedArgs, positionalArgs, bodyParameterNames, getChildBuffer());
+        } else {
+            boolean isDirectiveModel = tm instanceof TemplateDirectiveModel; 
+            if (isDirectiveModel || tm instanceof TemplateTransformModel) {
+                Map args;
+                if (namedArgs != null && !namedArgs.isEmpty()) {
+                    args = new HashMap();
+                    for (Iterator it = namedArgs.entrySet().iterator(); it.hasNext(); ) {
+                        Map.Entry entry = (Map.Entry) it.next();
+                        String key = (String) entry.getKey();
+                        ASTExpression valueExp = (ASTExpression) entry.getValue();
+                        TemplateModel value = valueExp.eval(env);
+                        args.put(key, value);
+                    }
+                } else {
+                    args = Collections.emptyMap();
+                }
+                if (isDirectiveModel) {
+                    env.visit(getChildBuffer(), (TemplateDirectiveModel) tm, args, bodyParameterNames);
+                } else { 
+                    env.visitAndTransform(getChildBuffer(), (TemplateTransformModel) tm, args);
+                }
+            } else if (tm == null) {
+                throw InvalidReferenceException.getInstance(nameExp, env);
+            } else {
+                throw new NonUserDefinedDirectiveLikeException(nameExp, tm, env);
+            }
+        }
+        return null;
+    }
+
+    @Override
+    protected String dump(boolean canonical) {
+        StringBuilder sb = new StringBuilder();
+        if (canonical) sb.append('<');
+        sb.append('@');
+        MessageUtil.appendExpressionAsUntearable(sb, nameExp);
+        boolean nameIsInParen = sb.charAt(sb.length() - 1) == ')';
+        if (positionalArgs != null) {
+            for (int i = 0; i < positionalArgs.size(); i++) {
+                ASTExpression argExp = (ASTExpression) positionalArgs.get(i);
+                if (i != 0) {
+                    sb.append(',');
+                }
+                sb.append(' ');
+                sb.append(argExp.getCanonicalForm());
+            }
+        } else {
+            List entries = getSortedNamedArgs();
+            for (int i = 0; i < entries.size(); i++) {
+                Map.Entry entry = (Map.Entry) entries.get(i);
+                ASTExpression argExp = (ASTExpression) entry.getValue();
+                sb.append(' ');
+                sb.append(_StringUtil.toFTLTopLevelIdentifierReference((String) entry.getKey()));
+                sb.append('=');
+                MessageUtil.appendExpressionAsUntearable(sb, argExp);
+            }
+        }
+        if (bodyParameterNames != null && !bodyParameterNames.isEmpty()) {
+            sb.append("; ");
+            for (int i = 0; i < bodyParameterNames.size(); i++) {
+                if (i != 0) {
+                    sb.append(", ");
+                }
+                sb.append(_StringUtil.toFTLTopLevelIdentifierReference((String) bodyParameterNames.get(i)));
+            }
+        }
+        if (canonical) {
+            if (getChildCount() == 0) {
+                sb.append("/>");
+            } else {
+                sb.append('>');
+                sb.append(getChildrenCanonicalForm());
+                sb.append("</@");
+                if (!nameIsInParen
+                        && (nameExp instanceof ASTExpVariable
+                            || (nameExp instanceof ASTExpDot && ((ASTExpDot) nameExp).onlyHasIdentifiers()))) {
+                    sb.append(nameExp.getCanonicalForm());
+                }
+                sb.append('>');
+            }
+        }
+        return sb.toString();
+    }
+
+    @Override
+    String getNodeTypeSymbol() {
+        return "@";
+    }
+
+    @Override
+    int getParameterCount() {
+        return 1/*nameExp*/
+                + (positionalArgs != null ? positionalArgs.size() : 0)
+                + (namedArgs != null ? namedArgs.size() * 2 : 0)
+                + (bodyParameterNames != null ? bodyParameterNames.size() : 0);
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        if (idx == 0) {
+            return nameExp;
+        } else {
+            int base = 1;
+            final int positionalArgsSize = positionalArgs != null ? positionalArgs.size() : 0;  
+            if (idx - base < positionalArgsSize) {
+                return positionalArgs.get(idx - base);
+            } else {
+                base += positionalArgsSize;
+                final int namedArgsSize = namedArgs != null ? namedArgs.size() : 0;
+                if (idx - base < namedArgsSize * 2) {
+                    Map.Entry namedArg = (Map.Entry) getSortedNamedArgs().get((idx - base) / 2);
+                    return (idx - base) % 2 == 0 ? namedArg.getKey() : namedArg.getValue();
+                } else {
+                    base += namedArgsSize * 2;
+                    final int bodyParameterNamesSize = bodyParameterNames != null ? bodyParameterNames.size() : 0;
+                    if (idx - base < bodyParameterNamesSize) {
+                        return bodyParameterNames.get(idx - base);
+                    } else {
+                        throw new IndexOutOfBoundsException();
+                    }
+                }
+            }
+        }
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        if (idx == 0) {
+            return ParameterRole.CALLEE;
+        } else {
+            int base = 1;
+            final int positionalArgsSize = positionalArgs != null ? positionalArgs.size() : 0;  
+            if (idx - base < positionalArgsSize) {
+                return ParameterRole.ARGUMENT_VALUE;
+            } else {
+                base += positionalArgsSize;
+                final int namedArgsSize = namedArgs != null ? namedArgs.size() : 0;
+                if (idx - base < namedArgsSize * 2) {
+                    return (idx - base) % 2 == 0 ? ParameterRole.ARGUMENT_NAME : ParameterRole.ARGUMENT_VALUE;
+                } else {
+                    base += namedArgsSize * 2;
+                    final int bodyParameterNamesSize = bodyParameterNames != null ? bodyParameterNames.size() : 0;
+                    if (idx - base < bodyParameterNamesSize) {
+                        return ParameterRole.TARGET_LOOP_VARIABLE;
+                    } else {
+                        throw new IndexOutOfBoundsException();
+                    }
+                }
+            }
+        }
+    }
+    
+    /**
+     * Returns the named args by source-code order; it's not meant to be used during template execution, too slow for
+     * that!
+     */
+    private List/*<Map.Entry<String, ASTExpression>>*/ getSortedNamedArgs() {
+        Reference ref = sortedNamedArgsCache;
+        if (ref != null) {
+            List res = (List) ref.get();
+            if (res != null) return res;
+        }
+        
+        List res = MiscUtil.sortMapOfExpressions(namedArgs);
+        sortedNamedArgsCache = new SoftReference(res);
+        return res;
+    }
+
+    @Override
+    @SuppressFBWarnings(value={ "IS2_INCONSISTENT_SYNC", "DC_DOUBLECHECK" }, justification="Performance tricks")
+    public Object getOrCreateCustomData(Object providerIdentity, ObjectFactory objectFactory)
+            throws CallPlaceCustomDataInitializationException {
+        // We are using double-checked locking, utilizing Java memory model "final" trick.
+        // Note that this.customDataHolder is NOT volatile.
+        
+        CustomDataHolder customDataHolder = this.customDataHolder;  // Findbugs false alarm
+        if (customDataHolder == null) {  // Findbugs false alarm
+            synchronized (this) {
+                customDataHolder = this.customDataHolder;
+                if (customDataHolder == null || customDataHolder.providerIdentity != providerIdentity) {
+                    customDataHolder = createNewCustomData(providerIdentity, objectFactory);
+                    this.customDataHolder = customDataHolder; 
+                }
+            }
+        }
+        
+        if (customDataHolder.providerIdentity != providerIdentity) {
+            synchronized (this) {
+                customDataHolder = this.customDataHolder;
+                if (customDataHolder == null || customDataHolder.providerIdentity != providerIdentity) {
+                    customDataHolder = createNewCustomData(providerIdentity, objectFactory);
+                    this.customDataHolder = customDataHolder;
+                }
+            }
+        }
+        
+        return customDataHolder.customData;
+    }
+
+    private CustomDataHolder createNewCustomData(Object provierIdentity, ObjectFactory objectFactory)
+            throws CallPlaceCustomDataInitializationException {
+        CustomDataHolder customDataHolder;
+        Object customData;
+        try {
+            customData = objectFactory.createObject();
+        } catch (Exception e) {
+            throw new CallPlaceCustomDataInitializationException(
+                    "Failed to initialize custom data for provider identity "
+                    + _StringUtil.tryToString(provierIdentity) + " via factory "
+                    + _StringUtil.tryToString(objectFactory), e);
+        }
+        if (customData == null) {
+            throw new NullPointerException("ObjectFactory.createObject() has returned null");
+        }
+        customDataHolder = new CustomDataHolder(provierIdentity, customData);
+        return customDataHolder;
+    }
+
+    @Override
+    public boolean isNestedOutputCacheable() {
+        return isChildrenOutputCacheable();
+    }
+    
+/*
+    //REVISIT
+    boolean heedsOpeningWhitespace() {
+        return nestedBlock == null;
+    }
+
+    //REVISIT
+    boolean heedsTrailingWhitespace() {
+        return nestedBlock == null;
+    }*/
+    
+    /**
+     * Used for implementing double check locking in implementing the
+     * {@link DirectiveCallPlace#getOrCreateCustomData(Object, ObjectFactory)}.
+     */
+    private static class CustomDataHolder {
+        
+        private final Object providerIdentity;
+        private final Object customData;
+        public CustomDataHolder(Object providerIdentity, Object customData) {
+            this.providerIdentity = providerIdentity;
+            this.customData = customData;
+        }
+        
+    }
+    
+    @Override
+    boolean isNestedBlockRepeater() {
+        return true;
+    }
+
+    @Override
+    boolean isShownInStackTrace() {
+        return true;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/ASTDirVisit.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/ASTDirVisit.java b/src/main/java/org/apache/freemarker/core/ASTDirVisit.java
new file mode 100644
index 0000000..e68cf72
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/ASTDirVisit.java
@@ -0,0 +1,127 @@
+/*
+ * 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.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateNodeModel;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+import org.apache.freemarker.core.model.impl.SimpleSequence;
+
+
+/**
+ * AST directive node: {@code #visit}.
+ */
+final class ASTDirVisit extends _ASTElement {
+    
+    ASTExpression targetNode, namespaces;
+    
+    ASTDirVisit(ASTExpression targetNode, ASTExpression namespaces) {
+        this.targetNode = targetNode;
+        this.namespaces = namespaces;
+    }
+
+    @Override
+    _ASTElement[] accept(Environment env) throws IOException, TemplateException {
+        TemplateModel node = targetNode.eval(env);
+        if (!(node instanceof TemplateNodeModel)) {
+            throw new NonNodeException(targetNode, node, env);
+        }
+        
+        TemplateModel nss = namespaces == null ? null : namespaces.eval(env);
+        if (namespaces instanceof ASTExpStringLiteral) {
+            nss = env.importLib(((TemplateScalarModel) nss).getAsString(), null);
+        } else if (namespaces instanceof ASTExpListLiteral) {
+            nss = ((ASTExpListLiteral) namespaces).evaluateStringsToNamespaces(env);
+        }
+        if (nss != null) {
+            if (nss instanceof Environment.Namespace) {
+                SimpleSequence ss = new SimpleSequence(1);
+                ss.add(nss);
+                nss = ss;
+            } else if (!(nss instanceof TemplateSequenceModel)) {
+                if (namespaces != null) {
+                    throw new NonSequenceException(namespaces, nss, env);
+                } else {
+                    // Should not occur
+                    throw new _MiscTemplateException(env, "Expecting a sequence of namespaces after \"using\"");
+                }
+            }
+        }
+        env.invokeNodeHandlerFor((TemplateNodeModel) node, (TemplateSequenceModel) nss);
+        return null;
+    }
+
+    @Override
+    protected String dump(boolean canonical) {
+        StringBuilder sb = new StringBuilder();
+        if (canonical) sb.append('<');
+        sb.append(getNodeTypeSymbol());
+        sb.append(' ');
+        sb.append(targetNode.getCanonicalForm());
+        if (namespaces != null) {
+            sb.append(" using ");
+            sb.append(namespaces.getCanonicalForm());
+        }
+        if (canonical) sb.append("/>");
+        return sb.toString();
+    }
+
+    @Override
+    String getNodeTypeSymbol() {
+        return "#visit";
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 2;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        switch (idx) {
+        case 0: return targetNode;
+        case 1: return namespaces;
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        switch (idx) {
+        case 0: return ParameterRole.NODE;
+        case 1: return ParameterRole.NAMESPACE;
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        return true;
+    }
+
+    @Override
+    boolean isShownInStackTrace() {
+        return true;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/ASTDollarInterpolation.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/ASTDollarInterpolation.java b/src/main/java/org/apache/freemarker/core/ASTDollarInterpolation.java
new file mode 100644
index 0000000..2396a60
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/ASTDollarInterpolation.java
@@ -0,0 +1,148 @@
+/*
+ * 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 org.apache.freemarker.core.util.FTLUtil;
+
+/**
+ * AST interpolation node: <tt>${exp}</tt>
+ */
+final class ASTDollarInterpolation extends ASTInterpolation {
+
+    private final ASTExpression expression;
+    
+    /** For {@code #escape x as ...} (legacy auto-escaping) */
+    private final ASTExpression escapedExpression;
+    
+    /** For OutputFormat-based auto-escaping */
+    private final OutputFormat outputFormat;
+    private final MarkupOutputFormat markupOutputFormat;
+    private final boolean autoEscape;
+
+    ASTDollarInterpolation(
+            ASTExpression expression, ASTExpression escapedExpression,
+            OutputFormat outputFormat, boolean autoEscape) {
+        this.expression = expression;
+        this.escapedExpression = escapedExpression;
+        this.outputFormat = outputFormat;
+        markupOutputFormat
+                = (MarkupOutputFormat) (outputFormat instanceof MarkupOutputFormat ? outputFormat : null);
+        this.autoEscape = autoEscape;
+    }
+
+    /**
+     * Outputs the string value of the enclosed expression.
+     */
+    @Override
+    _ASTElement[] accept(Environment env) throws TemplateException, IOException {
+        final Object moOrStr = calculateInterpolatedStringOrMarkup(env);
+        final Writer out = env.getOut();
+        if (moOrStr instanceof String) {
+            final String s = (String) moOrStr;
+            if (autoEscape) {
+                markupOutputFormat.output(s, out);
+            } else {
+                out.write(s);
+            }
+        } else {
+            final TemplateMarkupOutputModel mo = (TemplateMarkupOutputModel) moOrStr;
+            final MarkupOutputFormat moOF = mo.getOutputFormat();
+            // ATTENTION: Keep this logic in sync. ?esc/?noEsc's logic!
+            if (moOF != outputFormat && !outputFormat.isOutputFormatMixingAllowed()) {
+                final String srcPlainText;
+                // ATTENTION: Keep this logic in sync. ?esc/?noEsc's logic!
+                srcPlainText = moOF.getSourcePlainText(mo);
+                if (srcPlainText == null) {
+                    throw new _TemplateModelException(escapedExpression,
+                            "The value to print is in ", new _DelayedToString(moOF),
+                            " format, which differs from the current output format, ",
+                            new _DelayedToString(outputFormat), ". Format conversion wasn't possible.");
+                }
+                if (outputFormat instanceof MarkupOutputFormat) {
+                    ((MarkupOutputFormat) outputFormat).output(srcPlainText, out);
+                } else {
+                    out.write(srcPlainText);
+                }
+            } else {
+                moOF.output(mo, out);
+            }
+        }
+        return null;
+    }
+
+    @Override
+    protected Object calculateInterpolatedStringOrMarkup(Environment env) throws TemplateException {
+        return EvalUtil.coerceModelToStringOrMarkup(escapedExpression.eval(env), escapedExpression, null, env);
+    }
+
+    @Override
+    protected String dump(boolean canonical, boolean inStringLiteral) {
+        StringBuilder sb = new StringBuilder();
+        sb.append("${");
+        final String exprCF = expression.getCanonicalForm();
+        sb.append(inStringLiteral ? FTLUtil.escapeStringLiteralPart(exprCF, '"') : exprCF);
+        sb.append("}");
+        if (!canonical && expression != escapedExpression) {
+            sb.append(" auto-escaped");            
+        }
+        return sb.toString();
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return "${...}";
+    }
+
+    @Override
+    boolean heedsOpeningWhitespace() {
+        return true;
+    }
+
+    @Override
+    boolean heedsTrailingWhitespace() {
+        return true;
+    }
+
+    @Override
+    int getParameterCount() {
+        return 1;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        if (idx != 0) throw new IndexOutOfBoundsException();
+        return expression;
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        if (idx != 0) throw new IndexOutOfBoundsException();
+        return ParameterRole.CONTENT;
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        return false;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/ASTExpAddOrConcat.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/ASTExpAddOrConcat.java b/src/main/java/org/apache/freemarker/core/ASTExpAddOrConcat.java
new file mode 100644
index 0000000..7b2369a
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/ASTExpAddOrConcat.java
@@ -0,0 +1,312 @@
+/*
+ * 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.HashSet;
+import java.util.Set;
+
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+import org.apache.freemarker.core.model.TemplateHashModel;
+import org.apache.freemarker.core.model.TemplateHashModelEx;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateModelIterator;
+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.CollectionAndSequence;
+import org.apache.freemarker.core.model.impl.SimpleNumber;
+import org.apache.freemarker.core.model.impl.SimpleScalar;
+import org.apache.freemarker.core.model.impl.SimpleSequence;
+
+/**
+ * AST expression node: binary {@code +} operator. Note that this is treated separately from the other 4 arithmetic
+ * operators, since it's overloaded to mean concatenation of string-s, sequences and hash-es too.
+ */
+final class ASTExpAddOrConcat extends ASTExpression {
+
+    private final ASTExpression left;
+    private final ASTExpression right;
+
+    ASTExpAddOrConcat(ASTExpression left, ASTExpression right) {
+        this.left = left;
+        this.right = right;
+    }
+
+    @Override
+    TemplateModel _eval(Environment env) throws TemplateException {
+        return _eval(env, this, left, left.eval(env), right, right.eval(env));
+    }
+
+    /**
+     * @param leftExp
+     *            Used for error messages only; can be {@code null}
+     * @param rightExp
+     *            Used for error messages only; can be {@code null}
+     */
+    static TemplateModel _eval(Environment env,
+            ASTNode parent,
+            ASTExpression leftExp, TemplateModel leftModel,
+            ASTExpression rightExp, TemplateModel rightModel)
+            throws TemplateModelException, TemplateException, NonStringException {
+        if (leftModel instanceof TemplateNumberModel && rightModel instanceof TemplateNumberModel) {
+            Number first = EvalUtil.modelToNumber((TemplateNumberModel) leftModel, leftExp);
+            Number second = EvalUtil.modelToNumber((TemplateNumberModel) rightModel, rightExp);
+            return _evalOnNumbers(env, parent, first, second);
+        } else if (leftModel instanceof TemplateSequenceModel && rightModel instanceof TemplateSequenceModel) {
+            return new ConcatenatedSequence((TemplateSequenceModel) leftModel, (TemplateSequenceModel) rightModel);
+        } else {
+            boolean hashConcatPossible
+                    = leftModel instanceof TemplateHashModel && rightModel instanceof TemplateHashModel;
+            try {
+                // We try string addition first. If hash addition is possible, then instead of throwing exception
+                // we return null and do hash addition instead. (We can't simply give hash addition a priority, like
+                // with sequence addition above, as FTL strings are often also FTL hashes.)
+                Object leftOMOrStr = EvalUtil.coerceModelToStringOrMarkup(
+                        leftModel, leftExp, /* returnNullOnNonCoercableType = */ hashConcatPossible, null,
+                        env);
+                if (leftOMOrStr == null) {
+                    return _eval_concatenateHashes(leftModel, rightModel);
+                }
+
+                // Same trick with null return as above.
+                Object rightOMOrStr = EvalUtil.coerceModelToStringOrMarkup(
+                        rightModel, rightExp, /* returnNullOnNonCoercableType = */ hashConcatPossible, null,
+                        env);
+                if (rightOMOrStr == null) {
+                    return _eval_concatenateHashes(leftModel, rightModel);
+                }
+
+                if (leftOMOrStr instanceof String) {
+                    if (rightOMOrStr instanceof String) {
+                        return new SimpleScalar(((String) leftOMOrStr).concat((String) rightOMOrStr));
+                    } else { // rightOMOrStr instanceof TemplateMarkupOutputModel
+                        TemplateMarkupOutputModel<?> rightMO = (TemplateMarkupOutputModel<?>) rightOMOrStr; 
+                        return EvalUtil.concatMarkupOutputs(parent,
+                                rightMO.getOutputFormat().fromPlainTextByEscaping((String) leftOMOrStr),
+                                rightMO);
+                    }                    
+                } else { // leftOMOrStr instanceof TemplateMarkupOutputModel 
+                    TemplateMarkupOutputModel<?> leftMO = (TemplateMarkupOutputModel<?>) leftOMOrStr; 
+                    if (rightOMOrStr instanceof String) {  // markup output
+                        return EvalUtil.concatMarkupOutputs(parent,
+                                leftMO,
+                                leftMO.getOutputFormat().fromPlainTextByEscaping((String) rightOMOrStr));
+                    } else { // rightOMOrStr instanceof TemplateMarkupOutputModel
+                        return EvalUtil.concatMarkupOutputs(parent,
+                                leftMO,
+                                (TemplateMarkupOutputModel<?>) rightOMOrStr);
+                    }
+                }
+            } catch (NonStringOrTemplateOutputException e) {
+                // 2.4: Remove this catch; it's for BC, after reworking hash addition so it doesn't rely on this. But
+                // user code might throws this (very unlikely), and then in 2.3.x we did catch that too, incorrectly.
+                if (hashConcatPossible) {
+                    return _eval_concatenateHashes(leftModel, rightModel);
+                } else {
+                    throw e;
+                }
+            }
+        }
+    }
+
+    private static TemplateModel _eval_concatenateHashes(TemplateModel leftModel, TemplateModel rightModel)
+            throws TemplateModelException {
+        if (leftModel instanceof TemplateHashModelEx && rightModel instanceof TemplateHashModelEx) {
+            TemplateHashModelEx leftModelEx = (TemplateHashModelEx) leftModel;
+            TemplateHashModelEx rightModelEx = (TemplateHashModelEx) rightModel;
+            if (leftModelEx.size() == 0) {
+                return rightModelEx;
+            } else if (rightModelEx.size() == 0) {
+                return leftModelEx;
+            } else {
+                return new ConcatenatedHashEx(leftModelEx, rightModelEx);
+            }
+        } else {
+            return new ConcatenatedHash((TemplateHashModel) leftModel,
+                                        (TemplateHashModel) rightModel);
+        }
+    }
+
+    static TemplateModel _evalOnNumbers(Environment env, ASTNode parent, Number first, Number second)
+            throws TemplateException {
+        ArithmeticEngine ae = EvalUtil.getArithmeticEngine(env, parent);
+        return new SimpleNumber(ae.add(first, second));
+    }
+
+    @Override
+    boolean isLiteral() {
+        return constantValue != null || (left.isLiteral() && right.isLiteral());
+    }
+
+    @Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) {
+    	return new ASTExpAddOrConcat(
+    	left.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState),
+    	right.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState));
+    }
+
+    @Override
+    public String getCanonicalForm() {
+        return left.getCanonicalForm() + " + " + right.getCanonicalForm();
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return "+";
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 2;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        return idx == 0 ? left : right;
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        return ParameterRole.forBinaryOperatorOperand(idx);
+    }
+
+    private static final class ConcatenatedSequence
+    implements
+        TemplateSequenceModel {
+        private final TemplateSequenceModel left;
+        private final TemplateSequenceModel right;
+
+        ConcatenatedSequence(TemplateSequenceModel left, TemplateSequenceModel right) {
+            this.left = left;
+            this.right = right;
+        }
+
+        @Override
+        public int size()
+        throws TemplateModelException {
+            return left.size() + right.size();
+        }
+
+        @Override
+        public TemplateModel get(int i)
+        throws TemplateModelException {
+            int ls = left.size();
+            return i < ls ? left.get(i) : right.get(i - ls);
+        }
+    }
+
+    private static class ConcatenatedHash
+    implements TemplateHashModel {
+        protected final TemplateHashModel left;
+        protected final TemplateHashModel right;
+
+        ConcatenatedHash(TemplateHashModel left, TemplateHashModel right) {
+            this.left = left;
+            this.right = right;
+        }
+        
+        @Override
+        public TemplateModel get(String key)
+        throws TemplateModelException {
+            TemplateModel model = right.get(key);
+            return (model != null) ? model : left.get(key);
+        }
+
+        @Override
+        public boolean isEmpty()
+        throws TemplateModelException {
+            return left.isEmpty() && right.isEmpty();
+        }
+    }
+
+    private static final class ConcatenatedHashEx
+    extends ConcatenatedHash
+    implements TemplateHashModelEx {
+        private CollectionAndSequence keys;
+        private CollectionAndSequence values;
+        private int size;
+
+        ConcatenatedHashEx(TemplateHashModelEx left, TemplateHashModelEx right) {
+            super(left, right);
+        }
+        
+        @Override
+        public int size() throws TemplateModelException {
+            initKeys();
+            return size;
+        }
+
+        @Override
+        public TemplateCollectionModel keys()
+        throws TemplateModelException {
+            initKeys();
+            return keys;
+        }
+
+        @Override
+        public TemplateCollectionModel values()
+        throws TemplateModelException {
+            initValues();
+            return values;
+        }
+
+        private void initKeys()
+        throws TemplateModelException {
+            if (keys == null) {
+                HashSet keySet = new HashSet();
+                SimpleSequence keySeq = new SimpleSequence(32);
+                addKeys(keySet, keySeq, (TemplateHashModelEx) left);
+                addKeys(keySet, keySeq, (TemplateHashModelEx) right);
+                size = keySet.size();
+                keys = new CollectionAndSequence(keySeq);
+            }
+        }
+
+        private static void addKeys(Set set, SimpleSequence keySeq, TemplateHashModelEx hash)
+        throws TemplateModelException {
+            TemplateModelIterator it = hash.keys().iterator();
+            while (it.hasNext()) {
+                TemplateScalarModel tsm = (TemplateScalarModel) it.next();
+                if (set.add(tsm.getAsString())) {
+                    // The first occurence of the key decides the index;
+                    // this is consisten with stuff like java.util.LinkedHashSet.
+                    keySeq.add(tsm);
+                }
+            }
+        }        
+
+        private void initValues()
+        throws TemplateModelException {
+            if (values == null) {
+                SimpleSequence seq = new SimpleSequence(size());
+                // Note: size() invokes initKeys() if needed.
+            
+                int ln = keys.size();
+                for (int i  = 0; i < ln; i++) {
+                    seq.add(get(((TemplateScalarModel) keys.get(i)).getAsString()));
+                }
+                values = new CollectionAndSequence(seq);
+            }
+        }
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/ASTExpAnd.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/ASTExpAnd.java b/src/main/java/org/apache/freemarker/core/ASTExpAnd.java
new file mode 100644
index 0000000..08a9ffa
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/ASTExpAnd.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 &&} operator
+ */
+final class ASTExpAnd extends ASTExpBoolean {
+
+    private final ASTExpression lho;
+    private final ASTExpression rho;
+
+    ASTExpAnd(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 ASTExpAnd(
+    	        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);
+    }
+    
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/ASTExpBoolean.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/ASTExpBoolean.java b/src/main/java/org/apache/freemarker/core/ASTExpBoolean.java
new file mode 100644
index 0000000..e292389
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/ASTExpBoolean.java
@@ -0,0 +1,34 @@
+/*
+ * 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.TemplateModel;
+
+/**
+ * AST expression node superclass for expressions returning a boolean.
+ */
+abstract class ASTExpBoolean extends ASTExpression {
+
+    @Override
+    TemplateModel _eval(Environment env) throws TemplateException {
+        return evalToBoolean(env) ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/ASTExpBooleanLiteral.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/ASTExpBooleanLiteral.java b/src/main/java/org/apache/freemarker/core/ASTExpBooleanLiteral.java
new file mode 100644
index 0000000..47336cb
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/ASTExpBooleanLiteral.java
@@ -0,0 +1,91 @@
+/*
+ * 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.TemplateModel;
+
+/**
+ * AST expression node: boolean literal 
+ */
+final class ASTExpBooleanLiteral extends ASTExpression {
+
+    private final boolean val;
+
+    public ASTExpBooleanLiteral(boolean val) {
+        this.val = val;
+    }
+
+    static TemplateBooleanModel getTemplateModel(boolean b) {
+        return b? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+    }
+
+    @Override
+    boolean evalToBoolean(Environment env) {
+        return val;
+    }
+
+    @Override
+    public String getCanonicalForm() {
+        return val ? MiscUtil.C_TRUE : MiscUtil.C_FALSE;
+    }
+
+    @Override
+    String getNodeTypeSymbol() {
+        return getCanonicalForm();
+    }
+    
+    @Override
+    public String toString() {
+        return val ? MiscUtil.C_TRUE : MiscUtil.C_FALSE;
+    }
+
+    @Override
+    TemplateModel _eval(Environment env) {
+        return val ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+    }
+    
+    @Override
+    boolean isLiteral() {
+        return true;
+    }
+
+    @Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) {
+    	return new ASTExpBooleanLiteral(val);
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 0;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+    
+}


Mime
View raw message