Jinja keep indentation on include or macro
It would be easier if Jinja provided the facility. It looks like some work was done on this but the issue is currently closed (20 Nov 2019) and the pull request hasn't yet been merged. It could be because things get tricky quite quickly with indents (think of tabs and spaces, for example.)
The following is a simple solution I've found effective for generating Python code which, of course, needs to handle indenting well. It copes with files that use spaces for indentation.
auto_indent()
detects the indent level of a variable in a host template, then applies that indent to a piece of text.
import os
import itertools
import jinja2
def indent_lines(text_lines: list, indent: int):
return [' ' * indent + line for line in text_lines]
def matching_line(s, substring):
lineno = s[:s.index(substring)].count('\n')
return s.splitlines()[lineno]
def is_space(c):
return c == ' '
def indentation(line: str) -> int:
initial_spaces = ''.join(itertools.takewhile(is_space, line))
return len(initial_spaces)
def auto_indent(template: str, placeholder: str, content_to_indent: str):
placeholder_line = matching_line(template, '{{ ' + placeholder + ' }}')
indent_width = indentation(placeholder_line)
lines = content_to_indent.splitlines()
first_line = [lines[0]] # first line uses placeholder indent-- no added indent
rest = indent_lines(lines[1:], indent_width)
return os.linesep.join(first_line + rest)
Example:
action_class = """\
class Actions:
def __init__(self):
pass
def prequel(self):
pass
{{ methods }}
def sequel(self):
pass
"""
inserted_methods = """\
def create_branch():
pass
def merge_branch():
pass
"""
if __name__ == '__main__':
indented_methods = auto_indent(action_class, 'methods', inserted_methods)
print(jinja2.Template(action_class).render(methods=indented_methods))
Example output:
>>> python indent.py
class Actions:
def __init__(self):
pass
def prequel(self):
pass
def create_branch():
pass
def merge_branch():
pass
def sequel(self):
pass
I've written a jinja2
extension to work around this long standing issue. It automates the previously proposed solution of using {% filter indent(...) %}
by hooking into the preproccess
api provided by jinja.ext.Extension
.
If you add the extension in your jinja.Environment
you can use the following syntax to include templates that get indented correctly into the rest of your template. Notice the indent content
directive.
class MyClass:
def someOp():
pass
{% include "someOp.html" indent content %}
The result of rendering then becomes
class MyClass:
def someOp():
pass
def someOp2():
pass
One way is to wrap the include in a macro, then because the macro is a function, its output can be passed through the indent filter:
class MyClass:
def someOp():
pass
{% macro someop() %}{% include "someOp.html" %}{% endmacro %}
{{ someop()|indent }}
By default 'indent' indents 4 spaces and does not indent the first line, you can use e.g. 'indent(8)' to indent further, see http://jinja.pocoo.org/docs/templates/#list-of-builtin-filters for more details.
If what you're including is defined as a macro to begin with then the further wrapper macro is not needed, and you can jump straight to using the indent filter.
I was looking in Jinja2 to achieve the same and got to conclusion aligning multi-line block indentation with the originating Jinja statement is not possible currently.
I've posted a small PR to Jinja to add a new syntax {%* ... %}
and {{* ... }}
that does exactly this. See the PR for details:
https://github.com/pallets/jinja/pull/919