-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathqree.py
More file actions
executable file
·160 lines (141 loc) · 6.08 KB
/
qree.py
File metadata and controls
executable file
·160 lines (141 loc) · 6.08 KB
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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
"""
Qree: Tiny but mighty Python templating.
Copyright (c) 2020 Polydojo, Inc.
SOFTWARE LICENSING
------------------
The software is released "AS IS" under the MIT License,
WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED. Kindly
see LICENSE.txt for more details.
NO TRADEMARK RIGHTS
-------------------
The above software licensing terms DO NOT grant any right in the
trademarks, service marks, brand names or logos of Polydojo, Inc.
""";
import functools;
__version__ = "0.0.4"; # Req'd by flit.
__DEFAULT_TAG_MAP__ = { "@=": "@=", "@{": "@{", "@}": "@}",
"{{:": "{{:", ":}}": ":}}", "{{=": "{{=", "=}}": "=}}",
};
escHtml = lambda s: (str(s).replace("&", "&").replace("<", "<")
.replace(">", ">").replace('"', """).replace("'", "'") # TODO: Consider: .replace("`", "`")
);
def dictDefaults (dicty, defaults):
"Fills-in missing keys in `dicty` with those from `defaults`.";
for k in defaults:
if k not in dicty:
dicty[k] = defaults[k];
return dicty;
def findFirstMatch (haystack, needleList, fromIndex=0):
"Finds needle in `haystack from needlList w/ lowest index.";
foundNeedle = None;
minIndex = None;
for needle in needleList:
index = haystack.find(needle, fromIndex);
if index == -1:
pass;
elif (minIndex is None) or (index < minIndex):
foundNeedle = needle;
minIndex = index;
return (foundNeedle, minIndex);
def validateSubstitutionTagPair (opTag, clTag, tagMap):
"Helps escapeNonPyQuotes().";
assert opTag in [tagMap["{{="], tagMap["{{:"]];
expectedClTag = tagMap["=}}" if opTag == tagMap["{{="] else ":}}"];
if clTag != expectedClTag:
raise SyntaxError("Tag-mismatch. Expected %r, not %r" % (
expectedClTag, clTag,
));
# otherwise ...
return True;
def escapeNonPyQuotes (line, tagMap):
"Escapes single-quotes outside py-substitution bits.";
tags = list(map(tagMap.get, "{{= =}} {{: :}}".split()));
firstTag, firstIndex = findFirstMatch(line, tags);
if not firstTag:
return line.replace("'", r"\'");
# otherwise ...
nextTag, nextIndex = findFirstMatch(line, tags, firstIndex+1);
assert validateSubstitutionTagPair(firstTag, nextTag, tagMap);
beyondNextTagEndIndex = nextIndex + len(nextTag);
return (
line[ : firstIndex].replace("'", r"\'") +
line[firstIndex: beyondNextTagEndIndex] +
escapeNonPyQuotes(line[beyondNextTagEndIndex : ], tagMap) #+
);
def validateStandaloneIndentLine (line, tag):
"Ensures indent-line only has indent-tag, excl. comment.";
if line.split("#")[0].strip() != tag:
raise IndentationError("Invalid de/indent line: %r" % line);
return True;
def validateNoSpecialQreeToken (tplStr):
"Ensures that token '__qree' is absent in `tplStr`.";
if "__qree" in tplStr:
raise SyntaxError("The special token '__qree' is " +
"reserved for Qree itself. It may NOT appear " +
"in any template." #+
);
return True;
def quoteReplace (tplStr, variable="data", tagMap=None):
"Returns as string, the function-equivalent of `tplStr`.";
assert validateNoSpecialQreeToken(tplStr);
tagMap = dictDefaults(tagMap or {}, __DEFAULT_TAG_MAP__);
fnStr = "def templateFn (%s):\n" % variable;
innerIndentDepth = 0; # Depth due to @{ and @} only.
indentify = lambda: " " * ((1 + innerIndentDepth) * 4);
fnStr += indentify() + "from qree import escHtml as __qreeEsc;\n";
fnStr += indentify() + "__qreeOutput = '';\n";
for line in tplStr.splitlines(True):
# Param `keepends=True` ^^^^. (Not kwarg for py2.)
lx = line.lstrip();
if lx.startswith(tagMap["@="]):
pyCode = lx[len(tagMap["@="]) : ].lstrip();
# Remove whitespace right after '@=' ^^^^
fnStr += indentify() + pyCode + "\n";
elif lx.startswith(tagMap["@{"]):
assert validateStandaloneIndentLine(lx, tagMap["@{"])
innerIndentDepth += 1;
elif lx.startswith(tagMap["@}"]):
assert validateStandaloneIndentLine(lx, tagMap["@}"])
innerIndentDepth -= 1;
else:
fnStr += indentify() + "__qreeOutput += " + "'''" + (
escapeNonPyQuotes(line, tagMap) # <- Err thrower
.replace(tagMap["{{="], "''' + str(") # 1st, {{{-compat
.replace(tagMap["=}}"], ") + '''")
.replace(tagMap["{{:"], "''' + __qreeEsc(")
.replace(tagMap[":}}"], ") + '''")
) + "''';\n";
fnStr += indentify() + "return __qreeOutput;\n";
if innerIndentDepth != 0:
raise IndentationError("Tag-mismatch for tags " +
("%r and %r." % (tagMap["@{"], tagMap["@}"])) #+
);
return fnStr;
def execEval (fnStr):
"Converts `fnStr` to a callable function and returns it.";
exec(fnStr); # Via exec(), 'templateFn' has been defined.
return eval("templateFn");
def renderStr (tplStr, data=None, variable="data", tagMap=None):
"Render template `tplStr` using `data`.";
fnStr = quoteReplace(tplStr, variable, tagMap);
fn = execEval(fnStr);
return fn(data);
def renderPath (tplPath, data=None, variable="data", tagMap=None):
"Render template at path `tplPath` using `data`.";
with open(tplPath, "r") as f:
return renderStr(f.read(), data, variable, tagMap);
def view (tplPath, variable="data", tagMap=None):
"Returns a decorator for binding function to template at `tplPath`.";
def decorator (fn):
@functools.wraps(fn)
def wrapper (*a, **ka):
return renderPath(tplPath, fn(*a, **ka), variable, tagMap);
return wrapper;
return decorator;
# TODO/Consider:
#checkNonPathy = lambda s: "\n" in s or "{" in s or "@" in s;
#def render (s, data=None, variable="data", tagMap=None):
# "Wrapper that auto-picks renderPath() or renderStr()";
# renderFn = renderStr if checkNonPathy(s) else renderPath;
# return renderFn(s, data, variable, tagMap);
# End ######################################################