Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Preserving whitespace prefix in multiline strings #178

Open
ttsiodras opened this issue Feb 16, 2013 · 23 comments · May be fixed by #1456
Open

Preserving whitespace prefix in multiline strings #178

ttsiodras opened this issue Feb 16, 2013 · 23 comments · May be fixed by #1456

Comments

@ttsiodras
Copy link

In the StringTemplate engine - which I've used in some projects to emit C code - whitespace prefixes are automatically added in the output lines:

PrintCFunction(linesGlobal, linesLocal) ::= <<
void foo() {
    if (someRuntimeFlag) {
        <linesGlobal>
        if (anotherRuntimeFlag) {
            <linesLocal>
        }
    }
}
>>

When this template is rendered in StringTemplate, the whitespace prefixing the multilined linesGlobal and linesLocal strings, is copied for all the lines emitted. The generated C code is e.g.:

void foo() {
    if (someRuntimeFlag) {
        int i;
        i=1;   // <=== whitespace prefix copied in 2nd
        i++;   // <=== and 3rd line
        if (anotherRuntimeFlag) {
            int j=i;
            j++; //  <=== ditto
        }
    }
}

I am new to Jinja2 - and tried to replicate this:

#!/usr/bin/env python
from jinja2 import Template

linesGlobal='\n'.join(['int i;', 'i=1;'])
linesLocal='\n'.join(['int j=i;', 'j++;'])

tmpl = Template(u'''\
void foo() {
    if (someRuntimeFlag) {
        {{linesGlobal}}
        if (anotherRuntimeFlag) {
            {{linesLocal}}
        }
    }
}
''')

print tmpl.render(
    linesGlobal=linesGlobal,
    linesLocal=linesLocal)

...but saw it produce this:

void foo() {
    if (someRuntimeFlag) {
        int i;
i=1;
        if (anotherRuntimeFlag) {
            int j=i;
j++;
        }
    }
}

...which is not what I want. I managed to make the output emit proper whitespace prefixes with this:

...
if (someRuntimeFlag) {
    {{linesGlobal|indent(8)}}
    if (anotherRuntimeFlag) {
        {{linesLocal|indent(12)}}
    }
}

...but this is arguably bad, since I need to manually count whitespace for every string I emit...

Is there a better way that I am missing?

@jgehrcke
Copy link

jgehrcke commented Mar 4, 2013

I am interested in basically the same thing. The issue has also come up at stackoverflow: http://stackoverflow.com/questions/10821539/jinja-keep-indentation-on-include-or-macro

+1

@maxime-esa
Copy link

Also true when using the form:

    {{linesGlobal|join('\n')}}

This form does not behave as one would expect - since jinja2 is the one emmitting the newlines, it should make sure that they remain aligned with the last indentation level.

@fmarczin
Copy link

fmarczin commented Nov 7, 2014

This would be very nice to have! It would lead to much nicer templates and rendered output at the same time.

