Files
MyFSIO/crates/myfsio-server/templates/_convert.py

293 lines
8.3 KiB
Python

import os
import re
import sys
TEMPLATE_DIR = os.path.dirname(os.path.abspath(__file__))
TERNARY_RE = re.compile(
r"""(\{\{\s*)
(?:"([^"]*)"|'([^']*)') # literal A
\s+if\s+
([^{}]+?) # condition
\s+else\s+
(?:"([^"]*)"|'([^']*)') # literal B
(\s*\}\})""",
re.VERBOSE,
)
TERNARY_SET_RE = re.compile(
r"""(\{%\s*set\s+([A-Za-z_][A-Za-z_0-9]*)\s*=\s*)
(?:"([^"]*)"|'([^']*)')
\s+if\s+
([^{}]+?)
\s+else\s+
(?:"([^"]*)"|'([^']*)')
(\s*%\})""",
re.VERBOSE,
)
def convert_single_quoted_strings_in_expressions(text: str) -> str:
"""Inside {{...}} or {%...%}, swap ' for " around tokens that look like strings."""
def fix(m):
body = m.group(2)
body_fixed = re.sub(r"'([^'\\\n]*)'", r'"\1"', body)
return m.group(1) + body_fixed + m.group(3)
return re.sub(
r"(\{[{%])([^{}]*?)([}%]\})",
fix,
text,
flags=re.DOTALL,
)
def convert_inline_ternary(text: str) -> str:
def repl_expr(m):
a = m.group(2) if m.group(2) is not None else m.group(3)
cond = m.group(4)
b = m.group(5) if m.group(5) is not None else m.group(6)
return (
'{% if ' + cond + ' %}' + a + '{% else %}' + b + '{% endif %}'
)
def repl_set(m):
varname = m.group(2)
a = m.group(3) if m.group(3) is not None else m.group(4)
cond = m.group(5)
b = m.group(6) if m.group(6) is not None else m.group(7)
return (
'{% if ' + cond + ' %}{% set ' + varname + ' = "' + a + '" %}'
'{% else %}{% set ' + varname + ' = "' + b + '" %}{% endif %}'
)
prev = None
while prev != text:
prev = text
text = TERNARY_SET_RE.sub(repl_set, text)
text = TERNARY_RE.sub(repl_expr, text)
return text
def convert_request_args(text: str) -> str:
text = re.sub(
r'request\.args\.get\(\s*"([^"]+)"\s*,\s*"([^"]*)"\s*\)',
r'request_args.\1 | default(value="\2")',
text,
)
text = re.sub(
r'request\.args\.get\(\s*"([^"]+)"\s*\)',
r'request_args.\1',
text,
)
text = text.replace('request.endpoint', 'current_endpoint')
return text
def convert_items_keys(text: str) -> str:
text = re.sub(r'\.items\(\)', '', text)
text = re.sub(r'\.keys\(\)', '', text)
text = re.sub(r'\.values\(\)', '', text)
return text
def convert_tojson(text: str) -> str:
text = re.sub(r'\|\s*tojson\b', '| json_encode | safe', text)
return text
def convert_is_none(text: str) -> str:
text = re.sub(r'\bis\s+not\s+none\b', '!= null', text)
text = re.sub(r'\bis\s+none\b', '== null', text)
return text
def convert_namespace(text: str) -> str:
def repl(m):
body = m.group(1)
assigns = [a.strip() for a in body.split(',')]
return '{# namespace shim #}'
text = re.sub(
r'\{%\s*set\s+ns\s*=\s*namespace\(([^)]*)\)\s*%\}',
repl,
text,
)
text = re.sub(r'\bns\.([A-Za-z_][A-Za-z_0-9]*)\s*=\s*', r'{% set_global \1 = ', text)
text = re.sub(r'\bns\.([A-Za-z_][A-Za-z_0-9]*)', r'\1', text)
return text
def convert_url_for_positional(text: str) -> str:
"""url_for("x", ...) -> url_for(endpoint="x", ...)"""
def repl(m):
prefix = m.group(1)
endpoint = m.group(2)
rest = m.group(3) or ''
rest = rest.strip()
if rest.startswith(','):
rest = rest[1:].strip()
if rest:
return f'{prefix}(endpoint="{endpoint}", {rest})'
return f'{prefix}(endpoint="{endpoint}")'
pattern = re.compile(r'(url_for)\(\s*"([^"]+)"\s*((?:,[^()]*)?)\)')
prev = None
while prev != text:
prev = text
text = pattern.sub(repl, text)
return text
def convert_d_filter(text: str) -> str:
text = re.sub(r'\|\s*d\(\s*([^)]*?)\s*\)', lambda m: f'| default(value={m.group(1) or 0})', text)
return text
def convert_replace_filter(text: str) -> str:
def repl(m):
a = m.group(1)
b = m.group(2)
return f'| replace(from="{a}", to="{b}")'
text = re.sub(r'\|\s*replace\(\s*"([^"]*)"\s*,\s*"([^"]*)"\s*\)', repl, text)
return text
def convert_truncate_filter(text: str) -> str:
def repl(m):
n = m.group(1)
return f'| truncate(length={n})'
text = re.sub(r'\|\s*truncate\(\s*(\d+)\s*(?:,[^)]*)?\)', repl, text)
return text
def convert_strip_method(text: str) -> str:
text = re.sub(r'(\b[A-Za-z_][A-Za-z_0-9.\[\]"]*)\s*\.\s*strip\(\s*\)', r'\1 | trim', text)
return text
def convert_split_method(text: str) -> str:
def repl(m):
obj = m.group(1)
sep = m.group(2)
return f'{obj} | split(pat="{sep}")'
text = re.sub(r'(\b[A-Za-z_][A-Za-z_0-9.]*)\s*\.\s*split\(\s*"([^"]*)"\s*\)', repl, text)
return text
def convert_python_slice(text: str) -> str:
def repl_colon(m):
obj = m.group(1)
start = m.group(2) or '0'
end = m.group(3)
if start.startswith('-') or (end and end.startswith('-')):
return m.group(0)
if end:
return f'{obj} | slice(start={start}, end={end})'
return f'{obj} | slice(start={start})'
def repl_neg_end(m):
obj = m.group(1)
n = m.group(2)
return f'{obj} | slice(start=-{n})'
text = re.sub(
r'(\b[A-Za-z_][A-Za-z_0-9.]*)\[\s*(-?\d*)\s*:\s*(-?\d*)\s*\]',
repl_colon,
text,
)
text = re.sub(
r'(\b[A-Za-z_][A-Za-z_0-9.]*)\|\s*slice\(start=-(\d+)\s*,\s*end=\s*\)',
repl_neg_end,
text,
)
return text
def convert_inline_ternary_expr(text: str) -> str:
"""Handle arbitrary ternary inside {{ ... }}: A if COND else B -> {% if COND %}A{% else %}B{% endif %}"""
out_lines = []
for line in text.split('\n'):
out_lines.append(_convert_line_ternary(line))
return '\n'.join(out_lines)
def _convert_line_ternary(line: str) -> str:
if '{{' not in line or ' if ' not in line or ' else ' not in line:
return line
prev = None
while prev != line:
prev = line
m = re.search(r'\{\{\s*([^{}]+?)\s+if\s+([^{}]+?)\s+else\s+([^{}]+?)\s*\}\}', line)
if not m:
break
replacement = '{% if ' + m.group(2) + ' %}{{ ' + m.group(1) + ' }}{% else %}{{ ' + m.group(3) + ' }}{% endif %}'
line = line[:m.start()] + replacement + line[m.end():]
return line
def convert_dict_get(text: str) -> str:
"""Convert X.get("key", default) -> X.key | default(value=default) when simple."""
pattern = re.compile(
r'([A-Za-z_][A-Za-z_0-9]*(?:\.[A-Za-z_][A-Za-z_0-9]*)*)'
r'\.get\(\s*"([A-Za-z_][A-Za-z_0-9]*)"\s*(?:,\s*([^(){}]+?))?\s*\)'
)
def repl(m):
obj = m.group(1)
key = m.group(2)
default = (m.group(3) or '').strip()
if default:
return f'{obj}.{key} | default(value={default})'
return f'{obj}.{key}'
prev = None
while prev != text:
prev = text
text = pattern.sub(repl, text)
return text
def convert_file(path: str) -> bool:
with open(path, 'r', encoding='utf-8') as f:
original = f.read()
text = original
text = convert_single_quoted_strings_in_expressions(text)
text = convert_inline_ternary(text)
text = convert_request_args(text)
text = convert_items_keys(text)
text = convert_tojson(text)
text = convert_is_none(text)
text = convert_namespace(text)
text = convert_dict_get(text)
text = convert_url_for_positional(text)
text = convert_d_filter(text)
text = convert_replace_filter(text)
text = convert_truncate_filter(text)
text = convert_strip_method(text)
text = convert_split_method(text)
text = convert_python_slice(text)
text = convert_inline_ternary_expr(text)
if text != original:
with open(path, 'w', encoding='utf-8', newline='\n') as f:
f.write(text)
return True
return False
def main():
changed = []
for name in sorted(os.listdir(TEMPLATE_DIR)):
if not name.endswith('.html'):
continue
p = os.path.join(TEMPLATE_DIR, name)
if convert_file(p):
changed.append(name)
print('Changed:', len(changed))
for c in changed:
print(' -', c)
if __name__ == '__main__':
main()