From 8a70973557ff2605dada63f0dd2c08aa1cfcceb2 Mon Sep 17 00:00:00 2001 From: "Piotr Wilkin (ilintar)" Date: Thu, 19 Feb 2026 00:25:52 +0100 Subject: [PATCH] Add Jinja support for "indent" string filter (#19529) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add partial Jinja support for "indent" string filter * Fully implement indent * Add tests for all width variants. * Update tests/test-jinja.cpp Co-authored-by: Sigbjørn Skjæret * Fix getline ignoring trailing newlines * Update common/jinja/value.cpp Co-authored-by: Sigbjørn Skjæret * fix first indent condition --------- Co-authored-by: Sigbjørn Skjæret --- common/jinja/value.cpp | 43 ++++++++++++++++++++++++++++++++++++++++-- tests/test-jinja.cpp | 42 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 2 deletions(-) diff --git a/common/jinja/value.cpp b/common/jinja/value.cpp index 2aa156b177..9987836d18 100644 --- a/common/jinja/value.cpp +++ b/common/jinja/value.cpp @@ -4,6 +4,7 @@ // for converting from JSON to jinja values #include +#include #include #include #include @@ -715,8 +716,46 @@ const func_builtins & value_string_t::get_builtins() const { return args.get_pos(0); }}, {"tojson", tojson}, - {"indent", [](const func_args &) -> value { - throw not_implemented_exception("String indent builtin not implemented"); + {"indent", [](const func_args &args) -> value { + args.ensure_count(1, 4); + value val_input = args.get_pos(0); + value val_width = args.get_kwarg_or_pos("width", 1); + const bool first = args.get_kwarg_or_pos("first", 2)->as_bool(); // undefined == false + const bool blank = args.get_kwarg_or_pos("blank", 3)->as_bool(); // undefined == false + if (!is_val(val_input)) { + throw raised_exception("indent() first argument must be a string"); + } + std::string indent; + if (is_val(val_width)) { + indent.assign(val_width->as_int(), ' '); + } else if (is_val(val_width)) { + indent = val_width->as_string().str(); + } else { + indent = " "; + } + std::string indented; + std::string input = val_input->as_string().str(); + std::istringstream iss = std::istringstream(input); + std::string line; + while (std::getline(iss, line)) { + if (!indented.empty()) { + indented.push_back('\n'); + } + if ((indented.empty() ? first : (!line.empty() || blank))) { + indented += indent; + } + indented += line; + } + if (!input.empty() && input.back() == '\n') { + indented.push_back('\n'); + if (blank) { + indented += indent; + } + } + + auto res = mk_val(indented); + res->val_str.mark_input_based_on(val_input->as_string()); + return res; }}, {"join", [](const func_args &) -> value { throw not_implemented_exception("String join builtin not implemented"); diff --git a/tests/test-jinja.cpp b/tests/test-jinja.cpp index 1f25c6ae71..f5197bd33f 100644 --- a/tests/test-jinja.cpp +++ b/tests/test-jinja.cpp @@ -691,6 +691,48 @@ static void test_filters(testing & t) { "{\n \"a\": 1,\n \"b\": [\n 1,\n 2\n ]\n}" ); + test_template(t, "indent", + "{{ data|indent(2) }}", + {{ "data", "foo\nbar" }}, + "foo\n bar" + ); + + test_template(t, "indent first only", + "{{ data|indent(width=3,first=true) }}", + {{ "data", "foo\nbar" }}, + " foo\n bar" + ); + + test_template(t, "indent blank lines and first line", + "{{ data|indent(width=5,blank=true,first=true) }}", + {{ "data", "foo\n\nbar" }}, + " foo\n \n bar" + ); + + test_template(t, "indent with default width", + "{{ data|indent() }}", + {{ "data", "foo\nbar" }}, + "foo\n bar" + ); + + test_template(t, "indent with no newline", + "{{ data|indent }}", + {{ "data", "foo" }}, + "foo" + ); + + test_template(t, "indent with trailing newline", + "{{ data|indent(blank=true) }}", + {{ "data", "foo\n" }}, + "foo\n " + ); + + test_template(t, "indent with string", + "{{ data|indent(width='>>>>') }}", + {{ "data", "foo\nbar" }}, + "foo\n>>>>bar" + ); + test_template(t, "chained filters", "{{ ' HELLO '|trim|lower }}", json::object(),