Anki/pylib/anki/template/template.py
Damien Elmes f7ff0d1c17 flatten->render; pass field content in
This is paving the way to move the standard filters into Rust.

Non-empty fields are now determined in Rust, using a single regex
instead of the overkill stripHTMLMedia(). The old implementation
has been moved into the Pystache code for now.
2020-01-10 14:59:29 +10:00

167 lines
4.9 KiB
Python

import re
from typing import Any, Callable, Dict, Pattern
from anki.template2 import apply_field_filters
modifiers: Dict[str, Callable] = {}
def field_is_not_empty(field_text: str) -> bool:
# fixme: this is an overkill way of preventing a field with only
# a <br> or <div> from appearing non-empty
from anki.utils import stripHTMLMedia
field_text = stripHTMLMedia(field_text)
return field_text.strip() != ""
def modifier(symbol) -> Callable[[Any], Any]:
"""Decorator for associating a function with a Mustache tag modifier.
@modifier('P')
def render_tongue(self, tag_name=None, context=None):
return ":P %s" % tag_name
{{P yo }} => :P yo
"""
def set_modifier(func):
modifiers[symbol] = func
return func
return set_modifier
def get_or_attr(obj, name, default=None) -> Any:
try:
return obj[name]
except KeyError:
return default
except:
try:
return getattr(obj, name)
except AttributeError:
return default
class Template:
# The regular expression used to find a #section
section_re: Pattern = None
# The regular expression used to find a tag.
tag_re: Pattern = None
# Opening tag delimiter
otag = "{{"
# Closing tag delimiter
ctag = "}}"
def __init__(self, template, context=None) -> None:
self.template = template
self.context = context or {}
self.compile_regexps()
def render(self, template=None, context=None, encoding=None) -> str:
"""Turns a Mustache template into something wonderful."""
template = template or self.template
context = context or self.context
template = self.render_sections(template, context)
result = self.render_tags(template, context)
# if encoding is not None:
# result = result.encode(encoding)
return result
def compile_regexps(self) -> None:
"""Compiles our section and tag regular expressions."""
tags = {"otag": re.escape(self.otag), "ctag": re.escape(self.ctag)}
section = r"%(otag)s[\#|^]([^\}]*)%(ctag)s(.+?)%(otag)s/\1%(ctag)s"
self.section_re = re.compile(section % tags, re.M | re.S)
tag = r"%(otag)s(#|=|&|!|>|\{)?(.+?)\1?%(ctag)s+"
self.tag_re = re.compile(tag % tags)
def render_sections(self, template, context) -> str:
"""Expands sections."""
while 1:
match = self.section_re.search(template)
if match is None:
break
section, section_name, inner = match.group(0, 1, 2)
section_name = section_name.strip()
val = get_or_attr(context, section_name, None)
replacer = ""
inverted = section[2] == "^"
nonempty = field_is_not_empty(val or "")
if (nonempty and not inverted) or (not nonempty and inverted):
replacer = inner
template = template.replace(section, replacer)
return template
def render_tags(self, template, context) -> str:
"""Renders all the tags in a template for a context."""
repCount = 0
while 1:
if repCount > 100:
print("too many replacements")
break
repCount += 1
match = self.tag_re.search(template)
if match is None:
break
tag, tag_type, tag_name = match.group(0, 1, 2)
tag_name = tag_name.strip()
try:
func = modifiers[tag_type]
replacement = func(self, tag_name, context)
template = template.replace(tag, replacement)
except (SyntaxError, KeyError):
return "{{invalid template}}"
return template
# {{{ functions just like {{ in anki
@modifier("{")
def render_tag(self, tag_name, context) -> Any:
return self.render_unescaped(tag_name, context)
@modifier("!")
def render_comment(self, tag_name=None, context=None) -> str:
"""Rendering a comment always returns nothing."""
return ""
@modifier(None)
def render_unescaped(self, tag_name=None, context=None) -> Any:
"""Render a tag without escaping it."""
# split out field modifiers
*mods, tag = tag_name.split(":")
# return an error if field doesn't exist
txt = get_or_attr(context, tag)
if txt is None:
return "{unknown field %s}" % tag_name
# the filter closest to the field name is applied first
mods.reverse()
return apply_field_filters(tag, txt, context, mods)
@modifier("=")
def render_delimiter(self, tag_name=None, context=None) -> str:
"""Changes the Mustache delimiter."""
try:
self.otag, self.ctag = tag_name.split(" ")
except ValueError:
# invalid
return ""
self.compile_regexps()
return ""