Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion freemarker-core/src/main/java/freemarker/core/BuiltIn.java
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,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 = 302;
static final int NUMBER_OF_BIS = 307;
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";
Expand Down Expand Up @@ -115,6 +115,7 @@ abstract class BuiltIn extends Expression implements Cloneable {
putBI("date_if_unknown", "dateIfUnknown", new BuiltInsForDates.dateType_if_unknownBI(TemplateDateModel.DATE));
putBI("datetime", new BuiltInsForMultipleTypes.dateBI(TemplateDateModel.DATETIME));
putBI("datetime_if_unknown", "datetimeIfUnknown", new BuiltInsForDates.dateType_if_unknownBI(TemplateDateModel.DATETIME));
putBI("dedent", new BuiltInsForStringsBasic.dedentBI());
putBI("default", new BuiltInsForExistenceHandling.defaultBI());
putBI("double", new doubleBI());
putBI("drop_while", "dropWhile", new BuiltInsForSequences.drop_whileBI());
Expand All @@ -138,6 +139,7 @@ abstract class BuiltIn extends Expression implements Cloneable {
putBI("has_next", "hasNext", new BuiltInsForLoopVariables.has_nextBI());
putBI("html", new BuiltInsForStringsEncoding.htmlBI());
putBI("if_exists", "ifExists", new BuiltInsForExistenceHandling.if_existsBI());
putBI("indent", new BuiltInsForStringsBasic.indentBI());
putBI("index", new BuiltInsForLoopVariables.indexBI());
putBI("index_of", "indexOf", new BuiltInsForStringsBasic.index_ofBI(false));
putBI("int", new intBI());
Expand Down Expand Up @@ -272,6 +274,7 @@ abstract class BuiltIn extends Expression implements Cloneable {
putBI("item_parity_cap", "itemParityCap", new BuiltInsForLoopVariables.item_parity_capBI());
putBI("reverse", new reverseBI());
putBI("right_pad", "rightPad", new BuiltInsForStringsBasic.padBI(false));
putBI("right_pad_lines", "rightPadLines", new BuiltInsForStringsBasic.right_pad_linesBI());
putBI("root", new rootBI());
putBI("round", new roundBI());
putBI("remove_ending", "removeEnding", new BuiltInsForStringsBasic.remove_endingBI());
Expand Down Expand Up @@ -315,6 +318,7 @@ abstract class BuiltIn extends Expression implements Cloneable {
putBI(BI_NAME_SNAKE_CASE_WITH_ARGS_LAST, BI_NAME_CAMEL_CASE_WITH_ARGS_LAST,
new BuiltInsForCallables.with_args_lastBI());
putBI("word_list", "wordList", new BuiltInsForStringsBasic.word_listBI());
putBI("wrap", new BuiltInsForStringsBasic.wrapBI());
putBI("xhtml", new BuiltInsForStringsEncoding.xhtmlBI());
putBI("xml", new BuiltInsForStringsEncoding.xmlBI());
putBI("matches", new BuiltInsForStringsRegexp.matchesBI());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -495,7 +495,353 @@ TemplateModel calculateResult(String s, Environment env) throws TemplateExceptio
return new BIMethod(s);
}
}


static class indentBI extends BuiltInForString {

private class BIMethod implements TemplateMethodModelEx {

private final String s;

private BIMethod(String s) {
this.s = s;
}

@Override
public Object exec(List args) throws TemplateModelException {
int argCnt = args.size();
checkMethodArgCount(argCnt, 1, 1);

String prefix = getStringMethodArg(args, 0);

if (s.isEmpty()) {
return new SimpleScalar(s);
}

StringBuilder sb = new StringBuilder(s.length() + prefix.length() * 10);
int len = s.length();
boolean atLineStart = true;
for (int i = 0; i < len; i++) {
char c = s.charAt(i);
if (atLineStart && c != '\n' && c != '\r') {
sb.append(prefix);
}
sb.append(c);
atLineStart = (c == '\n' || (c == '\r' && (i + 1 >= len || s.charAt(i + 1) != '\n')));
}
return new SimpleScalar(sb.toString());
}
}

@Override
TemplateModel calculateResult(String s, Environment env) throws TemplateException {
return new BIMethod(s);
}
}

static class dedentBI extends BuiltInForString {

private class BIMethod implements TemplateMethodModelEx {

private final String s;

private BIMethod(String s) {
this.s = s;
}

@Override
public Object exec(List args) throws TemplateModelException {
int argCnt = args.size();
checkMethodArgCount(argCnt, 0, 1);

if (argCnt == 0) {
// No-argument form: strip the longest common leading whitespace
// (spaces and tabs) across all non-empty lines, like Python's
// textwrap.dedent. Empty lines are ignored when computing the
// common prefix.
return new SimpleScalar(dedentCommonLeadingWhitespace(s));
}

// Explicit-prefix form: remove the given prefix from each line that
// starts with it; leave other lines unchanged.
String prefix = getStringMethodArg(args, 0);

if (s.isEmpty() || prefix.isEmpty()) {
return new SimpleScalar(s);
}

int prefixLen = prefix.length();
StringBuilder sb = new StringBuilder(s.length());
int len = s.length();
boolean atLineStart = true;
int matchPos = 0;
boolean stripping = true;

for (int i = 0; i < len; i++) {
char c = s.charAt(i);
if (atLineStart && stripping) {
if (matchPos < prefixLen && c == prefix.charAt(matchPos)) {
matchPos++;
if (matchPos == prefixLen) {
stripping = false;
}
continue; // consume prefix char
} else {
// Prefix didn't match — emit what we skipped
sb.append(prefix, 0, matchPos);
stripping = false;
}
}
sb.append(c);
if (c == '\n') {
atLineStart = true;
matchPos = 0;
stripping = true;
} else if (c == '\r') {
atLineStart = true;
matchPos = 0;
stripping = true;
} else {
atLineStart = false;
}
}
// Handle trailing partial match (line without newline)
if (stripping && matchPos > 0 && matchPos < prefixLen) {
sb.append(prefix, 0, matchPos);
}
return new SimpleScalar(sb.toString());
}
}

/**
* Strip the longest leading-whitespace string (spaces and tabs only) that
* is a common prefix of every non-empty line. Empty lines are ignored when
* computing the prefix but remain empty in the output. Mirrors Python's
* textwrap.dedent semantics. Note: a leading tab and a leading space do
* not collapse — they're distinct characters with no common prefix.
*/
private static String dedentCommonLeadingWhitespace(String s) {
if (s.isEmpty()) return s;
int len = s.length();

// First pass: walk lines, find the leading-whitespace run of each,
// and compute the common prefix among non-empty lines.
String commonPrefix = null;
int lineStart = 0;
for (int i = 0; i <= len; i++) {
boolean atEnd = (i == len);
char c = atEnd ? '\n' : s.charAt(i);
if (atEnd || c == '\n' || c == '\r') {
int contentStart = lineStart;
while (contentStart < i) {
char cc = s.charAt(contentStart);
if (cc != ' ' && cc != '\t') break;
contentStart++;
}
boolean nonEmpty = contentStart < i;
if (nonEmpty) {
if (commonPrefix == null) {
commonPrefix = s.substring(lineStart, contentStart);
} else {
int maxLen = Math.min(commonPrefix.length(), contentStart - lineStart);
int matched = 0;
while (matched < maxLen
&& commonPrefix.charAt(matched) == s.charAt(lineStart + matched)) {
matched++;
}
if (matched < commonPrefix.length()) {
commonPrefix = commonPrefix.substring(0, matched);
}
if (commonPrefix.isEmpty()) break; // can't shrink further; finish quickly
}
}
if (!atEnd) {
// Step past \r\n if applicable
if (c == '\r' && i + 1 < len && s.charAt(i + 1) == '\n') i++;
lineStart = i + 1;
}
}
}

if (commonPrefix == null || commonPrefix.isEmpty()) {
return s;
}

// Second pass: emit each line with the common prefix stripped (from
// non-empty lines only).
int prefixLen = commonPrefix.length();
StringBuilder sb = new StringBuilder(len);
lineStart = 0;
for (int i = 0; i <= len; i++) {
boolean atEnd = (i == len);
if (atEnd || s.charAt(i) == '\n' || s.charAt(i) == '\r') {
int contentStart = lineStart;
while (contentStart < i) {
char cc = s.charAt(contentStart);
if (cc != ' ' && cc != '\t') break;
contentStart++;
}
boolean nonEmpty = contentStart < i;
if (nonEmpty) {
// Non-empty line: by construction it has the common prefix.
sb.append(s, lineStart + prefixLen, i);
} else {
// Whitespace-only or empty line — keep as is.
sb.append(s, lineStart, i);
}
if (!atEnd) {
sb.append(s.charAt(i));
if (s.charAt(i) == '\r' && i + 1 < len && s.charAt(i + 1) == '\n') {
i++;
sb.append('\n');
}
lineStart = i + 1;
}
}
}
return sb.toString();
}

@Override
TemplateModel calculateResult(String s, Environment env) throws TemplateException {
return new BIMethod(s);
}
}

static class wrapBI extends BuiltInForString {

private class BIMethod implements TemplateMethodModelEx {

private final String s;

private BIMethod(String s) {
this.s = s;
}

@Override
public Object exec(List args) throws TemplateModelException {
int argCnt = args.size();
checkMethodArgCount(argCnt, 2, 3);

int width = getNumberMethodArg(args, 0).intValue();
if (width < 1) {
throw new _TemplateModelException(
"?", key, "(...) argument #1 (width) must be at least 1.");
}

String firstPrefix = getStringMethodArg(args, 1);
String restPrefix = argCnt > 2 ? getStringMethodArg(args, 2) : firstPrefix;

String[] words = s.split("\\s+");
if (words.length == 0 || (words.length == 1 && words[0].isEmpty())) {
return new SimpleScalar(firstPrefix + "\n");
}

StringBuilder sb = new StringBuilder();
String currentPrefix = firstPrefix;
int lineLen = currentPrefix.length();
sb.append(currentPrefix);
boolean firstWord = true;

for (String word : words) {
if (word.isEmpty()) continue;
if (firstWord) {
sb.append(word);
lineLen += word.length();
firstWord = false;
} else {
if (lineLen + 1 + word.length() > width) {
sb.append('\n');
currentPrefix = restPrefix;
sb.append(currentPrefix);
sb.append(word);
lineLen = currentPrefix.length() + word.length();
} else {
sb.append(' ');
sb.append(word);
lineLen += 1 + word.length();
}
}
}
sb.append('\n');
return new SimpleScalar(sb.toString());
}
}

@Override
TemplateModel calculateResult(String s, Environment env) throws TemplateException {
return new BIMethod(s);
}
}

static class right_pad_linesBI extends BuiltInForString {

private class BIMethod implements TemplateMethodModelEx {

private final String s;

private BIMethod(String s) {
this.s = s;
}

@Override
public Object exec(List args) throws TemplateModelException {
int argCnt = args.size();
checkMethodArgCount(argCnt, 1, 2);

int column = getNumberMethodArg(args, 0).intValue();
if (column < 0) {
throw new _TemplateModelException(
"?", key, "(...) argument #1 must be non-negative.");
}

char fillChar = ' ';
if (argCnt > 1) {
String filling = getStringMethodArg(args, 1);
if (filling.length() != 1) {
throw new _TemplateModelException(
"?", key, "(...) argument #2 must be a single character string.");
}
fillChar = filling.charAt(0);
}

if (s.isEmpty()) {
return new SimpleScalar(s);
}

StringBuilder sb = new StringBuilder(s.length() + column);
int lineStart = 0;
int len = s.length();
for (int i = 0; i <= len; i++) {
if (i == len || s.charAt(i) == '\n' || s.charAt(i) == '\r') {
int lineLen = i - lineStart;
sb.append(s, lineStart, i);
// Pad to column (skip empty lines)
if (lineLen > 0) {
for (int p = lineLen; p < column; p++) {
sb.append(fillChar);
}
}
// Append the line ending
if (i < len) {
sb.append(s.charAt(i));
if (s.charAt(i) == '\r' && i + 1 < len && s.charAt(i + 1) == '\n') {
i++;
sb.append('\n');
}
}
lineStart = i + 1;
}
}
return new SimpleScalar(sb.toString());
}
}

@Override
TemplateModel calculateResult(String s, Environment env) throws TemplateException {
return new BIMethod(s);
}
}

static class remove_beginningBI extends BuiltInForString {

private class BIMethod implements TemplateMethodModelEx {
Expand Down
Loading