From a51a4e4d31c96f9fc3fd9c04f5e3f3508692e276 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sat, 11 Jan 2020 19:38:30 +1000 Subject: [PATCH] drop pystache and move legacy code into separate file --- LICENSE | 1 - pylib/LICENSE | 10 -- pylib/anki/collection.py | 2 +- pylib/anki/media.py | 2 +- pylib/anki/template.py | 145 +++++++++++++++ pylib/anki/template/LICENSE | 20 --- pylib/anki/template/README.anki | 8 - pylib/anki/template/__init__.py | 9 - pylib/anki/template/template.py | 167 ------------------ pylib/anki/template/view.py | 118 ------------- .../anki/{template2.py => template_legacy.py} | 161 +++-------------- pylib/tests/test_models.py | 10 -- pylib/tests/test_template.py | 2 +- 13 files changed, 171 insertions(+), 484 deletions(-) delete mode 100644 pylib/LICENSE create mode 100644 pylib/anki/template.py delete mode 100644 pylib/anki/template/LICENSE delete mode 100644 pylib/anki/template/README.anki delete mode 100644 pylib/anki/template/__init__.py delete mode 100644 pylib/anki/template/template.py delete mode 100644 pylib/anki/template/view.py rename pylib/anki/{template2.py => template_legacy.py} (54%) diff --git a/LICENSE b/LICENSE index 3054ca8e7..861e7e585 100644 --- a/LICENSE +++ b/LICENSE @@ -6,7 +6,6 @@ The following included source code items use a license other than AGPL3: In the pylib folder: - * The anki/template/ folder is based off pystache: MIT. * The SuperMemo importer: GPL3. * The Pauker importer: BSD-3. * statsbg.py: CC BY-SA 3.0. diff --git a/pylib/LICENSE b/pylib/LICENSE deleted file mode 100644 index f3a3c9c66..000000000 --- a/pylib/LICENSE +++ /dev/null @@ -1,10 +0,0 @@ -Anki is licensed under the GNU Affero General Public License, version 3 or -later, with portions contributed by Anki users licensed under the BSD-3 -license: https://github.com/ankitects/anki-contributors. - -The following included source code items use a license other than AGPL3: - - * The anki/template/ folder is based off pystache: MIT. - * The SuperMemo importer: GPL3. - * The Pauker importer: BSD-3. - * statsbg.py: CC BY-SA 3.0. diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index edab0967a..e0a36594e 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -30,7 +30,7 @@ from anki.rsbackend import RustBackend from anki.sched import Scheduler as V1Scheduler from anki.schedv2 import Scheduler as V2Scheduler from anki.tags import TagManager -from anki.template2 import render_qa_from_field_map +from anki.template import render_qa_from_field_map from anki.types import NoteType, QAData, Template from anki.utils import ( devMode, diff --git a/pylib/anki/media.py b/pylib/anki/media.py index df99e1880..5d365fa48 100644 --- a/pylib/anki/media.py +++ b/pylib/anki/media.py @@ -18,7 +18,7 @@ from anki.consts import * from anki.db import DB, DBError from anki.lang import _ from anki.latex import mungeQA -from anki.template2 import expand_clozes +from anki.template import expand_clozes from anki.utils import checksum, isMac, isWin diff --git a/pylib/anki/template.py b/pylib/anki/template.py new file mode 100644 index 000000000..c93a68c70 --- /dev/null +++ b/pylib/anki/template.py @@ -0,0 +1,145 @@ +# Copyright: Ankitects Pty Ltd and contributors +# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +""" +This file contains the Python portion of the template rendering code. + +Templates can have filters applied to field replacements. The Rust template +rendering code will apply any built in filters, and stop at the first +unrecognized filter. The remaining filters are returned to Python, +and applied using the hook system. For example, +{{myfilter:hint:text:Field}} will apply the built in text and hint filters, +and then attempt to apply myfilter. If no add-ons have provided the filter, +the text is not modified. + +Add-ons can register a filter by adding a hook to "fmod_". +As standard filters will not be run after a custom filter, it is up to the +add-on to do any further processing that is required. + +The hook is called with the arguments +(field_text, filter_args, field_map, field_name, ""). +The last argument is no longer used. +If the field name contains a hyphen, it is split on the hyphen, eg +{{foo-bar:baz}} calls fmod_foo with filter_args set to "bar". + +A Python implementation of the standard filters is currently available in the +template_legacy.py file. +""" + +from __future__ import annotations + +import re +from typing import Dict, List, Tuple, Union + +import anki +from anki.hooks import runFilter +from anki.rsbackend import TemplateReplacement +from anki.sound import stripSounds + + +def render_template( + col: anki.storage._Collection, format: str, fields: Dict[str, str] +) -> str: + "Render a single template." + rendered = col.backend.render_template(format, fields) + return apply_custom_filters(rendered, fields) + + +def render_qa_from_field_map( + col: anki.storage._Collection, + qfmt: str, + afmt: str, + fields: Dict[str, str], + card_ord: int, +) -> Tuple[str, str]: + "Renders the provided templates, returning rendered q & a text." + # question + format = re.sub("{{(?!type:)(.*?)cloze:", r"{{\1cq-%d:" % (card_ord + 1), qfmt) + format = format.replace("<%cloze:", "<%%cq:%d:" % (card_ord + 1)) + qtext = render_template(col, format, fields) + + # answer + format = re.sub("{{(.*?)cloze:", r"{{\1ca-%d:" % (card_ord + 1), afmt) + format = format.replace("<%cloze:", "<%%ca:%d:" % (card_ord + 1)) + fields["FrontSide"] = stripSounds(qtext) + atext = render_template(col, format, fields) + + return qtext, atext + + +def apply_custom_filters( + rendered: List[Union[str, TemplateReplacement]], fields: Dict[str, str] +) -> str: + "Complete rendering by applying any pending custom filters." + res = "" + for node in rendered: + if isinstance(node, str): + res += node + else: + res += apply_field_filters( + node.field_name, node.current_text, fields, node.filters + ) + return res + + +# Filters +########################################################################## + + +def apply_field_filters( + field_name: str, field_text: str, fields: Dict[str, str], filters: List[str] +) -> str: + """Apply filters to field text, returning modified text.""" + for filter in filters: + if "-" in filter: + filter_base, filter_args = filter.split("-", maxsplit=1) + else: + filter_base = filter + filter_args = "" + + # the fifth argument is no longer used + field_text = runFilter( + "fmod_" + filter_base, field_text, filter_args, fields, field_name, "" + ) + return field_text + + +# Cloze handling +########################################################################## + +# Matches a {{c123::clozed-out text::hint}} Cloze deletion, case-insensitively. +# The regex should be interpolated with a regex number and creates the following +# named groups: +# - tag: The lowercase or uppercase 'c' letter opening the Cloze. +# The c/C difference is only relevant to the legacy code. +# - content: Clozed-out content. +# - hint: Cloze hint, if provided. +clozeReg = r"(?si)\{\{(?Pc)%s::(?P.*?)(::(?P.*?))?\}\}" + +# Constants referring to group names within clozeReg. +CLOZE_REGEX_MATCH_GROUP_TAG = "tag" +CLOZE_REGEX_MATCH_GROUP_CONTENT = "content" +CLOZE_REGEX_MATCH_GROUP_HINT = "hint" + +# used by the media check functionality +def expand_clozes(string: str) -> List[str]: + "Render all clozes in string." + ords = set(re.findall(r"{{c(\d+)::.+?}}", string)) + strings = [] + + def qrepl(m): + if m.group(CLOZE_REGEX_MATCH_GROUP_HINT): + return "[%s]" % m.group(CLOZE_REGEX_MATCH_GROUP_HINT) + else: + return "[...]" + + def arepl(m): + return m.group(CLOZE_REGEX_MATCH_GROUP_CONTENT) + + for ord in ords: + s = re.sub(clozeReg % ord, qrepl, string) + s = re.sub(clozeReg % ".+?", arepl, s) + strings.append(s) + strings.append(re.sub(clozeReg % ".+?", arepl, string)) + + return strings diff --git a/pylib/anki/template/LICENSE b/pylib/anki/template/LICENSE deleted file mode 100644 index 2745bcce5..000000000 --- a/pylib/anki/template/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -Copyright (c) 2009 Chris Wanstrath - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/pylib/anki/template/README.anki b/pylib/anki/template/README.anki deleted file mode 100644 index d1e9223dc..000000000 --- a/pylib/anki/template/README.anki +++ /dev/null @@ -1,8 +0,0 @@ -Anki uses a modified version of Pystache to provide Mustache-like syntax. -Behaviour is a little different from standard Mustache: - -- {{text}} returns text verbatim with no HTML escaping -- {{{text}}} does the same and exists for backwards compatibility -- partial rendering is disabled for security reasons -- certain keywords like 'cloze' are treated specially - diff --git a/pylib/anki/template/__init__.py b/pylib/anki/template/__init__.py deleted file mode 100644 index beff00e84..000000000 --- a/pylib/anki/template/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -from typing import Any - -from .template import Template - - -def render(template, context=None, **kwargs) -> Any: - context = context and context.copy() or {} - context.update(kwargs) - return Template(template, context).render() diff --git a/pylib/anki/template/template.py b/pylib/anki/template/template.py deleted file mode 100644 index 81b3a1319..000000000 --- a/pylib/anki/template/template.py +++ /dev/null @@ -1,167 +0,0 @@ -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
or
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 "" diff --git a/pylib/anki/template/view.py b/pylib/anki/template/view.py deleted file mode 100644 index ae384742d..000000000 --- a/pylib/anki/template/view.py +++ /dev/null @@ -1,118 +0,0 @@ -import os.path -import re -from typing import Any - -from .template import Template - - -class View: - # Path where this view's template(s) live - template_path = "." - - # Extension for templates - template_extension = "mustache" - - # The name of this template. If none is given the View will try - # to infer it based on the class name. - template_name: str = None - - # Absolute path to the template itself. Pystache will try to guess - # if it's not provided. - template_file: str = None - - # Contents of the template. - template: str = None - - # Character encoding of the template file. If None, Pystache will not - # do any decoding of the template. - template_encoding: str = None - - def __init__(self, template=None, context=None, **kwargs) -> None: - self.template = template - self.context = context or {} - - # If the context we're handed is a View, we want to inherit - # its settings. - if isinstance(context, View): - self.inherit_settings(context) - - if kwargs: - self.context.update(kwargs) - - def inherit_settings(self, view) -> None: - """Given another View, copies its settings.""" - if view.template_path: - self.template_path = view.template_path - - if view.template_name: - self.template_name = view.template_name - - def load_template(self) -> Any: - if self.template: - return self.template - - if self.template_file: - return self._load_template() - - name = self.get_template_name() + "." + self.template_extension - - if isinstance(self.template_path, str): - self.template_file = os.path.join(self.template_path, name) - return self._load_template() - - for path in self.template_path: - self.template_file = os.path.join(path, name) - if os.path.exists(self.template_file): - return self._load_template() - - raise IOError('"%s" not found in "%s"' % (name, ":".join(self.template_path),)) - - def _load_template(self) -> str: - f = open(self.template_file, "r") - try: - template = f.read() - if self.template_encoding and isinstance(template, bytes): - template = str(template, self.template_encoding) - finally: - f.close() - return template - - def get_template_name(self, name=None) -> Any: - """TemplatePartial => template_partial - Takes a string but defaults to using the current class' name or - the `template_name` attribute - """ - if self.template_name: - return self.template_name - - if not name: - name = self.__class__.__name__ - - def repl(match): - return "_" + match.group(0).lower() - - return re.sub("[A-Z]", repl, name)[1:] - - def __contains__(self, needle) -> bool: - return needle in self.context or hasattr(self, needle) - - def __getitem__(self, attr) -> Any: - val = self.get(attr, None) - if not val: - raise KeyError("No such key.") - return val - - def get(self, attr, default) -> Any: - attr = self.context.get(attr, getattr(self, attr, default)) - - if hasattr(attr, "__call__"): - return attr() - else: - return attr - - def render(self, encoding=None) -> str: - template = self.load_template() - return Template(template, self).render(encoding=encoding) - - def __str__(self) -> str: - return self.render() diff --git a/pylib/anki/template2.py b/pylib/anki/template_legacy.py similarity index 54% rename from pylib/anki/template2.py rename to pylib/anki/template_legacy.py index f0da2dd3b..ad9850fc8 100644 --- a/pylib/anki/template2.py +++ b/pylib/anki/template_legacy.py @@ -2,125 +2,33 @@ # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html """ -This file contains some code related to templates that is not directly -connected to pystache. It may be renamed in the future. +This file contains code that is no longer used by Anki, but left around +for the benefit of add-ons. It may go away in the future, so please copy +any routines you need into your own add-on instead of using them directly +from this module. + +If your add-on was previously calling anki.template.render(), you now +need to call anki.template.render_template(), passing col in as the first +argument. """ from __future__ import annotations import re -from typing import Any, Callable, Dict, List, Tuple, Union +from typing import Any, Callable -import anki -from anki.hooks import addHook, runFilter from anki.lang import _ -from anki.rsbackend import TemplateReplacement -from anki.sound import stripSounds +from anki.template import ( + CLOZE_REGEX_MATCH_GROUP_CONTENT, + CLOZE_REGEX_MATCH_GROUP_HINT, + CLOZE_REGEX_MATCH_GROUP_TAG, + clozeReg, +) from anki.utils import stripHTML - -def render_qa_from_field_map( - col: anki.storage._Collection, - qfmt: str, - afmt: str, - fields: Dict[str, str], - card_ord: int, -) -> Tuple[str, str]: - "Renders the provided templates, returning rendered q & a text." - # question - format = re.sub("{{(?!type:)(.*?)cloze:", r"{{\1cq-%d:" % (card_ord + 1), qfmt) - format = format.replace("<%cloze:", "<%%cq:%d:" % (card_ord + 1)) - qtext = render_template(col, format, fields) - - # answer - format = re.sub("{{(.*?)cloze:", r"{{\1ca-%d:" % (card_ord + 1), afmt) - format = format.replace("<%cloze:", "<%%ca:%d:" % (card_ord + 1)) - fields["FrontSide"] = stripSounds(qtext) - atext = render_template(col, format, fields) - - return qtext, atext - - -def render_template( - col: anki.storage._Collection, format: str, fields: Dict[str, str] -) -> str: - "Render a single template." - rendered = col.backend.render_template(format, fields) - return apply_custom_filters(rendered, fields) - - -def apply_custom_filters( - rendered: List[Union[str, TemplateReplacement]], fields: Dict[str, str] -) -> str: - "Complete rendering by applying any pending custom filters." - res = "" - for node in rendered: - if isinstance(node, str): - res += node - else: - res += apply_field_filters( - node.field_name, node.current_text, fields, node.filters - ) - return res - - -# Filters -########################################################################## -# -# Applies field filters defined by hooks. Given {{filterName:field}} in -# the template, the hook fmod_filterName is called with the arguments -# (field_text, filter_args, fields, field_name, ""). -# The last argument is no longer used. -# If the field name contains a hyphen, it is split on the hyphen, eg -# {{foo-bar:baz}} calls fmod_foo with filter_args set to "bar". - - -def apply_field_filters( - field_name: str, field_text: str, fields: Dict[str, str], filters: List[str] -) -> str: - """Apply filters to field text, returning modified text.""" - filters = _adjust_filters(filters) - - for filter in filters: - if "-" in filter: - filter_base, filter_args = filter.split("-", maxsplit=1) - else: - filter_base = filter - filter_args = "" - - # the fifth argument is no longer used - - field_text = runFilter( - "fmod_" + filter_base, field_text, filter_args, fields, field_name, "" - ) - if not isinstance(field_text, str): - return "{field modifier '%s' on template invalid}" % filter - return field_text - - -def _adjust_filters(filters: List[str]) -> List[str]: - "Handle the type:cloze: special case." - if filters == ["cloze", "type"]: - filters = ["type-cloze"] - return filters - - # Cloze filter ########################################################################## -# Matches a {{c123::clozed-out text::hint}} Cloze deletion, case-insensitively. -# The regex should be interpolated with a regex number and creates the following -# named groups: -# - tag: The lowercase or uppercase 'c' letter opening the Cloze. -# - content: Clozed-out content. -# - hint: Cloze hint, if provided. -clozeReg = r"(?si)\{\{(?Pc)%s::(?P.*?)(::(?P.*?))?\}\}" - -# Constants referring to group names within clozeReg. -CLOZE_REGEX_MATCH_GROUP_TAG = "tag" -CLOZE_REGEX_MATCH_GROUP_CONTENT = "content" -CLOZE_REGEX_MATCH_GROUP_HINT = "hint" - def _clozeText(txt: str, ord: str, type: str) -> str: """Process the given Cloze deletion within the given template.""" @@ -207,29 +115,6 @@ def _removeFormattingFromMathjax(txt, ord) -> str: ) -def expand_clozes(string: str) -> List[str]: - "Render all clozes in string." - ords = set(re.findall(r"{{c(\d+)::.+?}}", string)) - strings = [] - - def qrepl(m): - if m.group(CLOZE_REGEX_MATCH_GROUP_HINT): - return "[%s]" % m.group(CLOZE_REGEX_MATCH_GROUP_HINT) - else: - return "[...]" - - def arepl(m): - return m.group(CLOZE_REGEX_MATCH_GROUP_CONTENT) - - for ord in ords: - s = re.sub(clozeReg % ord, qrepl, string) - s = re.sub(clozeReg % ".+?", arepl, s) - strings.append(s) - strings.append(re.sub(clozeReg % ".+?", arepl, string)) - - return strings - - def _cloze_filter(field_text: str, filter_args: str, q_or_a: str): return _clozeText(field_text, filter_args, q_or_a) @@ -242,8 +127,8 @@ def cloze_afilter(field_text: str, filter_args: str, *args): return _cloze_filter(field_text, filter_args, "a") -addHook("fmod_cq", cloze_qfilter) -addHook("fmod_ca", cloze_afilter) +# addHook("fmod_cq", cloze_qfilter) +# addHook("fmod_ca", cloze_afilter) # Other filters ########################################################################## @@ -309,9 +194,9 @@ def type_answer_filter(txt: str, filter_args: str, context, tag: str, dummy) -> return f"[[type:{tag}]]" -addHook("fmod_text", text_filter) -addHook("fmod_type", type_answer_filter) -addHook("fmod_hint", hint_filter) -addHook("fmod_kanji", kanji_filter) -addHook("fmod_kana", kana_filter) -addHook("fmod_furigana", furigana_filter) +# addHook("fmod_text", text_filter) +# addHook("fmod_type", type_answer_filter) +# addHook("fmod_hint", hint_filter) +# addHook("fmod_kanji", kanji_filter) +# addHook("fmod_kana", kana_filter) +# addHook("fmod_furigana", furigana_filter) diff --git a/pylib/tests/test_models.py b/pylib/tests/test_models.py index 54376a7a7..884c44cda 100644 --- a/pylib/tests/test_models.py +++ b/pylib/tests/test_models.py @@ -1,7 +1,6 @@ # coding: utf-8 import time -import anki.template from anki.consts import MODEL_CLOZE from anki.utils import isWin, joinFields, stripHTML from tests.shared import getEmptyCol @@ -350,15 +349,6 @@ def test_modelChange(): assert deck.db.scalar("select count() from cards where nid = ?", f.id) == 1 -def test_templates2(): - d = dict(Foo="x", Bar="y") - assert anki.template.render("{{Foo}}", d) == "x" - assert anki.template.render("{{#Foo}}{{Foo}}{{/Foo}}", d) == "x" - assert anki.template.render("{{#Foo}}{{Foo}}{{/Foo}}", d) == "x" - assert anki.template.render("{{#Bar}}{{#Foo}}{{Foo}}{{/Foo}}{{/Bar}}", d) == "x" - assert anki.template.render("{{#Baz}}{{#Foo}}{{Foo}}{{/Foo}}{{/Baz}}", d) == "" - - def test_availOrds(): d = getEmptyCol() m = d.models.current() diff --git a/pylib/tests/test_template.py b/pylib/tests/test_template.py index 71f53ef3b..d4338b9aa 100644 --- a/pylib/tests/test_template.py +++ b/pylib/tests/test_template.py @@ -1,4 +1,4 @@ -from anki.template2 import _removeFormattingFromMathjax +from anki.template_legacy import _removeFormattingFromMathjax def test_remove_formatting_from_mathjax():