summaryrefslogtreecommitdiffstats
path: root/crmsh/handles.py
blob: e43f41522381d77c18cbe7efa2c208475c9b888f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# Copyright (C) 2015 Kristoffer Gronlund <kgronlund@suse.com>
# See COPYING for license information.

import re


headmatcher = re.compile(r'\{\{(\#|\^)?([A-Za-z0-9\#\$:_-]+)\}\}')


class value(object):
    """
    An object that is indexable in mustasches,
    but also evaluates to a value itself.
    """
    def __init__(self, obj, value):
        self.value = value
        self.obj = obj
        self.get = obj.get

    def __call__(self):
        return self.value

    def __repr__(self):
        return "handles.value(%s, %s)" % (repr(self.obj), repr(self.value))

    def __str__(self):
        return "handles.value(%s, %s)" % (repr(self.obj), repr(self.value))


def _join(d1, d2):
    d = d1.copy()
    d.update(d2)
    return d


def _resolve(path, context, strict):
    for values in context:
        r = path
        p = values
        while r and p is not None:
            p, r = p.get(r[0]), r[1:]
        if strict and r:
            continue
        if callable(p):
            p = p()
        if p is not None:
            return p
    if strict:
        raise ValueError("Not set: %s" % (':'.join(path)))
    return None


def _push(path, value, context):
    root = {}
    leaf = root
    for x in path[:-1]:
        leaf = {}
        root[x] = leaf
    leaf[path[-1]] = value
    ret = [root] + context
    return ret


def _textify(obj):
    if obj is None:
        return ''
    elif obj is True:
        return 'true'
    elif obj is False:
        return 'false'
    return str(obj)


def _parse(template, context, strict):
    ret = ""
    while template:
        head = headmatcher.search(template)
        if head is None:
            ret += template
            break
        istart, iend, prefix, key = head.start(0), head.end(0), head.group(1), head.group(2)
        if istart > 0:
            ret += template[:istart]
        path, block, invert = key.split(':'), prefix == '#', prefix == '^'
        if not path:
            raise ValueError("empty {{}} block found")
        obj = _resolve(path, context, strict)
        if block or invert:
            tailtag = '{{/%s}}' % (key)
            tailidx = iend + template[head.end(0):].find(tailtag)
            if tailidx < iend:
                raise ValueError("Unclosed conditional: %s" % head.group(0))
            iend = tailidx + len(tailtag)
            body = template[head.end(0):tailidx]
            if body.startswith('\n') and (not ret or ret.endswith('\n')):
                ret = ret[:-1]
            if block:
                if obj in (None, False):
                    pass
                elif isinstance(obj, (tuple, list)):
                    for it in obj:
                        ret += _parse(body, _push(path, it, context), strict)
                else:
                    ret += _parse(body, context, strict)
            elif not obj:
                ret += _parse(body, _push(path, "", context), strict)
            if ret.endswith('\n') and template[iend:].startswith('\n'):
                iend += 1
        elif obj is not None:
            ret += _textify(obj)
        template = template[iend:]
    return ret


def parse(template, values, strict=False):
    """
    Takes as input a template string and a dict
    of values, and replaces the following:
    {{object:key}} = look up key in object and insert value
    {{object}} = insert value if not None or False.
    {{#object}} ... {{/object}} = if object is a dict or value, process text. if object
    is a list, process text for each item in the list
    (can't nest these for items with the same name)
    {{^object}} ... {{/object}} = if object is falsy, process text.
    If a path evaluates to a callable, the callable will be invoked to get the value.
    """
    return _parse(template, [values], strict)