import re
import typing
from wheezy.template.comp import Tuple
from wheezy.template.utils import find_balanced
RE_ARGS = re.compile(r'\s*(?P<expr>(([\'"]).*?\3|.+?))\s*\,')
RE_KWARGS = re.compile(
r'\s*(?P<name>\w+)\s*=\s*(?P<expr>([\'"].*?[\'"]|.+?))\s*\,'
)
RE_STR_VALUE = re.compile(r'^[\'"](?P<value>.+)[\'"]$')
RE_INT_VALUE = re.compile(r"^(?P<value>(\d+))$")
# region: core extension
[docs]class DeterminedExtension(object):
"""Tranlates function calls between template engines.
Strictly determined known calls are converted to preprocessor
calls, e.g.::
@_('Name:')
@path_for('default')
@path_for('static', path='/static/css/site.css')
Those that are not strictly determined are ignored and processed
by runtime engine.
"""
def __init__(
self,
known_calls: typing.List[str],
runtime_token_start: str = "@",
token_start: str = "#",
) -> None:
self.token_start = token_start
self.pattern = re.compile(
r"%s(%s)(?=\()" % (runtime_token_start, "|".join(known_calls))
)
self.preprocessors = [self.preprocess]
def preprocess(self, source: str) -> str:
result = []
start = 0
for m in self.pattern.finditer(source):
pstart = m.end()
pend = find_balanced(source, pstart)
if determined(source[pstart + 1 : pend - 1]):
name = m.group(1)
result.append(source[start : m.start()])
result.append(self.token_start + "ctx['" + name + "']")
start = pstart
if start:
result.append(source[start:])
return "".join(result)
else:
return source
[docs]def determined(expression: str) -> bool:
"""Checks if expresion is strictly determined.
>>> determined("'default'")
True
>>> determined('name')
False
>>> determined("'default', id=id")
False
>>> determined("'default', lang=100")
True
>>> determined('')
True
"""
args, kwargs = parse_params(expression)
for arg in args:
if not str_or_int(arg):
return False
for arg in kwargs.values():
if not str_or_int(arg):
return False
return True
[docs]def parse_kwargs(text: str) -> typing.Mapping[str, str]:
"""Parses key-value type of parameters.
>>> parse_kwargs('id=item.id')
{'id': 'item.id'}
>>> sorted(parse_kwargs('lang="en", id=12').items())
[('id', '12'), ('lang', '"en"')]
"""
kwargs = {}
for m in RE_KWARGS.finditer(text + ","):
groups = m.groupdict()
kwargs[groups["name"].rstrip("_")] = groups["expr"]
return kwargs
[docs]def parse_args(text: str) -> typing.List[str]:
"""Parses argument type of parameters.
>>> parse_args('')
[]
>>> parse_args('10, "x"')
['10', '"x"']
>>> parse_args("'x', 100")
["'x'", '100']
>>> parse_args('"default"')
['"default"']
"""
args = []
for m in RE_ARGS.finditer(text + ","):
args.append(m.group("expr"))
return args
[docs]def parse_params(
text: str,
) -> Tuple[typing.List[str], typing.Mapping[str, str]]:
"""Parses function parameters.
>>> parse_params('')
([], {})
>>> parse_params('id=item.id')
([], {'id': 'item.id'})
>>> parse_params('"default"')
(['"default"'], {})
>>> parse_params('"default", lang="en"')
(['"default"'], {'lang': '"en"'})
"""
if "=" in text:
args = text.split("=")[0]
if "," in args:
args = args.rsplit(",", 1)[0]
kwargs = text[len(args) :]
return parse_args(args), parse_kwargs(kwargs)
else:
return [], parse_kwargs(text)
else:
return parse_args(text), {}
[docs]def str_or_int(text: str) -> bool:
"""Ensures ``text`` as string or int expression.
>>> str_or_int('"default"')
True
>>> str_or_int("'Hello'")
True
>>> str_or_int('100')
True
>>> str_or_int('item.id')
False
"""
m = RE_STR_VALUE.match(text)
if m:
return True
else:
m = RE_INT_VALUE.match(text)
if m:
return True
return False