Why not create another whitespace option similar to {%+ and {%- that prepends the current indentation on whatever it evaluates to? Could be {%= or {%|

+1

@mpaolini
Copy link

mpaolini commented Dec 3, 2014

+1

needed here for templating API blueprint documentation:

{% macro entity_one() -%}
{
    "one": 1,
    "two": 2
}
{% endmacro -%}

+ Response 200 (application/json):

        {
            "entity": [
                {{ entity_one() }}
            ]
        }

is rendered now :

+ Response 200 (application/json):

        {
            "entity": [
                {
    "one": 1,
    "two": 2

}

            ]
        }

@inducer
Copy link

inducer commented Dec 21, 2014

When emitting YAML or Python, this becomes pretty crucial.

@DataGreed
Copy link

Ran into the same problem.

Is there a workaround for now other than defining a macro for every included tempkate and manually entering the indentation?

@kaikuchn
Copy link

Sorry for reviving this old issue, I just came across the same problem and googling brought me here. After some more looking around I found that by now there is a nice way to achieve this through the indent filter

@Cigizmoond-Vyhuholev
Copy link

@kaikuchn Thank you, dude! It works.

@ttsiodras
Copy link
Author

ttsiodras commented Jan 26, 2018

@kaikuchn , @Cigizmoond-Vyhuholev Guys, I am not sure I follow... as you can see in my original report at the top, I do mention a workaround with the indent filter - but also clearly state that it doesn't address the issue in the simple and powerful way that e.g. StringTemplate does, because it forces you to count indentation spaces every time you need to emit a block of lines... If you have to do that and you are generating code of any form (C, Python, whatever) you'll very quickly just abandon the process altogether...

Then again, I may have misunderstood what you meant... Can you share exactly how you'd implement my original requirement shown at the top? i.e. generate the same kind of output with Jinja2 syntax? This is what I don't like...

if (someRuntimeFlag) {
    {{linesGlobal|indent(8)}}
    if (anotherRuntimeFlag) {
        {{linesLocal|indent(12)}}
    }
}

...because I need to count the "8" and "12" in each and every template where I emit code. In comparison, in StringTemplate...

PrintCFunction(linesGlobal, linesLocal) ::= <<
void foo() {
    if (someRuntimeFlag) {
        <linesGlobal>
        if (anotherRuntimeFlag) {
            <linesLocal>
        }
    }
}
>>

tomas-mazak added a commit to tomas-mazak/jinja that referenced this issue Oct 31, 2018
Fixes pallets#178

Blocks now support a new syntax '{%* ... %}' that alings the indentation of
multiline string with the block statement itself. This is especially
useful with YAML or other languages where indentation matter. Example:

labels.j2:
```
tla: webtool
env: {{ env }}
```

deployment.yaml.j2:
```
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  labels:
    {% include 'labels.j2' %}
  name: webtool
spec:
  selector:
    matchLabels:
      {% include 'labels.j2' %}
  strategy:
    type: Recreate
```
...renders to broken YAML:
```
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  labels:
    tla: webtool
env: qa
  name: webtool
spec:
  selector:
    matchLabels:
      tla: webtool
env: qa
  strategy:
    type: Recreate
```

deployment_new_syntax.yaml.j2:
```
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  labels:
    {%* include 'labels.j2' %}
  name: webtool
spec:
  selector:
    matchLabels:
      {%* include 'labels.j2' %}
  strategy:
    type: Recreate
```
...renders correctly:
```
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  labels:
    tla: webtool
    env: qa
  name: webtool
spec:
  selector:
    matchLabels:
      tla: webtool
      env: qa
  strategy:
    type: Recreate
```
tomas-mazak added a commit to tomas-mazak/jinja that referenced this issue Jun 28, 2019
Fixes pallets#178

Blocks now support a new syntax '{%* ... %}' that alings the indentation of
multiline string with the block statement itself. This is especially
useful with YAML or other languages where indentation matter. Example:

labels.j2:
```
tla: webtool
env: {{ env }}
```

deployment.yaml.j2:
```
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  labels:
    {% include 'labels.j2' %}
  name: webtool
spec:
  selector:
    matchLabels:
      {% include 'labels.j2' %}
  strategy:
    type: Recreate
```
...renders to broken YAML:
```
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  labels:
    tla: webtool
env: qa
  name: webtool
spec:
  selector:
    matchLabels:
      tla: webtool
env: qa
  strategy:
    type: Recreate
```

deployment_new_syntax.yaml.j2:
```
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  labels:
    {%* include 'labels.j2' %}
  name: webtool
spec:
  selector:
    matchLabels:
      {%* include 'labels.j2' %}
  strategy:
    type: Recreate
```
...renders correctly:
```
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  labels:
    tla: webtool
    env: qa
  name: webtool
spec:
  selector:
    matchLabels:
      tla: webtool
      env: qa
  strategy:
    type: Recreate
```
@septatrix
Copy link
Contributor

I noticed that #919 was closed due to a code change which would apparently require some major refactoring of the PR. However I would really like to see this feature implemented in my favourite templating engine.

If this is still something the core devs would like to see implemented (the community sure wants it as it is the PR and open issue with the most thumbs up) I would be eager to help and maybe even try to implement this on my own.

@cheshirekow
Copy link

Would also love to see this feature. I'll note that the indent workaround can't be used when the indentation level isn't known. e.g.:

{{field.type}} {{field.name}}; {{field.comment|indent(??)}}

Where the indentation level to be preserved depends on the length of the first two value.

@yajo
Copy link

yajo commented Sep 1, 2020

I have one hypothesis. What about left-trimming 1st level of indentation in block declaration?

Example:

{%- macro some_yaml_block(sub_block) ~%}
  label indented with how many spaces: 2
  amount of spaces that will be trimmed due to that "~" char above: 2
  {%- if sub_block ~%}
    "yay! we can indent ifs!": true
    the minimal indent in this if block is 2, so:
      this extra-indented value is still indented properly
  {%- endif %}
{%- endmacro -%}

now we invoke that here:
  {{ some_yaml_block(true) }}

The rendering would be:

now we invoke that here:
  label indented with how many spaces: 2
  amount of spaces that will be trimmed due to that "~" char above: 2
  "yay! we can indent ifs!": true
  the minimal indent in this if block is 2, so:
    this extra-indented value is still indented properly

Basically, when finishing a block with ~%}, Jinja would:

  1. Remove the 1st characters if it is a EOL sequence, as indicated in the newline_sequence Environment parameter.
  2. Render the content internally.
  3. Count how many common whitespace characters prefix those rendered lines.
  4. Strip them from the beginning of each line.

If you, later, need to include this block with some specific indentation, all you need is to call it like some_yaml_block|indent. Since the indent was normalized at block declaration, you can later specify it without problem. And the block would behave consistently across calls.

@GergelyKalmar GergelyKalmar linked a pull request May 30, 2021 that will close this issue
6 tasks
@stereobutter
Copy link

This feature would be super useful for folks using jinja to template config files which nowadays almost always involves yaml somewhere. Why not use yaml-specific tooling you ask? A lot of tools aren't using just yaml but a mix of different config formats. For instance butane the config format for Fedora CoreOS uses yaml itself but needs to play with other tools like systemd that use other file formats for their configuration. Using jinja to template this out and assemble the file using include would be a nice workflow were it not for the indentation issues with multiline include blocks.

@stereobutter
Copy link

For anyone interested I've written an extension that works around this long standing issue so that I may use jinja2 for templating yaml config files. It works by hooking intopreprocess to transform something like {% include "template.j2" indent content %} into a regular include statement with an appropriate {% filter indent(...) %} block that indents the included content accordingly. Somewhat of a gross hack? Maybe?! Does it work? Yup.

@milahu
Copy link

milahu commented Jun 14, 2022

What about left-trimming 1st level of indentation in block declaration?

+1

should be optional, similar to the trim_blocks and lstrip_blocks options
the new option could be called dedent_blocks or unindent_blocks

probably this should throw error on mixed indent (tabs vs spaces), similar to python

related issue on stackoverflow: Jinja2 correctly indent included block

You can apply filters to whole text blocks: Template Designer Documentation / Filters

{% filter indent(width=4) %}
{% include "./sub-template.yml.j2" %}
{% endfilter %}

@rightaway
Copy link

@davidism Do you have any comment on #1456 which helps with this? This issue is 10 years old and #919 was closed. Can #178 (comment) be included as the fix?

@stereobutter
Copy link

stereobutter commented Jul 4, 2022

@rightaway #178 (comment) would only work for include blocks though. Getting this to work for extends would also be useful and I‘ve tinkered a bit with this already but it isn’t pretty. This is to say that fixing the underlying problem of jinja2 not dealing with indentation correctly would be infinitely better.

@GergelyKalmar
Copy link

I think that the solution that I've started working on some time ago in #1456 would be an elegant way of solving this issue, essentially only requiring adding the indent_blocks environment parameter to get automatic indenting/dedenting while rendering. I managed to make an implementation that worked for the majority of cases, however, there were still some more complex nested cases where it didn't work yet.

I only had a weekend to work on it though and I'm not very familiar with the inner workings of everything, nonetheless, if someone could finish that work or provide some input on the PR properly that would be awesome.

@sunhaitao
Copy link

I believe I found an elegant way to work around this: by calling template.generate creatively.

The details can be found in this demo:

import jinja2

def j2_gen(template, context, key_for_extras="__EXTRAS"):
    """Call `template.generate` with extras."""
    extras = context.get(key_for_extras, {})
    extras.update({
        "current_column": 0
    })
    newline_sequence = template.environment.newline_sequence

    for chunk in template.generate(
        {**context, key_for_extras: extras}
    ):
        yield chunk

        index = chunk.rfind(newline_sequence)
        if index == -1:
            extras["current_column"] += len(chunk)
        else:
            extras["current_column"] = (
                len(chunk) - len(newline_sequence) - index
            )

t = jinja2.Template(
"""\
{% macro render_lines() -%}
ABC
  XYZ
{%- endmacro -%}
{{" " * indent}}{{render_lines() | indent(__EXTRAS.current_column)}}
"""
)

for i in range(3):
    print("---+" * 4)
    print("".join(list(j2_gen(t, {"indent": 4 * i}))))

It prints the following text:

---+---+---+---+
ABC
  XYZ
---+---+---+---+
    ABC
      XYZ
---+---+---+---+
        ABC
          XYZ

I think whether to introduce a similar mechanism to Jinja is open to discuss, but adding some tests to ensure this always works seems to be a good idea.

@milahu
Copy link

milahu commented Oct 7, 2022

btw, this works in nickel

Multiline strings are "indentation-aware". This means that one could use an indented string interpolation and the indentation would behave as expected:

let log = m%"
if log:
  print("log:", s)
"%m in m%"
def concat(str_array, log=false):
  res = []
  for s in str_array:
    %{log}
    res.append(s)
  return res
"%m
def concat(str_array, log=false):
  res = []
  for s in str_array:
    if log:
      print("log:", s)
    res.append(s)
  return res

@mardukbp
Copy link

I was in a similar situation as @ttsiodras and after some hours of searching and experimenting I finally understood the problem and the solution became evident.

The requirement is to write several lines all having the same indentation. A template engine takes data and a pattern as input and produces a string as output. If the data is a list and one wants to use the same pattern with each element of the list, the solution is to iterate over the list!

Data:

linesGlobal = [
    'int i;', 
    'i=1;'
]
linesLocal = [
    'int j=i;', 
    'j++;'
]

Template:

void foo() {
    if (someRuntimeFlag) {
        {%- for line in linesGlobal %}
        {{line}}
        {%- endfor %}
        if (anotherRuntimeFlag) {
            {%- for line in linesLocal %}
            {{line}}
            {%- endfor %}
        }
    }
}

Generated string:

void foo() {
    if (someRuntimeFlag) {
        int i;
        i=1;
        if (anotherRuntimeFlag) {
            int j=i;
            j++;
        }
    }
}

@maxime-esa
Copy link

maxime-esa commented Nov 18, 2024

@mardukbp Point is data is not a list. It's a multiline block of text that has possibly been generated by the application of another template.

@mardukbp
Copy link

@maxime-esa That is precisely the situation that brought me here. The multiline string can be converted into data (e.g. with splitlines) and after removing its original indentation it can be used in a template with any level of indentation.

What we see here is that templates represented as strings are not easy to compose. But data is. That is why programming languages with macros are better suited for code generation (e.g. Clojure or Elixir).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet