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 2026/08/07 00:17:53 UTC

(freemarker) 01/01: [WIP] Added `?lines`

This is an automated email from the ASF dual-hosted git repository.

ddekany pushed a commit to branch feature/lines-bi
in repository https://gitbox.apache.org/repos/asf/freemarker.git

commit daf8ac9bab70fec33b382154ad67c69cbb0ddfd0
Author: ddekany <dd...@apache.org>
AuthorDate: Fri Aug 7 01:59:56 2026 +0200

    [WIP] Added `?lines`
---
 .../src/main/java/freemarker/core/BuiltIn.java     |   3 +-
 .../freemarker/core/BuiltInsForStringsBasic.java   |  35 +++++++
 .../java/freemarker/core/LinesBuiltInTest.java     | 105 +++++++++++++++++++++
 3 files changed, 142 insertions(+), 1 deletion(-)

diff --git a/freemarker-core/src/main/java/freemarker/core/BuiltIn.java b/freemarker-core/src/main/java/freemarker/core/BuiltIn.java
index bf0b8914..6194235e 100644
--- a/freemarker-core/src/main/java/freemarker/core/BuiltIn.java
+++ b/freemarker-core/src/main/java/freemarker/core/BuiltIn.java
@@ -86,7 +86,7 @@ abstract class BuiltIn extends Expression implements Cloneable {
 
     static final Set<String> CAMEL_CASE_NAMES = new TreeSet<>();
     static final Set<String> SNAKE_CASE_NAMES = new TreeSet<>();
-    static final int NUMBER_OF_BIS = 307;
+    static final int NUMBER_OF_BIS = 308;
     static final HashMap<String, BuiltIn> BUILT_INS_BY_NAME = new HashMap<>(NUMBER_OF_BIS * 3 / 2 + 1, 1f);
 
     static final String BI_NAME_SNAKE_CASE_WITH_ARGS = "with_args";
@@ -250,6 +250,7 @@ abstract class BuiltIn extends Expression implements Cloneable {
         putBI("last", new lastBI());
         putBI("left_pad", "leftPad", new BuiltInsForStringsBasic.padBI(true));
         putBI("length", new BuiltInsForStringsBasic.lengthBI());
+        putBI("lines", new BuiltInsForStringsBasic.linesBI());
         putBI("long", new longBI());
         putBI("lower_abc", "lowerAbc", new BuiltInsForNumbers.lower_abcBI());
         putBI("lower_case", "lowerCase", new BuiltInsForStringsBasic.lower_caseBI());
diff --git a/freemarker-core/src/main/java/freemarker/core/BuiltInsForStringsBasic.java b/freemarker-core/src/main/java/freemarker/core/BuiltInsForStringsBasic.java
index 40419986..1b499865 100644
--- a/freemarker-core/src/main/java/freemarker/core/BuiltInsForStringsBasic.java
+++ b/freemarker-core/src/main/java/freemarker/core/BuiltInsForStringsBasic.java
@@ -682,7 +682,42 @@ class BuiltInsForStringsBasic {
         TemplateModel calculateResult(String s, Environment env) throws TemplateModelException {
             return new SplitMethod(s);
         }
+    }
+
+    static class linesBI extends BuiltInForString {
 
+        @Override
+        TemplateModel calculateResult(String s, Environment env) throws TemplateException {
+            SimpleSequence result = new SimpleSequence(_ObjectWrappers.SAFE_OBJECT_WRAPPER);
+            int lineStartPos = 0;
+            int pendingEmptyLines = 0;
+            for (int pos = 0; pos < s.length(); pos++) {
+                char c = s.charAt(pos);
+                if (c == '\r' || c == '\n') {
+                    if (pos == lineStartPos) {
+                        pendingEmptyLines++;
+                    } else {
+                        while (pendingEmptyLines > 0) {
+                            result.add(TemplateScalarModel.EMPTY_STRING);
+                            pendingEmptyLines--;
+                        }
+                        result.add(new SimpleScalar(s.substring(lineStartPos, pos)));
+                    }
+                    if (c == '\r' && pos + 1 < s.length() && s.charAt(pos + 1) == '\n') {
+                        pos++;
+                    }
+                    lineStartPos = pos + 1;
+                }
+            }
+            if (lineStartPos < s.length()) {
+                while (pendingEmptyLines > 0) {
+                    result.add(TemplateScalarModel.EMPTY_STRING);
+                    pendingEmptyLines--;
+                }
+                result.add(new SimpleScalar(s.substring(lineStartPos)));
+            }
+            return result;
+        }
     }
 
     static class starts_withBI extends BuiltInForString {
diff --git a/freemarker-core/src/test/java/freemarker/core/LinesBuiltInTest.java b/freemarker-core/src/test/java/freemarker/core/LinesBuiltInTest.java
new file mode 100644
index 00000000..ad130534
--- /dev/null
+++ b/freemarker-core/src/test/java/freemarker/core/LinesBuiltInTest.java
@@ -0,0 +1,105 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package freemarker.core;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import freemarker.template.Configuration;
+import freemarker.test.TemplateTest;
+
+/**
+ * Checks indent/dedend/wrap built-ns; for the thorough testing of the text transformations see the
+ * {@link _CoreStringUtilsTest}!
+ */
+@RunWith(Parameterized.class)
+public class LinesBuiltInTest extends TemplateTest {
+
+    private final String lineBreak;
+
+    @Parameterized.Parameters(name = "{0}")
+    public static Collection<Object[]> parameters() {
+        return List.of(
+                new Object[]{"LF", "\n"},
+                new Object[]{"CRLF", "\r\n"},
+                new Object[]{"CR", "\r"});
+    }
+
+    public LinesBuiltInTest(String lineBreakName, String lineBreak) {
+        this.lineBreak = lineBreak;
+    }
+
+
+    @Override
+    protected Configuration createConfiguration() throws Exception {
+        return new Configuration(Configuration.VERSION_2_3_35);
+    }
+
+    private String lb(String s) {
+        return s.replace("\n", lineBreak);
+    }
+
+    @Test
+    public void testLinesBiBasics() throws Exception {
+        assertExpOutput("''?lines?join('|')", "");
+        assertExpOutput("'line1'?lines?join('|')", "line1");
+
+        addToDataModel("s", lb("line1\nline2"));
+        assertExpOutput("s?lines?join('|')", "line1|line2");
+
+        addToDataModel("s", lb("line1\nline2\nline3"));
+        assertExpOutput("s?lines?join('|')", "line1|line2|line3");
+    }
+
+    @Test
+    public void testLinesBiTrailingEmptyLinesRemoved() throws Exception {
+        assertExpOutput("'\n'?lines?join('|')", "");
+        assertExpOutput("'\n\n'?lines?join('|')", "");
+
+        for (String s : List.of("line1\n", "line1\n\n", "line1\n\n\n")) {
+            addToDataModel("s", lb(s));
+            assertExpOutput("s?lines?join('|')", "line1");
+        }
+        for (String s : List.of("line1\nline2\n", "line1\nline2\n\n", "line1\nline2\n\n\n")) {
+            addToDataModel("s", lb(s));
+            assertExpOutput("s?lines?join('|')", "line1|line2");
+        }
+    }
+
+    @Test
+    public void testLinesBiNonTrailingEmptyLinesKept() throws Exception {
+        addToDataModel("s", lb("\nline1\nline2"));
+        assertExpOutput("s?lines?join('|')", "|line1|line2");
+
+        addToDataModel("s", lb("\n\nline1"));
+        assertExpOutput("s?lines?join('|')", "||line1");
+
+        addToDataModel("s", lb("\n\n\nline1\n\n"));
+        assertExpOutput("s?lines?join('|')", "|||line1");
+
+        addToDataModel("s", lb("line1\n\nline2\n\n\nline3"));
+        assertExpOutput("s?lines?join('|')", "line1||line2|||line3");
+    }
+
+}