jrnl.plugins.template   A
last analyzed

Complexity

Total Complexity 30

Size/Duplication

Total Lines 140
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 113
dl 0
loc 140
rs 10
c 0
b 0
f 0
wmc 30

11 Methods

Rating   Name   Duplication   Size   Complexity  
C Template._expand() 0 37 10
A Template._strip_single_nl() 0 6 4
A Template.render() 0 4 2
A Template._eval_context() 0 7 1
A Template._expand_loops() 0 18 2
A Template.render_block() 0 4 2
A Template._expand_cond() 0 12 2
A Template.__init__() 0 4 1
A Template._expand_vars() 0 6 3
A Template.from_file() 0 8 2
A Template._get_blocks() 0 7 1
1
import re
2
3
import yaml
4
5
VAR_RE = r"[_a-zA-Z][a-zA-Z0-9_]*"
6
EXPRESSION_RE = r"[\[\]():.a-zA-Z0-9_]*"
7
PRINT_RE = r"{{ *(.+?) *}}"
8
START_BLOCK_RE = r"{% *(if|for) +(.+?) *%}"
9
END_BLOCK_RE = r"{% *end(for|if) *%}"
10
FOR_RE = r"{{% *for +({varname}) +in +([^%]+) *%}}".format(varname=VAR_RE)
11
IF_RE = r"{% *if +(.+?) *%}"
12
BLOCK_RE = r"{% *block +(.+?) *%}((?:.|\n)+?){% *endblock *%}"
13
INCLUDE_RE = r"{% *include +(.+?) *%}"
14
15
16
class Template:
17
    def __init__(self, template):
18
        self.template = template
19
        self.clean_template = None
20
        self.blocks = {}
21
22
    @classmethod
23
    def from_file(cls, filename):
24
        with open(filename) as f:
25
            front_matter, body = f.read().strip("-\n").split("---", 2)
26
            front_matter = yaml.load(front_matter, Loader=yaml.FullLoader)
27
            template = cls(body)
28
        template.__dict__.update(front_matter)
29
        return template
30
31
    def render(self, **vars):
32
        if self.clean_template is None:
33
            self._get_blocks()
34
        return self._expand(self.clean_template, **vars)
35
36
    def render_block(self, block, **vars):
37
        if self.clean_template is None:
38
            self._get_blocks()
39
        return self._expand(self.blocks[block], **vars)
40
41
    def _eval_context(self, vars):
42
        import asteval
43
44
        e = asteval.Interpreter(use_numpy=False, writer=None)
45
        e.symtable.update(vars)
46
        e.symtable["__last_iteration"] = vars.get("__last_iteration", False)
47
        return e
48
49
    def _get_blocks(self):
50
        def s(match):
51
            name, contents = match.groups()
52
            self.blocks[name] = self._strip_single_nl(contents)
53
            return ""
54
55
        self.clean_template = re.sub(BLOCK_RE, s, self.template, flags=re.MULTILINE)
56
57
    def _expand(self, template, **vars):
58
        stack = sorted(
59
            [
60
                (m.start(), 1, m.groups()[0])
61
                for m in re.finditer(START_BLOCK_RE, template)
62
            ]
63
            + [
64
                (m.end(), -1, m.groups()[0])
65
                for m in re.finditer(END_BLOCK_RE, template)
66
            ]
67
        )
68
69
        last_nesting, nesting = 0, 0
70
        start = 0
71
        result = ""
72
        block_type = None
73
        if not stack:
74
            return self._expand_vars(template, **vars)
75
76
        for pos, indent, typ in stack:
77
            nesting += indent
78
            if nesting == 1 and last_nesting == 0:
79
                block_type = typ
80
                result += self._expand_vars(template[start:pos], **vars)
81
                start = pos
82
            elif nesting == 0 and last_nesting == 1:
83
                if block_type == "if":
84
                    result += self._expand_cond(template[start:pos], **vars)
85
                elif block_type == "for":
86
                    result += self._expand_loops(template[start:pos], **vars)
87
                elif block_type == "block":
88
                    result += self._save_block(template[start:pos], **vars)
89
                start = pos
90
            last_nesting = nesting
91
92
        result += self._expand_vars(template[stack[-1][0] :], **vars)
93
        return result
94
95
    def _expand_vars(self, template, **vars):
96
        safe_eval = self._eval_context(vars)
97
        expanded = re.sub(
98
            INCLUDE_RE, lambda m: self.render_block(m.groups()[0], **vars), template
99
        )
100
        return re.sub(PRINT_RE, lambda m: str(safe_eval(m.groups()[0])), expanded)
101
102
    def _expand_cond(self, template, **vars):
103
        start_block = re.search(IF_RE, template, re.M)
104
        end_block = list(re.finditer(END_BLOCK_RE, template, re.M))[-1]
105
        expression = start_block.groups()[0]
106
        sub_template = self._strip_single_nl(
107
            template[start_block.end() : end_block.start()]
108
        )
109
110
        safe_eval = self._eval_context(vars)
111
        if safe_eval(expression):
112
            return self._expand(sub_template)
113
        return ""
114
115
    def _strip_single_nl(self, template, strip_r=True):
116
        if template[0] == "\n":
117
            template = template[1:]
118
        if strip_r and template[-1] == "\n":
119
            template = template[:-1]
120
        return template
121
122
    def _expand_loops(self, template, **vars):
123
        start_block = re.search(FOR_RE, template, re.M)
124
        end_block = list(re.finditer(END_BLOCK_RE, template, re.M))[-1]
125
        var_name, iterator = start_block.groups()
126
        sub_template = self._strip_single_nl(
127
            template[start_block.end() : end_block.start()], strip_r=False
128
        )
129
130
        safe_eval = self._eval_context(vars)
131
132
        result = ""
133
        items = safe_eval(iterator)
134
        for idx, var in enumerate(items):
135
            vars[var_name] = var
136
            vars["__last_iteration"] = idx == len(items) - 1
137
            result += self._expand(sub_template, **vars)
138
        del vars[var_name]
139
        return self._strip_single_nl(result)
140