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:25 UTC

[42/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/BuiltInsForMultipleTypes.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForMultipleTypes.java b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForMultipleTypes.java
new file mode 100644
index 0000000..ab3df64
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForMultipleTypes.java
@@ -0,0 +1,717 @@
+/*
+ * 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.Date;
+import java.util.List;
+
+import org.apache.freemarker.core.model.TemplateBooleanModel;
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+import org.apache.freemarker.core.model.TemplateCollectionModelEx;
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateDirectiveModel;
+import org.apache.freemarker.core.model.TemplateHashModel;
+import org.apache.freemarker.core.model.TemplateHashModelEx;
+import org.apache.freemarker.core.model.TemplateMarkupOutputModel;
+import org.apache.freemarker.core.model.TemplateMethodModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateModelWithAPISupport;
+import org.apache.freemarker.core.model.TemplateNodeModel;
+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.TemplateTransformModel;
+import org.apache.freemarker.core.model.impl.SimpleDate;
+import org.apache.freemarker.core.model.impl.SimpleNumber;
+import org.apache.freemarker.core.model.impl.SimpleScalar;
+import org.apache.freemarker.core.util.BugException;
+import org.apache.freemarker.core.valueformat.TemplateDateFormat;
+import org.apache.freemarker.core.valueformat.TemplateNumberFormat;
+import org.apache.freemarker.core.valueformat.TemplateValueFormatException;
+
+/**
+ * A holder for builtins that didn't fit into any other category.
+ */
+class BuiltInsForMultipleTypes {
+
+    static class cBI extends AbstractCBI {
+        
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            TemplateModel model = target.eval(env);
+            if (model instanceof TemplateNumberModel) {
+                return formatNumber(env, model);
+            } else if (model instanceof TemplateBooleanModel) {
+                return new SimpleScalar(((TemplateBooleanModel) model).getAsBoolean()
+                        ? MiscUtil.C_TRUE : MiscUtil.C_FALSE);
+            } else {
+                throw new UnexpectedTypeException(
+                        target, model,
+                        "number or boolean", new Class[] { TemplateNumberModel.class, TemplateBooleanModel.class },
+                        env);
+            }
+        }
+
+        @Override
+        protected TemplateModel formatNumber(Environment env, TemplateModel model) throws TemplateModelException {
+            Number num = _EvalUtil.modelToNumber((TemplateNumberModel) model, target);
+            if (num instanceof Integer || num instanceof Long) {
+                // Accelerate these fairly common cases
+                return new SimpleScalar(num.toString());
+            } else if (num instanceof Double) {
+                double n = num.doubleValue();
+                if (n == Double.POSITIVE_INFINITY) {
+                    return new SimpleScalar("INF");
+                }
+                if (n == Double.NEGATIVE_INFINITY) {
+                    return new SimpleScalar("-INF");
+                }
+                if (Double.isNaN(n)) {
+                    return new SimpleScalar("NaN");
+                }
+                // Deliberately falls through
+            } else if (num instanceof Float) {
+                float n = num.floatValue();
+                if (n == Float.POSITIVE_INFINITY) {
+                    return new SimpleScalar("INF");
+                }
+                if (n == Float.NEGATIVE_INFINITY) {
+                    return new SimpleScalar("-INF");
+                }
+                if (Float.isNaN(n)) {
+                    return new SimpleScalar("NaN");
+                }
+                // Deliberately falls through
+            }
+        
+            return new SimpleScalar(env.getCNumberFormat().format(num));
+        }
+        
+    }
+
+    static class dateBI extends ASTExpBuiltIn {
+        private class DateParser
+        implements
+            TemplateDateModel,
+            TemplateMethodModel,
+            TemplateHashModel {
+            private final String text;
+            private final Environment env;
+            private final TemplateDateFormat defaultFormat;
+            private TemplateDateModel cachedValue;
+            
+            DateParser(String text, Environment env)
+            throws TemplateException {
+                this.text = text;
+                this.env = env;
+                defaultFormat = env.getTemplateDateFormat(dateType, Date.class, target, false);
+            }
+            
+            @Override
+            public Object exec(List args) throws TemplateModelException {
+                checkMethodArgCount(args, 0, 1);
+                return args.size() == 0 ? getAsDateModel() : get((String) args.get(0));
+            }
+            
+            @Override
+            public TemplateModel get(String pattern) throws TemplateModelException {
+                TemplateDateFormat format;
+                try {
+                    format = env.getTemplateDateFormat(pattern, dateType, Date.class, target, dateBI.this, true);
+                } catch (TemplateException e) {
+                    // `e` should always be a TemplateModelException here, but to be sure: 
+                    throw _CoreAPI.ensureIsTemplateModelException("Failed to get format", e); 
+                }
+                return toTemplateDateModel(parse(format));
+            }
+
+            private TemplateDateModel toTemplateDateModel(Object date) throws _TemplateModelException {
+                if (date instanceof Date) {
+                    return new SimpleDate((Date) date, dateType);
+                } else {
+                    TemplateDateModel tm = (TemplateDateModel) date;
+                    if (tm.getDateType() != dateType) {
+                        throw new _TemplateModelException("The result of the parsing was of the wrong date type.");
+                    }
+                    return tm;
+                }
+            }
+
+            private TemplateDateModel getAsDateModel() throws TemplateModelException {
+                if (cachedValue == null) {
+                    cachedValue = toTemplateDateModel(parse(defaultFormat));
+                }
+                return cachedValue;
+            }
+            
+            @Override
+            public Date getAsDate() throws TemplateModelException {
+                return getAsDateModel().getAsDate();
+            }
+    
+            @Override
+            public int getDateType() {
+                return dateType;
+            }
+    
+            @Override
+            public boolean isEmpty() {
+                return false;
+            }
+    
+            private Object parse(TemplateDateFormat df)
+            throws TemplateModelException {
+                try {
+                    return df.parse(text, dateType);
+                } catch (TemplateValueFormatException e) {
+                    throw new _TemplateModelException(e,
+                            "The string doesn't match the expected date/time/date-time format. "
+                            + "The string to parse was: ", new _DelayedJQuote(text), ". ",
+                            "The expected format was: ", new _DelayedJQuote(df.getDescription()), ".",
+                            e.getMessage() != null ? "\nThe nested reason given follows:\n" : "",
+                            e.getMessage() != null ? e.getMessage() : "");
+                }
+            }
+            
+        }
+        
+        private final int dateType;
+        
+        dateBI(int dateType) {
+            this.dateType = dateType;
+        }
+        
+        @Override
+        TemplateModel _eval(Environment env)
+                throws TemplateException {
+            TemplateModel model = target.eval(env);
+            if (model instanceof TemplateDateModel) {
+                TemplateDateModel dmodel = (TemplateDateModel) model;
+                int dtype = dmodel.getDateType();
+                // Any date model can be coerced into its own type
+                if (dateType == dtype) {
+                    return model;
+                }
+                // unknown and datetime can be coerced into any date type
+                if (dtype == TemplateDateModel.UNKNOWN || dtype == TemplateDateModel.DATETIME) {
+                    return new SimpleDate(dmodel.getAsDate(), dateType);
+                }
+                throw new _MiscTemplateException(this,
+                            "Cannot convert ", TemplateDateModel.TYPE_NAMES.get(dtype),
+                            " to ", TemplateDateModel.TYPE_NAMES.get(dateType));
+            }
+            // Otherwise, interpret as a string and attempt 
+            // to parse it into a date.
+            String s = target.evalAndCoerceToPlainText(env);
+            return new DateParser(s, env);
+        }
+
+    }
+
+    static class apiBI extends ASTExpBuiltIn {
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            if (!env.getAPIBuiltinEnabled()) {
+                throw new _MiscTemplateException(this,
+                        "Can't use ?api, because the \"", MutableProcessingConfiguration.API_BUILTIN_ENABLED_KEY,
+                        "\" configuration setting is false. Think twice before you set it to true though. Especially, "
+                        + "it shouldn't abused for modifying Map-s and Collection-s.");
+            }
+            final TemplateModel tm = target.eval(env);
+            if (!(tm instanceof TemplateModelWithAPISupport)) {
+                target.assertNonNull(tm, env);
+                throw new APINotSupportedTemplateException(env, target, tm);
+            }
+            return ((TemplateModelWithAPISupport) tm).getAPI();
+        }
+    }
+
+    static class has_apiBI extends ASTExpBuiltIn {
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            final TemplateModel tm = target.eval(env);
+            target.assertNonNull(tm, env);
+            return tm instanceof TemplateModelWithAPISupport ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+        }
+    }
+    
+    static class is_booleanBI extends ASTExpBuiltIn {
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            TemplateModel tm = target.eval(env);
+            target.assertNonNull(tm, env);
+            return (tm instanceof TemplateBooleanModel)  ?
+                TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+        }
+    }
+
+    static class is_collectionBI extends ASTExpBuiltIn {
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            TemplateModel tm = target.eval(env);
+            target.assertNonNull(tm, env);
+            return (tm instanceof TemplateCollectionModel) ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+        }
+    }
+
+    static class is_collection_exBI extends ASTExpBuiltIn {
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            TemplateModel tm = target.eval(env);
+            target.assertNonNull(tm, env);
+            return (tm instanceof TemplateCollectionModelEx) ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+        }
+    }
+
+    static class is_dateLikeBI extends ASTExpBuiltIn {
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            TemplateModel tm = target.eval(env);
+            target.assertNonNull(tm, env);
+            return (tm instanceof TemplateDateModel)  ?
+                TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+        }
+    }
+
+    static class is_dateOfTypeBI extends ASTExpBuiltIn {
+        
+        private final int dateType;
+        
+        is_dateOfTypeBI(int dateType) {
+            this.dateType = dateType;
+        }
+
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            TemplateModel tm = target.eval(env);
+            target.assertNonNull(tm, env);
+            return (tm instanceof TemplateDateModel) && ((TemplateDateModel) tm).getDateType() == dateType
+                ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+        }
+    }
+
+    static class is_directiveBI extends ASTExpBuiltIn {
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            TemplateModel tm = target.eval(env);
+            target.assertNonNull(tm, env);
+            // WRONG: it also had to check ASTDirMacro.isFunction()
+            return (tm instanceof TemplateTransformModel || tm instanceof ASTDirMacro || tm instanceof TemplateDirectiveModel) ?
+                TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+        }
+    }
+
+    static class is_enumerableBI extends ASTExpBuiltIn {
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            TemplateModel tm = target.eval(env);
+            target.assertNonNull(tm, env);
+            return (tm instanceof TemplateSequenceModel || tm instanceof TemplateCollectionModel)
+                    ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+        }
+    }
+
+    static class is_hash_exBI extends ASTExpBuiltIn {
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            TemplateModel tm = target.eval(env);
+            target.assertNonNull(tm, env);
+            return (tm instanceof TemplateHashModelEx) ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+        }
+    }
+
+    static class is_hashBI extends ASTExpBuiltIn {
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            TemplateModel tm = target.eval(env);
+            target.assertNonNull(tm, env);
+            return (tm instanceof TemplateHashModel) ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+        }
+    }
+
+    static class is_indexableBI extends ASTExpBuiltIn {
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            TemplateModel tm = target.eval(env);
+            target.assertNonNull(tm, env);
+            return (tm instanceof TemplateSequenceModel) ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+        }
+    }
+
+    static class is_macroBI extends ASTExpBuiltIn {
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            TemplateModel tm = target.eval(env);
+            target.assertNonNull(tm, env);
+            // WRONG: it also had to check ASTDirMacro.isFunction()
+            return (tm instanceof ASTDirMacro)  ?
+                TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+        }
+    }
+
+    static class is_markup_outputBI extends ASTExpBuiltIn {
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            TemplateModel tm = target.eval(env);
+            target.assertNonNull(tm, env);
+            return (tm instanceof TemplateMarkupOutputModel)  ?
+                TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+        }
+    }
+    
+    static class is_methodBI extends ASTExpBuiltIn {
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            TemplateModel tm = target.eval(env);
+            target.assertNonNull(tm, env);
+            return (tm instanceof TemplateMethodModel)  ?
+                TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+        }
+    }
+
+    static class is_nodeBI extends ASTExpBuiltIn {
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            TemplateModel tm = target.eval(env);
+            target.assertNonNull(tm, env);
+            return (tm instanceof TemplateNodeModel)  ?
+                TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+        }
+    }
+
+    static class is_numberBI extends ASTExpBuiltIn {
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            TemplateModel tm = target.eval(env);
+            target.assertNonNull(tm, env);
+            return (tm instanceof TemplateNumberModel)  ?
+                TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+        }
+    }
+
+    static class is_sequenceBI extends ASTExpBuiltIn {
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            TemplateModel tm = target.eval(env);
+            target.assertNonNull(tm, env);
+            return tm instanceof TemplateSequenceModel
+                    ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+        }
+    }
+
+    static class is_stringBI extends ASTExpBuiltIn {
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            TemplateModel tm = target.eval(env);
+            target.assertNonNull(tm, env);
+            return (tm instanceof TemplateScalarModel)  ?
+                TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+        }
+    }
+
+    static class is_transformBI extends ASTExpBuiltIn {
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            TemplateModel tm = target.eval(env);
+            target.assertNonNull(tm, env);
+            return (tm instanceof TemplateTransformModel)  ?
+                TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+        }
+    }
+
+    static class namespaceBI extends ASTExpBuiltIn {
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            TemplateModel tm = target.eval(env);
+            if (!(tm instanceof ASTDirMacro)) {
+                throw new UnexpectedTypeException(
+                        target, tm,
+                        "macro or function", new Class[] { ASTDirMacro.class },
+                        env);
+            } else {
+                return env.getMacroNamespace((ASTDirMacro) tm);
+            }
+        }
+    }
+
+    static class sizeBI extends ASTExpBuiltIn {
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            TemplateModel model = target.eval(env);
+
+            final int size;
+            if (model instanceof TemplateSequenceModel) {
+                size = ((TemplateSequenceModel) model).size();
+            } else if (model instanceof TemplateCollectionModelEx) {
+                size = ((TemplateCollectionModelEx) model).size();
+            } else if (model instanceof TemplateHashModelEx) {
+                size = ((TemplateHashModelEx) model).size();
+            } else {
+                throw new UnexpectedTypeException(
+                        target, model,
+                        "extended-hash or sequence or extended collection",
+                        new Class[] {
+                                TemplateHashModelEx.class,
+                                TemplateSequenceModel.class,
+                                TemplateCollectionModelEx.class
+                        },
+                        env);
+            }
+            return new SimpleNumber(size);
+        }
+    }
+    
+    static class stringBI extends ASTExpBuiltIn {
+        
+        private class BooleanFormatter
+        implements 
+            TemplateScalarModel, 
+            TemplateMethodModel {
+            private final TemplateBooleanModel bool;
+            private final Environment env;
+            
+            BooleanFormatter(TemplateBooleanModel bool, Environment env) {
+                this.bool = bool;
+                this.env = env;
+            }
+    
+            @Override
+            public Object exec(List args) throws TemplateModelException {
+                checkMethodArgCount(args, 2);
+                return new SimpleScalar((String) args.get(bool.getAsBoolean() ? 0 : 1));
+            }
+    
+            @Override
+            public String getAsString() throws TemplateModelException {
+                // Boolean should have come first... but that change would be non-BC. 
+                if (bool instanceof TemplateScalarModel) {
+                    return ((TemplateScalarModel) bool).getAsString();
+                } else {
+                    try {
+                        return env.formatBoolean(bool.getAsBoolean(), true);
+                    } catch (TemplateException e) {
+                        throw new TemplateModelException(e);
+                    }
+                }
+            }
+        }
+    
+        private class DateFormatter
+        implements
+            TemplateScalarModel,
+            TemplateHashModel,
+            TemplateMethodModel {
+            private final TemplateDateModel dateModel;
+            private final Environment env;
+            private final TemplateDateFormat defaultFormat;
+            private String cachedValue;
+    
+            DateFormatter(TemplateDateModel dateModel, Environment env)
+            throws TemplateException {
+                this.dateModel = dateModel;
+                this.env = env;
+                
+                final int dateType = dateModel.getDateType();
+                defaultFormat = dateType == TemplateDateModel.UNKNOWN
+                        ? null  // Lazy unknown type error in getAsString()
+                        : env.getTemplateDateFormat(
+                                dateType, _EvalUtil.modelToDate(dateModel, target).getClass(), target, true);
+            }
+    
+            @Override
+            public Object exec(List args) throws TemplateModelException {
+                checkMethodArgCount(args, 1);
+                return formatWith((String) args.get(0));
+            }
+
+            @Override
+            public TemplateModel get(String key)
+            throws TemplateModelException {
+                return formatWith(key);
+            }
+
+            private TemplateModel formatWith(String key)
+            throws TemplateModelException {
+                try {
+                    return new SimpleScalar(env.formatDateToPlainText(dateModel, key, target, stringBI.this, true));
+                } catch (TemplateException e) {
+                    // `e` should always be a TemplateModelException here, but to be sure: 
+                    throw _CoreAPI.ensureIsTemplateModelException("Failed to format value", e); 
+                }
+            }
+            
+            @Override
+            public String getAsString()
+            throws TemplateModelException {
+                if (cachedValue == null) {
+                    if (defaultFormat == null) {
+                        if (dateModel.getDateType() == TemplateDateModel.UNKNOWN) {
+                            throw MessageUtil.newCantFormatUnknownTypeDateException(target, null);
+                        } else {
+                            throw new BugException();
+                        }
+                    }
+                    try {
+                        cachedValue = _EvalUtil.assertFormatResultNotNull(defaultFormat.formatToPlainText(dateModel));
+                    } catch (TemplateValueFormatException e) {
+                        try {
+                            throw MessageUtil.newCantFormatDateException(defaultFormat, target, e, true);
+                        } catch (TemplateException e2) {
+                            // `e` should always be a TemplateModelException here, but to be sure: 
+                            throw _CoreAPI.ensureIsTemplateModelException("Failed to format date/time/datetime", e2); 
+                        }
+                    }
+                }
+                return cachedValue;
+            }
+    
+            @Override
+            public boolean isEmpty() {
+                return false;
+            }
+        }
+        
+        private class NumberFormatter
+        implements
+            TemplateScalarModel,
+            TemplateHashModel,
+            TemplateMethodModel {
+            private final TemplateNumberModel numberModel;
+            private final Number number;
+            private final Environment env;
+            private final TemplateNumberFormat defaultFormat;
+            private String cachedValue;
+    
+            NumberFormatter(TemplateNumberModel numberModel, Environment env) throws TemplateException {
+                this.env = env;
+                
+                // As we format lazily, we need a snapshot of the format inputs:
+                this.numberModel = numberModel;
+                number = _EvalUtil.modelToNumber(numberModel, target);  // for BackwardCompatibleTemplateNumberFormat-s
+                try {
+                    defaultFormat = env.getTemplateNumberFormat(stringBI.this, true);
+                } catch (TemplateException e) {
+                    // `e` should always be a TemplateModelException here, but to be sure: 
+                    throw _CoreAPI.ensureIsTemplateModelException("Failed to get default number format", e); 
+                }
+            }
+    
+            @Override
+            public Object exec(List args) throws TemplateModelException {
+                checkMethodArgCount(args, 1);
+                return get((String) args.get(0));
+            }
+    
+            @Override
+            public TemplateModel get(String key) throws TemplateModelException {
+                TemplateNumberFormat format;
+                try {
+                    format = env.getTemplateNumberFormat(key, stringBI.this, true);
+                } catch (TemplateException e) {
+                    // `e` should always be a TemplateModelException here, but to be sure: 
+                    throw _CoreAPI.ensureIsTemplateModelException("Failed to get number format", e); 
+                }
+                
+                String result;
+                try {
+                    result = env.formatNumberToPlainText(numberModel, format, target, true);
+                } catch (TemplateException e) {
+                    // `e` should always be a TemplateModelException here, but to be sure: 
+                    throw _CoreAPI.ensureIsTemplateModelException("Failed to format number", e); 
+                }
+                
+                return new SimpleScalar(result);
+            }
+            
+            @Override
+            public String getAsString() throws TemplateModelException {
+                if (cachedValue == null) {
+                    try {
+                        cachedValue = env.formatNumberToPlainText(numberModel, defaultFormat, target, true);
+                    } catch (TemplateException e) {
+                        // `e` should always be a TemplateModelException here, but to be sure: 
+                        throw _CoreAPI.ensureIsTemplateModelException("Failed to format number", e); 
+                    }
+                }
+                return cachedValue;
+            }
+    
+            @Override
+            public boolean isEmpty() {
+                return false;
+            }
+        }
+    
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            TemplateModel model = target.eval(env);
+            if (model instanceof TemplateNumberModel) {
+                TemplateNumberModel numberModel = (TemplateNumberModel) model;
+                Number num = _EvalUtil.modelToNumber(numberModel, target);
+                return new NumberFormatter(numberModel, env);
+            } else if (model instanceof TemplateDateModel) {
+                TemplateDateModel dm = (TemplateDateModel) model;
+                return new DateFormatter(dm, env);
+            } else if (model instanceof SimpleScalar) {
+                return model;
+            } else if (model instanceof TemplateBooleanModel) {
+                return new BooleanFormatter((TemplateBooleanModel) model, env);
+            } else if (model instanceof TemplateScalarModel) {
+                return new SimpleScalar(((TemplateScalarModel) model).getAsString());
+            } else {            
+                throw new UnexpectedTypeException(
+                        target, model,
+                        "number, date, boolean or string",
+                        new Class[] {
+                            TemplateNumberModel.class, TemplateDateModel.class, TemplateBooleanModel.class,
+                            TemplateScalarModel.class
+                        },
+                        env);
+            }
+        }
+    }
+
+    // Can't be instantiated
+    private BuiltInsForMultipleTypes() { }
+
+    static abstract class AbstractCBI extends ASTExpBuiltIn {
+        
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            TemplateModel model = target.eval(env);
+            if (model instanceof TemplateNumberModel) {
+                return formatNumber(env, model);
+            } else if (model instanceof TemplateBooleanModel) {
+                return new SimpleScalar(((TemplateBooleanModel) model).getAsBoolean()
+                        ? MiscUtil.C_TRUE : MiscUtil.C_FALSE);
+            } else {
+                throw new UnexpectedTypeException(
+                        target, model,
+                        "number or boolean", new Class[] { TemplateNumberModel.class, TemplateBooleanModel.class },
+                        env);
+            }
+        }
+    
+        protected abstract TemplateModel formatNumber(Environment env, TemplateModel model) throws TemplateModelException;
+        
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForNodes.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForNodes.java b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForNodes.java
new file mode 100644
index 0000000..39bc546
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForNodes.java
@@ -0,0 +1,154 @@
+/*
+ * 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.List;
+
+import org.apache.freemarker.core.model.TemplateMethodModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateNodeModel;
+import org.apache.freemarker.core.model.TemplateNodeModelEx;
+import org.apache.freemarker.core.model.impl.SimpleScalar;
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * A holder for builtins that operate exclusively on (XML-)node left-hand value.
+ */
+class BuiltInsForNodes {
+    
+    static class ancestorsBI extends BuiltInForNode {
+       @Override
+    TemplateModel calculateResult(TemplateNodeModel nodeModel, Environment env) throws TemplateModelException {
+           AncestorSequence result = new AncestorSequence(env);
+           TemplateNodeModel parent = nodeModel.getParentNode();
+           while (parent != null) {
+               result.add(parent);
+               parent = parent.getParentNode();
+           }
+           return result;
+       }
+    }
+    
+    static class childrenBI extends BuiltInForNode {
+       @Override
+    TemplateModel calculateResult(TemplateNodeModel nodeModel, Environment env) throws TemplateModelException {
+            return nodeModel.getChildNodes();
+       }
+    }
+    
+    static class node_nameBI extends BuiltInForNode {
+       @Override
+    TemplateModel calculateResult(TemplateNodeModel nodeModel, Environment env) throws TemplateModelException {
+            return new SimpleScalar(nodeModel.getNodeName());
+       }
+    }
+    
+    
+    static class node_namespaceBI extends BuiltInForNode {
+        @Override
+        TemplateModel calculateResult(TemplateNodeModel nodeModel, Environment env) throws TemplateModelException {
+            String nsURI = nodeModel.getNodeNamespace();
+            return nsURI == null ? null : new SimpleScalar(nsURI);
+        }
+    }
+    
+    static class node_typeBI extends BuiltInForNode {
+       @Override
+    TemplateModel calculateResult(TemplateNodeModel nodeModel, Environment env) throws TemplateModelException {
+            return new SimpleScalar(nodeModel.getNodeType());
+        }
+    }
+
+    static class parentBI extends BuiltInForNode {
+       @Override
+    TemplateModel calculateResult(TemplateNodeModel nodeModel, Environment env) throws TemplateModelException {
+            return nodeModel.getParentNode();
+       }
+    }
+    
+    static class rootBI extends BuiltInForNode {
+       @Override
+    TemplateModel calculateResult(TemplateNodeModel nodeModel, Environment env) throws TemplateModelException {
+            TemplateNodeModel result = nodeModel;
+            TemplateNodeModel parent = nodeModel.getParentNode();
+            while (parent != null) {
+                result = parent;
+                parent = result.getParentNode();
+            }
+            return result;
+       }
+    }
+
+    static class previousSiblingBI extends BuiltInForNodeEx {
+        @Override
+        TemplateModel calculateResult(TemplateNodeModelEx nodeModel, Environment env) throws TemplateModelException {
+            return nodeModel.getPreviousSibling();
+        }
+    }
+
+    static class nextSiblingBI extends  BuiltInForNodeEx {
+        @Override
+        TemplateModel calculateResult(TemplateNodeModelEx nodeModel, Environment env) throws TemplateModelException {
+            return nodeModel.getNextSibling();
+        }
+    }
+    
+    // Can't be instantiated
+    private BuiltInsForNodes() { }
+
+    static class AncestorSequence extends NativeSequence implements TemplateMethodModel {
+
+        private static final int INITIAL_CAPACITY = 12;
+
+        private Environment env;
+        
+        AncestorSequence(Environment env) {
+            super(INITIAL_CAPACITY);
+            this.env = env;
+        }
+        
+        @Override
+        public Object exec(List names) throws TemplateModelException {
+            if (names == null || names.isEmpty()) {
+                return this;
+            }
+            AncestorSequence result = new AncestorSequence(env);
+            for (int i = 0; i < size(); i++) {
+                TemplateNodeModel tnm = (TemplateNodeModel) get(i);
+                String nodeName = tnm.getNodeName();
+                String nsURI = tnm.getNodeNamespace();
+                if (nsURI == null) {
+                    if (names.contains(nodeName)) {
+                        result.add(tnm);
+                    }
+                } else {
+                    for (int j = 0; j < names.size(); j++) {
+                        if (_StringUtil.matchesQName((String) names.get(j), nodeName, nsURI, env)) {
+                            result.add(tnm);
+                            break;
+                        }
+                    }
+                }
+            }
+            return result;
+        }
+    }    
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForNumbers.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForNumbers.java b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForNumbers.java
new file mode 100644
index 0000000..58a2aa6
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForNumbers.java
@@ -0,0 +1,319 @@
+/*
+ * 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.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.Date;
+
+import org.apache.freemarker.core.model.TemplateBooleanModel;
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.model.impl.SimpleDate;
+import org.apache.freemarker.core.model.impl.SimpleNumber;
+import org.apache.freemarker.core.model.impl.SimpleScalar;
+import org.apache.freemarker.core.util._NumberUtil;
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * A holder for builtins that operate exclusively on number left-hand value.
+ */
+class BuiltInsForNumbers {
+
+    private static abstract class abcBI extends BuiltInForNumber {
+
+        @Override
+        TemplateModel calculateResult(Number num, TemplateModel model) throws TemplateModelException {
+            final int n;
+            try {
+                n = _NumberUtil.toIntExact(num);
+            } catch (ArithmeticException e) {
+                throw new _TemplateModelException(target,
+                        "The left side operand value isn't compatible with ?", key, ": ", e.getMessage());
+         
+            }
+            if (n <= 0) {
+                throw new _TemplateModelException(target,
+                        "The left side operand of to ?", key, " must be at least 1, but was ", Integer.valueOf(n), ".");
+            }
+            return new SimpleScalar(toABC(n));
+        }
+
+        protected abstract String toABC(int n);
+        
+    }
+
+    static class lower_abcBI extends abcBI {
+
+        @Override
+        protected String toABC(int n) {
+            return _StringUtil.toLowerABC(n);
+        }
+        
+    }
+
+    static class upper_abcBI extends abcBI {
+
+        @Override
+        protected String toABC(int n) {
+            return _StringUtil.toUpperABC(n);
+        }
+        
+    }
+    
+    static class absBI extends BuiltInForNumber {
+        @Override
+        TemplateModel calculateResult(Number num, TemplateModel model) throws TemplateModelException {
+            if (num instanceof Integer) {
+                int n = num.intValue();
+                if (n < 0) {
+                    return new SimpleNumber(-n);
+                } else {
+                    return model;
+                }
+            } else if (num instanceof BigDecimal) {
+                BigDecimal n = (BigDecimal) num;
+                if (n.signum() < 0) {
+                    return new SimpleNumber(n.negate());
+                } else {
+                    return model;
+                }
+            } else if (num instanceof Double) {
+                double n = num.doubleValue();
+                if (n < 0) {
+                    return new SimpleNumber(-n);
+                } else {
+                    return model;
+                }
+            } else if (num instanceof Float) {
+                float n = num.floatValue();
+                if (n < 0) {
+                    return new SimpleNumber(-n);
+                } else {
+                    return model;
+                }
+            } else if (num instanceof Long) {
+                long n = num.longValue();
+                if (n < 0) {
+                    return new SimpleNumber(-n);
+                } else {
+                    return model;
+                }
+            } else if (num instanceof Short) {
+                short n = num.shortValue();
+                if (n < 0) {
+                    return new SimpleNumber(-n);
+                } else {
+                    return model;
+                }
+            } else if (num instanceof Byte) {
+                byte n = num.byteValue();
+                if (n < 0) {
+                    return new SimpleNumber(-n);
+                } else {
+                    return model;
+                }
+            } else if (num instanceof BigInteger) {
+                BigInteger n = (BigInteger) num;
+                if (n.signum() < 0) {
+                    return new SimpleNumber(n.negate());
+                } else {
+                    return model;
+                }
+            } else {
+                throw new _TemplateModelException("Unsupported number class: ", num.getClass());
+            }            
+        }
+    }
+    
+    static class byteBI extends BuiltInForNumber {
+        @Override
+        TemplateModel calculateResult(Number num, TemplateModel model) {
+            if (num instanceof Byte) {
+                return model;
+            }
+            return new SimpleNumber(Byte.valueOf(num.byteValue()));
+        }
+    }
+
+    static class ceilingBI extends BuiltInForNumber {
+        @Override
+        TemplateModel calculateResult(Number num, TemplateModel model) {
+            return new SimpleNumber(new BigDecimal(num.doubleValue()).divide(BIG_DECIMAL_ONE, 0, BigDecimal.ROUND_CEILING));
+        }
+    }
+
+    static class doubleBI extends BuiltInForNumber {
+        @Override
+        TemplateModel calculateResult(Number num, TemplateModel model) {
+            if (num instanceof Double) {
+                return model;
+            }
+            return new SimpleNumber(num.doubleValue());
+        }
+    }
+
+    static class floatBI extends BuiltInForNumber {
+        @Override
+        TemplateModel calculateResult(Number num, TemplateModel model) {
+            if (num instanceof Float) {
+                return model;
+            }
+            return new SimpleNumber(num.floatValue());
+        }
+    }
+
+    static class floorBI extends BuiltInForNumber {
+        @Override
+        TemplateModel calculateResult(Number num, TemplateModel model) {
+            return new SimpleNumber(new BigDecimal(num.doubleValue()).divide(BIG_DECIMAL_ONE, 0, BigDecimal.ROUND_FLOOR));
+        }
+    }
+
+    static class intBI extends BuiltInForNumber {
+        @Override
+        TemplateModel calculateResult(Number num, TemplateModel model) {
+            if (num instanceof Integer) {
+                return model;
+            }
+            return new SimpleNumber(num.intValue());
+        }
+    }
+
+    static class is_infiniteBI extends BuiltInForNumber {
+        @Override
+        TemplateModel calculateResult(Number num, TemplateModel model) throws TemplateModelException {
+            return _NumberUtil.isInfinite(num) ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+        }
+    }
+
+
+    static class is_nanBI extends BuiltInForNumber {
+        @Override
+        TemplateModel calculateResult(Number num, TemplateModel model) throws TemplateModelException {
+            return _NumberUtil.isNaN(num) ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+        }
+    }
+
+    // Does both someNumber?long and someDate?long, thus it doesn't extend NumberBuiltIn
+    static class longBI extends ASTExpBuiltIn {
+        @Override
+        TemplateModel _eval(Environment env)
+                throws TemplateException {
+            TemplateModel model = target.eval(env);
+            if (!(model instanceof TemplateNumberModel)
+                    && model instanceof TemplateDateModel) {
+                Date date = _EvalUtil.modelToDate((TemplateDateModel) model, target);
+                return new SimpleNumber(date.getTime());
+            } else {
+                Number num = target.modelToNumber(model, env);
+                if (num instanceof Long) {
+                    return model;
+                }
+                return new SimpleNumber(num.longValue());
+            }
+        }
+    }
+
+    static class number_to_dateBI extends BuiltInForNumber {
+        
+        private final int dateType;
+        
+        number_to_dateBI(int dateType) {
+            this.dateType = dateType;
+        }
+        
+        @Override
+        TemplateModel calculateResult(Number num, TemplateModel model)
+        throws TemplateModelException {
+            return new SimpleDate(new Date(safeToLong(num)), dateType);
+        }
+    }
+
+    static class roundBI extends BuiltInForNumber {
+        private static final BigDecimal half = new BigDecimal("0.5");
+        @Override
+        TemplateModel calculateResult(Number num, TemplateModel model) {
+            return new SimpleNumber(new BigDecimal(num.doubleValue()).add(half).divide(BIG_DECIMAL_ONE, 0, BigDecimal.ROUND_FLOOR));
+        }
+    }
+    
+    static class shortBI extends BuiltInForNumber {
+        @Override
+        TemplateModel calculateResult(Number num, TemplateModel model) {
+            if (num instanceof Short) {
+                return model;
+            }
+            return new SimpleNumber(Short.valueOf(num.shortValue()));
+        }
+    }
+
+    private static long safeToLong(Number num) throws TemplateModelException {
+        if (num instanceof Double) {
+            double d = Math.round(num.doubleValue());
+            if (d > Long.MAX_VALUE || d < Long.MIN_VALUE) {
+                throw new _TemplateModelException(
+                        "Number doesn't fit into a 64 bit signed integer (long): ", Double.valueOf(d));
+            } else {
+                return (long) d;
+            }
+        } else if (num instanceof Float) {
+            float f = Math.round(num.floatValue());
+            if (f > Long.MAX_VALUE || f < Long.MIN_VALUE) {
+                throw new _TemplateModelException(
+                        "Number doesn't fit into a 64 bit signed integer (long): ", Float.valueOf(f));
+            } else {
+                return (long) f;
+            }
+        } else if (num instanceof BigDecimal) {
+            BigDecimal bd = ((BigDecimal) num).setScale(0, BigDecimal.ROUND_HALF_UP);
+            if (bd.compareTo(BIG_DECIMAL_LONG_MAX) > 0 || bd.compareTo(BIG_DECIMAL_LONG_MIN) < 0) {
+                throw new _TemplateModelException("Number doesn't fit into a 64 bit signed integer (long): ", bd);
+            } else {
+                return bd.longValue();
+            }
+        } else if (num instanceof BigInteger) {
+            BigInteger bi = (BigInteger) num;
+            if (bi.compareTo(BIG_INTEGER_LONG_MAX) > 0 || bi.compareTo(BIG_INTEGER_LONG_MIN) < 0) {
+                throw new _TemplateModelException("Number doesn't fit into a 64 bit signed integer (long): ", bi);
+            } else {
+                return bi.longValue();
+            }
+        } else if (num instanceof Long || num instanceof Integer || num instanceof Byte || num instanceof Short) {
+            return num.longValue();
+        } else {
+            // Should add Atomic* types in 2.4...
+            throw new _TemplateModelException("Unsupported number type: ", num.getClass());
+        }
+    }
+    
+    private static final BigDecimal BIG_DECIMAL_ONE = new BigDecimal("1");
+    private static final BigDecimal BIG_DECIMAL_LONG_MIN = BigDecimal.valueOf(Long.MIN_VALUE); 
+    private static final BigDecimal BIG_DECIMAL_LONG_MAX = BigDecimal.valueOf(Long.MAX_VALUE);
+    private static final BigInteger BIG_INTEGER_LONG_MIN = BigInteger.valueOf(Long.MIN_VALUE); 
+    
+    private static final BigInteger BIG_INTEGER_LONG_MAX = BigInteger.valueOf(Long.MAX_VALUE);
+    
+    // Can't be instantiated
+    private BuiltInsForNumbers() { }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForOutputFormatRelated.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForOutputFormatRelated.java b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForOutputFormatRelated.java
new file mode 100644
index 0000000..2ae2fe7
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForOutputFormatRelated.java
@@ -0,0 +1,84 @@
+/*
+ * 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.TemplateMarkupOutputModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.outputformat.MarkupOutputFormat;
+
+class BuiltInsForOutputFormatRelated {
+
+    static class no_escBI extends AbstractConverterBI {
+
+        @Override
+        protected TemplateModel calculateResult(String lho, MarkupOutputFormat outputFormat, Environment env)
+                throws TemplateException {
+            return outputFormat.fromMarkup(lho);
+        }
+        
+    }
+
+    static class escBI extends AbstractConverterBI {
+
+        @Override
+        protected TemplateModel calculateResult(String lho, MarkupOutputFormat outputFormat, Environment env)
+                throws TemplateException {
+            return outputFormat.fromPlainTextByEscaping(lho);
+        }
+        
+    }
+    
+    static abstract class AbstractConverterBI extends MarkupOutputFormatBoundBuiltIn {
+
+        @Override
+        protected TemplateModel calculateResult(Environment env) throws TemplateException {
+            TemplateModel lhoTM = target.eval(env);
+            Object lhoMOOrStr = _EvalUtil.coerceModelToStringOrMarkup(lhoTM, target, null, env);
+            MarkupOutputFormat contextOF = outputFormat;
+            if (lhoMOOrStr instanceof String) { // TemplateMarkupOutputModel
+                return calculateResult((String) lhoMOOrStr, contextOF, env);
+            } else {
+                TemplateMarkupOutputModel lhoMO = (TemplateMarkupOutputModel) lhoMOOrStr;
+                MarkupOutputFormat lhoOF = lhoMO.getOutputFormat();
+                // ATTENTION: Keep this logic in sync. with ${...}'s logic!
+                if (lhoOF == contextOF || contextOF.isOutputFormatMixingAllowed()) {
+                    // bypass
+                    return lhoMO;
+                } else {
+                    // ATTENTION: Keep this logic in sync. with ${...}'s logic!
+                    String lhoPlainTtext = lhoOF.getSourcePlainText(lhoMO);
+                    if (lhoPlainTtext == null) {
+                        throw new _TemplateModelException(target,
+                                "The left side operand of ?", key, " is in ", new _DelayedToString(lhoOF),
+                                " format, which differs from the current output format, ",
+                                new _DelayedToString(contextOF), ". Conversion wasn't possible.");
+                    }
+                    // Here we know that lho is escaped plain text. So we re-escape it to the current format and
+                    // bypass it, just as if the two output formats were the same earlier.
+                    return contextOF.fromPlainTextByEscaping(lhoPlainTtext);
+                }
+            }
+        }
+        
+        protected abstract TemplateModel calculateResult(String lho, MarkupOutputFormat outputFormat, Environment env)
+                throws TemplateException;
+        
+    }
+    
+}