You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@freemarker.apache.org by dd...@apache.org on 2017/05/14 10:53:29 UTC

[46/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirRecurse.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirRecurse.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirRecurse.java
new file mode 100644
index 0000000..b95b3fc
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirRecurse.java
@@ -0,0 +1,130 @@
+/*
+ * 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;
+
+
+/**
+ * AST directive node: {@code #recurse}.
+ */
+final class ASTDirRecurse extends ASTDirective {
+    
+    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) {
+                NativeSequence ss = new NativeSequence(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/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirReturn.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirReturn.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirReturn.java
new file mode 100644
index 0000000..0e82a74
--- /dev/null
+++ b/freemarker-core/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}.
+ */
+final class ASTDirReturn extends ASTDirective {
+
+    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 && getParent() 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/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirSep.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirSep.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirSep.java
new file mode 100644
index 0000000..9e83e83
--- /dev/null
+++ b/freemarker-core/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 ASTDirective {
+
+    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/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirSetting.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirSetting.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirSetting.java
new file mode 100644
index 0000000..68a0672
--- /dev/null
+++ b/freemarker-core/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 ASTDirective {
+
+    private final String key;
+    private final ASTExpression value;
+    
+    static final String[] SETTING_NAMES = new String[] {
+            // Must be sorted alphabetically!
+            MutableProcessingConfiguration.BOOLEAN_FORMAT_KEY_CAMEL_CASE,
+            MutableProcessingConfiguration.BOOLEAN_FORMAT_KEY_SNAKE_CASE,
+            MutableProcessingConfiguration.DATE_FORMAT_KEY_CAMEL_CASE,
+            MutableProcessingConfiguration.DATE_FORMAT_KEY_SNAKE_CASE,
+            MutableProcessingConfiguration.DATETIME_FORMAT_KEY_CAMEL_CASE,
+            MutableProcessingConfiguration.DATETIME_FORMAT_KEY_SNAKE_CASE,
+            MutableProcessingConfiguration.LOCALE_KEY,
+            MutableProcessingConfiguration.NUMBER_FORMAT_KEY_CAMEL_CASE,
+            MutableProcessingConfiguration.NUMBER_FORMAT_KEY_SNAKE_CASE,
+            MutableProcessingConfiguration.OUTPUT_ENCODING_KEY_CAMEL_CASE,
+            MutableProcessingConfiguration.OUTPUT_ENCODING_KEY_SNAKE_CASE,
+            MutableProcessingConfiguration.SQL_DATE_AND_TIME_TIME_ZONE_KEY_CAMEL_CASE,
+            MutableProcessingConfiguration.SQL_DATE_AND_TIME_TIME_ZONE_KEY,
+            MutableProcessingConfiguration.TIME_FORMAT_KEY_CAMEL_CASE,
+            MutableProcessingConfiguration.TIME_ZONE_KEY_CAMEL_CASE,
+            MutableProcessingConfiguration.TIME_FORMAT_KEY_SNAKE_CASE,
+            MutableProcessingConfiguration.TIME_ZONE_KEY_SNAKE_CASE,
+            MutableProcessingConfiguration.URL_ESCAPING_CHARSET_KEY_CAMEL_CASE,
+            MutableProcessingConfiguration.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 (Configuration.ExtendableBuilder.getSettingNames(true).contains(key)
+                    || Configuration.ExtendableBuilder.getSettingNames(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 != ParsingConfiguration.AUTO_DETECT_NAMING_CONVENTION
+                            ? namingConvention : ParsingConfiguration.LEGACY_NAMING_CONVENTION /* [2.4] CAMEL_CASE */;
+                }
+                
+                boolean first = true;
+                for (String correctName : SETTING_NAMES) {
+                    int correctNameNamingConvention = _StringUtil.getIdentifierNamingConvention(correctName);
+                    if (shownNamingConvention == ParsingConfiguration.CAMEL_CASE_NAMING_CONVENTION
+                            ? correctNameNamingConvention != ParsingConfiguration.LEGACY_NAMING_CONVENTION
+                            : correctNameNamingConvention != ParsingConfiguration.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/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirStop.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirStop.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirStop.java
new file mode 100644
index 0000000..f453734
--- /dev/null
+++ b/freemarker-core/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 ASTDirective {
+
+    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/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirSwitch.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirSwitch.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirSwitch.java
new file mode 100644
index 0000000..e66c419
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirSwitch.java
@@ -0,0 +1,129 @@
+/*
+ * 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 ASTDirective {
+
+    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) {
+            // #break was called
+        }
+        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/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirTOrTrOrTl.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirTOrTrOrTl.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirTOrTrOrTl.java
new file mode 100644
index 0000000..937bc18
--- /dev/null
+++ b/freemarker-core/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 ASTDirective {
+    
+    private static final int TYPE_T = 0;
+    private static final int TYPE_LT = 1;
+    private static final int TYPE_RT = 2;
+    private static 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/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirUserDefined.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirUserDefined.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirUserDefined.java
new file mode 100644
index 0000000..6042bd8
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirUserDefined.java
@@ -0,0 +1,343 @@
+/*
+ * 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 ASTDirective implements DirectiveCallPlace {
+
+    private ASTExpression nameExp;
+    private Map namedArgs;
+    private List positionalArgs, bodyParameterNames;
+    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()) {
+                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/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirVisit.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirVisit.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirVisit.java
new file mode 100644
index 0000000..4a4023b
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirVisit.java
@@ -0,0 +1,126 @@
+/*
+ * 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;
+
+
+/**
+ * AST directive node: {@code #visit}.
+ */
+final class ASTDirVisit extends ASTDirective {
+    
+    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) {
+                NativeSequence ss = new NativeSequence(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/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirective.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirective.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirective.java
new file mode 100644
index 0000000..778fed1
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirective.java
@@ -0,0 +1,98 @@
+/*
+ * 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.Collections;
+import java.util.Set;
+import java.util.TreeSet;
+
+/**
+ * AST directive node superclass.
+ */
+abstract class ASTDirective extends ASTElement {
+
+    private static void addName(Set<String> allNames, Set<String> lcNames, Set<String> ccNames,
+                                String commonName) {
+        allNames.add(commonName);
+        lcNames.add(commonName);
+        ccNames.add(commonName);
+    }
+
+    private static void addName(Set<String> allNames, Set<String> lcNames, Set<String> ccNames,
+                                String lcName, String ccName) {
+        allNames.add(lcName);
+        allNames.add(ccName);
+        lcNames.add(lcName);
+        ccNames.add(ccName);
+    }
+
+    static final Set<String> ALL_BUILT_IN_DIRECTIVE_NAMES;
+    static final Set<String> LEGACY_BUILT_IN_DIRECTIVE_NAMES;
+    static final Set<String> CAMEL_CASE_BUILT_IN_DIRECTIVE_NAMES;
+    static {
+        Set<String> allNames = new TreeSet();
+        Set<String> lcNames = new TreeSet();
+        Set<String> ccNames = new TreeSet();
+
+        addName(allNames, lcNames, ccNames, "assign");
+        addName(allNames, lcNames, ccNames, "attempt");
+        addName(allNames, lcNames, ccNames, "autoesc", "autoEsc");
+        addName(allNames, lcNames, ccNames, "break");
+        addName(allNames, lcNames, ccNames, "case");
+        addName(allNames, lcNames, ccNames, "compress");
+        addName(allNames, lcNames, ccNames, "default");
+        addName(allNames, lcNames, ccNames, "else");
+        addName(allNames, lcNames, ccNames, "elseif", "elseIf");
+        addName(allNames, lcNames, ccNames, "escape");
+        addName(allNames, lcNames, ccNames, "fallback");
+        addName(allNames, lcNames, ccNames, "flush");
+        addName(allNames, lcNames, ccNames, "ftl");
+        addName(allNames, lcNames, ccNames, "function");
+        addName(allNames, lcNames, ccNames, "global");
+        addName(allNames, lcNames, ccNames, "if");
+        addName(allNames, lcNames, ccNames, "import");
+        addName(allNames, lcNames, ccNames, "include");
+        addName(allNames, lcNames, ccNames, "items");
+        addName(allNames, lcNames, ccNames, "list");
+        addName(allNames, lcNames, ccNames, "local");
+        addName(allNames, lcNames, ccNames, "lt");
+        addName(allNames, lcNames, ccNames, "macro");
+        addName(allNames, lcNames, ccNames, "nested");
+        addName(allNames, lcNames, ccNames, "noautoesc", "noAutoEsc");
+        addName(allNames, lcNames, ccNames, "noescape", "noEscape");
+        addName(allNames, lcNames, ccNames, "noparse", "noParse");
+        addName(allNames, lcNames, ccNames, "nt");
+        addName(allNames, lcNames, ccNames, "outputformat", "outputFormat");
+        addName(allNames, lcNames, ccNames, "recover");
+        addName(allNames, lcNames, ccNames, "recurse");
+        addName(allNames, lcNames, ccNames, "return");
+        addName(allNames, lcNames, ccNames, "rt");
+        addName(allNames, lcNames, ccNames, "sep");
+        addName(allNames, lcNames, ccNames, "setting");
+        addName(allNames, lcNames, ccNames, "stop");
+        addName(allNames, lcNames, ccNames, "switch");
+        addName(allNames, lcNames, ccNames, "t");
+        addName(allNames, lcNames, ccNames, "visit");
+
+        ALL_BUILT_IN_DIRECTIVE_NAMES = Collections.unmodifiableSet(allNames);
+        LEGACY_BUILT_IN_DIRECTIVE_NAMES = Collections.unmodifiableSet(lcNames);
+        CAMEL_CASE_BUILT_IN_DIRECTIVE_NAMES = Collections.unmodifiableSet(ccNames);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDollarInterpolation.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDollarInterpolation.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDollarInterpolation.java
new file mode 100644
index 0000000..1e5a7b4
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDollarInterpolation.java
@@ -0,0 +1,151 @@
+/*
+ * 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.model.TemplateMarkupOutputModel;
+import org.apache.freemarker.core.outputformat.MarkupOutputFormat;
+import org.apache.freemarker.core.outputformat.OutputFormat;
+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/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTElement.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTElement.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTElement.java
new file mode 100644
index 0000000..a9cbfc0
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTElement.java
@@ -0,0 +1,445 @@
+/*
+ * 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.util.Collections;
+import java.util.Enumeration;
+
+import org.apache.freemarker.core.util._ArrayEnumeration;
+
+/**
+ * AST non-expression node superclass: Superclass of directive calls, interpolations, static text, top-level comments,
+ * or other such non-expression node in the parsed template. Some information that can be found here can be accessed
+ * through the {@link Environment#getCurrentDirectiveCallPlace()}, which is a published API, and thus promises backward
+ * compatibility.
+ */
+// TODO [FM3] Get rid of "public" and thus the "_" prefix
+abstract class ASTElement extends ASTNode {
+
+    private static final int INITIAL_CHILD_BUFFER_CAPACITY = 6;
+
+    private ASTElement parent;
+
+    /**
+     * Contains 1 or more nested elements with optional trailing {@code null}-s, or is {@code null} exactly if there are
+     * no nested elements.
+     */
+    private ASTElement[] childBuffer;
+
+    /**
+     * Contains the number of elements in the {@link #childBuffer}, not counting the trailing {@code null}-s. If this is
+     * 0, then and only then {@link #childBuffer} must be {@code null}.
+     */
+    private int childCount;
+
+    /**
+     * The index of the element in the parent's {@link #childBuffer} array.
+     * 
+     * @since 2.3.23
+     */
+    private int index;
+
+    /**
+     * Executes this {@link ASTElement}. Usually should not be called directly, but through
+     * {@link Environment#visit(ASTElement)} or a similar {@link Environment} method.
+     *
+     * @param env
+     *            The runtime environment
+     * 
+     * @return The template elements to execute (meant to be used for nested elements), or {@code null}. Can have
+     *         <em>trailing</em> {@code null}-s (unused buffer capacity). Returning the nested elements instead of
+     *         executing them inside this method is a trick used for decreasing stack usage when there's nothing to do
+     *         after the children was processed anyway.
+     */
+    abstract ASTElement[] accept(Environment env) throws TemplateException, IOException;
+
+    /**
+     * One-line description of the element, that contain all the information that is used in {@link #getCanonicalForm()}
+     * , except the nested content (elements) of the element. The expressions inside the element (the parameters) has to
+     * be shown. Meant to be used for stack traces, also for tree views that don't go down to the expression-level.
+     * There are no backward-compatibility guarantees regarding the format used ATM, but it must be regular enough to be
+     * machine-parseable, and it must contain all information necessary for restoring an AST equivalent to the original.
+     * 
+     * This final implementation calls {@link #dump(boolean) dump(false)}.
+     * 
+     * @see #getCanonicalForm()
+     * @see #getNodeTypeSymbol()
+     */
+    public final String getDescription() {
+        return dump(false);
+    }
+
+    /**
+     * This final implementation calls {@link #dump(boolean) dump(false)}.
+     */
+    @Override
+    public final String getCanonicalForm() {
+        return dump(true);
+    }
+
+    final String getChildrenCanonicalForm() {
+        return getChildrenCanonicalForm(childBuffer);
+    }
+    
+    static String getChildrenCanonicalForm(ASTElement[] children) {
+        if (children == null) {
+            return "";
+        }
+        StringBuilder sb = new StringBuilder();
+        for (ASTElement child : children) {
+            if (child == null) {
+                break;
+            }
+            sb.append(child.getCanonicalForm());
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Tells if the element should show up in error stack traces. Note that this will be ignored for the top (current)
+     * element of a stack trace, as that's always shown.
+     */
+    boolean isShownInStackTrace() {
+        return false;
+    }
+
+    /**
+     * Tells if this element possibly executes its nested content for many times. This flag is useful when a template
+     * AST is modified for running time limiting (see {@link ThreadInterruptionSupportTemplatePostProcessor}). Elements
+     * that use {@link #childBuffer} should not need this, as the insertion of the timeout checks is impossible there,
+     * given their rigid nested element schema.
+     */
+    abstract boolean isNestedBlockRepeater();
+
+    /**
+     * Brings the implementation of {@link #getCanonicalForm()} and {@link #getDescription()} to a single place. Don't
+     * call those methods in method on {@code this}, because that will result in infinite recursion!
+     * 
+     * @param canonical
+     *            if {@code true}, it calculates the return value of {@link #getCanonicalForm()}, otherwise of
+     *            {@link #getDescription()}.
+     */
+    abstract protected String dump(boolean canonical);
+
+    // Methods to implement TemplateNodeModel
+
+    public String getNodeName() {
+        String className = getClass().getName();
+        int shortNameOffset = className.lastIndexOf('.') + 1;
+        return className.substring(shortNameOffset);
+    }
+
+    // Methods so that we can implement the Swing TreeNode API.
+
+    public boolean isLeaf() {
+        return childCount == 0;
+    }
+
+    public int getIndex(ASTElement node) {
+        for (int i = 0; i < childCount; i++) {
+            if (childBuffer[i].equals(node)) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    public int getChildCount() {
+        return childCount;
+    }
+
+    /**
+     * Note: For element with {@code #nestedBlock}, this will hide the {@code #nestedBlock} when that's a
+     * {@link ASTImplicitParent}.
+     */
+    public Enumeration children() {
+        return childBuffer != null
+                ? new _ArrayEnumeration(childBuffer, childCount)
+                : Collections.enumeration(Collections.EMPTY_LIST);
+    }
+
+    public void setChildAt(int index, ASTElement element) {
+        if (index < childCount && index >= 0) {
+            childBuffer[index] = element;
+            element.index = index;
+            element.parent = this;
+        } else {
+            throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + childCount);
+        }
+    }
+    
+    /**
+     * The element whose child this element is, or {@code null} if this is the root node.
+     */
+    final ASTElement getParent() {
+        return parent;
+    }
+
+    final void setChildBufferCapacity(int capacity) {
+        int ln = childCount;
+        ASTElement[] newChildBuffer = new ASTElement[capacity];
+        for (int i = 0; i < ln; i++) {
+            newChildBuffer[i] = childBuffer[i];
+        }
+        childBuffer = newChildBuffer;
+    }
+
+    /**
+     * Inserts a new nested element after the last nested element.
+     */
+    final void addChild(ASTElement nestedElement) {
+        addChild(childCount, nestedElement);
+    }
+
+    /**
+     * Inserts a new nested element at the given index, which can also be one higher than the current highest index.
+     */
+    final void addChild(int index, ASTElement nestedElement) {
+        final int lChildCount = childCount;
+
+        ASTElement[] lChildBuffer = childBuffer;
+        if (lChildBuffer == null) {
+            lChildBuffer = new ASTElement[INITIAL_CHILD_BUFFER_CAPACITY];
+            childBuffer = lChildBuffer;
+        } else if (lChildCount == lChildBuffer.length) {
+            setChildBufferCapacity(lChildCount != 0 ? lChildCount * 2 : 1);
+            lChildBuffer = childBuffer;
+        }
+        // At this point: nestedElements == this.nestedElements, and has sufficient capacity.
+
+        for (int i = lChildCount; i > index; i--) {
+            ASTElement movedElement = lChildBuffer[i - 1];
+            movedElement.index = i;
+            lChildBuffer[i] = movedElement;
+        }
+        nestedElement.index = index;
+        nestedElement.parent = this;
+        lChildBuffer[index] = nestedElement;
+        childCount = lChildCount + 1;
+    }
+
+    final ASTElement getChild(int index) {
+        return childBuffer[index];
+    }
+
+    /**
+     * @return Array containing 1 or more nested elements with optional trailing {@code null}-s, or is {@code null}
+     *         exactly if there are no nested elements.
+     */
+    final ASTElement[] getChildBuffer() {
+        return childBuffer;
+    }
+
+    /**
+     * @param buffWithCnt Maybe {@code null}
+     * 
+     * @since 2.3.24
+     */
+    final void setChildren(TemplateElements buffWithCnt) {
+        ASTElement[] childBuffer = buffWithCnt.getBuffer();
+        int childCount = buffWithCnt.getCount();
+        for (int i = 0; i < childCount; i++) {
+            ASTElement child = childBuffer[i];
+            child.index = i;
+            child.parent = this;
+        }
+        this.childBuffer = childBuffer;
+        this.childCount = childCount;
+    }
+
+    final int getIndex() {
+        return index;
+    }
+
+    /**
+     * This is a special case, because a root element is not contained in another element, so we couldn't set the
+     * private fields.
+     */
+    final void setFieldsForRootElement() {
+        index = 0;
+        parent = null;
+    }
+
+    /**
+     * Walk the AST subtree rooted by this element, and do simplifications where possible, also removes superfluous
+     * whitespace.
+     * 
+     * @param stripWhitespace
+     *            whether to remove superfluous whitespace
+     * 
+     * @return The element this element should be replaced with in the parent. If it's the same as this element, no
+     *         actual replacement will happen. Note that adjusting the {@link #parent} and {@link #index} of the result
+     *         is the duty of the caller, not of this method.
+     */
+    ASTElement postParseCleanup(boolean stripWhitespace) throws ParseException {
+        int childCount = this.childCount;
+        if (childCount != 0) {
+            for (int i = 0; i < childCount; i++) {
+                ASTElement te = childBuffer[i];
+                
+                /*
+                // Assertion:
+                if (te.getIndex() != i) {
+                    throw new BugException("Invalid index " + te.getIndex() + " (expected: "
+                            + i + ") for: " + te.dump(false));
+                }
+                if (te.getParent() != this) {
+                    throw new BugException("Invalid parent " + te.getParent() + " (expected: "
+                            + this.dump(false) + ") for: " + te.dump(false));
+                }
+                */
+                
+                te = te.postParseCleanup(stripWhitespace);
+                childBuffer[i] = te;
+                te.parent = this;
+                te.index = i;
+            }
+            for (int i = 0; i < childCount; i++) {
+                ASTElement te = childBuffer[i];
+                if (te.isIgnorable(stripWhitespace)) {
+                    childCount--;
+                    // As later isIgnorable calls might investigates the siblings, we have to move all the items now. 
+                    for (int j = i; j < childCount; j++) {
+                        final ASTElement te2 = childBuffer[j + 1];
+                        childBuffer[j] = te2;
+                        te2.index = j;
+                    }
+                    childBuffer[childCount] = null;
+                    this.childCount = childCount;
+                    i--;
+                }
+            }
+            if (childCount == 0) {
+                childBuffer = null;
+            } else if (childCount < childBuffer.length
+                    && childCount <= childBuffer.length * 3 / 4) {
+                ASTElement[] trimmedChildBuffer = new ASTElement[childCount];
+                for (int i = 0; i < childCount; i++) {
+                    trimmedChildBuffer[i] = childBuffer[i];
+                }
+                childBuffer = trimmedChildBuffer;
+            }
+        }
+        return this;
+    }
+
+    boolean isIgnorable(boolean stripWhitespace) {
+        return false;
+    }
+
+    // The following methods exist to support some fancier tree-walking
+    // and were introduced to support the whitespace cleanup feature in 2.2
+
+    ASTElement prevTerminalNode() {
+        ASTElement prev = previousSibling();
+        if (prev != null) {
+            return prev.getLastLeaf();
+        } else if (parent != null) {
+            return parent.prevTerminalNode();
+        }
+        return null;
+    }
+
+    ASTElement nextTerminalNode() {
+        ASTElement next = nextSibling();
+        if (next != null) {
+            return next.getFirstLeaf();
+        } else if (parent != null) {
+            return parent.nextTerminalNode();
+        }
+        return null;
+    }
+
+    ASTElement previousSibling() {
+        if (parent == null) {
+            return null;
+        }
+        return index > 0 ? parent.childBuffer[index - 1] : null;
+    }
+
+    ASTElement nextSibling() {
+        if (parent == null) {
+            return null;
+        }
+        return index + 1 < parent.childCount ? parent.childBuffer[index + 1] : null;
+    }
+
+    private ASTElement getFirstChild() {
+        return childCount == 0 ? null : childBuffer[0];
+    }
+
+    private ASTElement getLastChild() {
+        final int childCount = this.childCount;
+        return childCount == 0 ? null : childBuffer[childCount - 1];
+    }
+
+    private ASTElement getFirstLeaf() {
+        ASTElement te = this;
+        while (!te.isLeaf() && !(te instanceof ASTDirMacro) && !(te instanceof ASTDirCapturingAssignment)) {
+            // A macro or macro invocation is treated as a leaf here for special reasons
+            te = te.getFirstChild();
+        }
+        return te;
+    }
+
+    private ASTElement getLastLeaf() {
+        ASTElement te = this;
+        while (!te.isLeaf() && !(te instanceof ASTDirMacro) && !(te instanceof ASTDirCapturingAssignment)) {
+            // A macro or macro invocation is treated as a leaf here for special reasons
+            te = te.getLastChild();
+        }
+        return te;
+    }
+
+    /**
+     * Tells if executing this element has output that only depends on the template content and that has no side
+     * effects.
+     */
+    boolean isOutputCacheable() {
+        return false;
+    }
+
+    boolean isChildrenOutputCacheable() {
+        int ln = childCount;
+        for (int i = 0; i < ln; i++) {
+            if (!childBuffer[i].isOutputCacheable()) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * determines whether this element's presence on a line indicates that we should not strip opening whitespace in the
+     * post-parse whitespace gobbling step.
+     */
+    boolean heedsOpeningWhitespace() {
+        return false;
+    }
+
+    /**
+     * determines whether this element's presence on a line indicates that we should not strip trailing whitespace in
+     * the post-parse whitespace gobbling step.
+     */
+    boolean heedsTrailingWhitespace() {
+        return false;
+    }
+}