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");
+ }
+
+}