diff --git a/proto/backend.proto b/proto/backend.proto
index fa93ddcb0..e546b6ce7 100644
--- a/proto/backend.proto
+++ b/proto/backend.proto
@@ -14,6 +14,7 @@ message BackendInput {
Empty deck_tree = 18;
FindCardsIn find_cards = 19;
BrowserRowsIn browser_rows = 20;
+ FlattenTemplateIn flatten_template = 21;
PlusOneIn plus_one = 2046; // temporary, for testing
}
@@ -26,6 +27,7 @@ message BackendOutput {
DeckTreeOut deck_tree = 18;
FindCardsOut find_cards = 19;
BrowserRowsOut browser_rows = 20;
+ FlattenTemplateOut flatten_template = 21;
PlusOneOut plus_one = 2046; // temporary, for testing
@@ -124,3 +126,24 @@ message BrowserRowsOut {
// just sort fields for proof of concept
repeated string sort_fields = 1;
}
+
+message FlattenTemplateIn {
+ string template_text = 1;
+ repeated string nonempty_field_names = 2;
+}
+
+message FlattenTemplateOut {
+ repeated FlattenedTemplateNode nodes = 1;
+}
+
+message FlattenedTemplateNode {
+ oneof value {
+ string text = 1;
+ FlattenedTemplateReplacement replacement = 2;
+ }
+}
+
+message FlattenedTemplateReplacement {
+ string field_name = 1;
+ repeated string filters = 2;
+}
diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py
index 6ff2992d4..435a62735 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_from_field_map
+from anki.template2 import render_qa_from_field_map
from anki.types import NoteType, QAData, Template
from anki.utils import (
devMode,
@@ -666,7 +666,7 @@ where c.nid = n.id and c.id in %s group by nid"""
fields = runFilter("mungeFields", fields, model, data, self)
# render fields
- qatext = render_from_field_map(qfmt, afmt, fields, card_ord)
+ qatext = render_qa_from_field_map(self, qfmt, afmt, fields, card_ord)
ret: Dict[str, Any] = dict(q=qatext[0], a=qatext[1], id=card_id)
# allow add-ons to modify the generated result
diff --git a/pylib/anki/rsbackend.py b/pylib/anki/rsbackend.py
index 50ff15561..9623f5d27 100644
--- a/pylib/anki/rsbackend.py
+++ b/pylib/anki/rsbackend.py
@@ -1,8 +1,8 @@
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
# pylint: skip-file
-
-from typing import Dict, List
+from dataclasses import dataclass
+from typing import Dict, List, Union
import ankirspy # pytype: disable=import-error
@@ -46,6 +46,12 @@ def proto_template_reqs_to_legacy(
return legacy_reqs
+@dataclass
+class TemplateReplacement:
+ field_name: str
+ filters: List[str]
+
+
class RustBackend:
def __init__(self, path: str):
self._backend = ankirspy.Backend(path)
@@ -88,3 +94,26 @@ class RustBackend:
)
)
).sched_timing_today
+
+ def flatten_template(
+ self, template: str, nonempty_fields: List[str]
+ ) -> List[Union[str, TemplateReplacement]]:
+ out = self._run_command(
+ pb.BackendInput(
+ flatten_template=pb.FlattenTemplateIn(
+ template_text=template, nonempty_field_names=nonempty_fields
+ )
+ )
+ ).flatten_template
+ results: List[Union[str, TemplateReplacement]] = []
+ for node in out.nodes:
+ if node.WhichOneof("value") == "text":
+ results.append(node.text)
+ else:
+ results.append(
+ TemplateReplacement(
+ field_name=node.replacement.field_name,
+ filters=list(node.replacement.filters),
+ )
+ )
+ return results
diff --git a/pylib/anki/template2.py b/pylib/anki/template2.py
index bde7a4197..eb175d952 100644
--- a/pylib/anki/template2.py
+++ b/pylib/anki/template2.py
@@ -9,33 +9,75 @@ connected to pystache. It may be renamed in the future.
from __future__ import annotations
import re
-from typing import Any, Callable, Dict, List, Tuple
+from typing import Any, Callable, Dict, List, Tuple, Union
import anki
from anki.hooks import addHook, runFilter
from anki.lang import _
+from anki.rsbackend import TemplateReplacement
from anki.sound import stripSounds
from anki.utils import stripHTML, stripHTMLMedia
-def render_from_field_map(
- qfmt: str, afmt: str, fields: Dict[str, str], card_ord: int
+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 = anki.template.render(format, fields)
+ 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 = anki.template.render(format, fields)
+ 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."
+ old_output = anki.template.render(format, fields)
+
+ nonempty = nonempty_fields(fields)
+ flattened = col.backend.flatten_template(format, nonempty)
+ new_output = render_flattened_template(flattened, fields)
+
+ if old_output != new_output:
+ print(
+ f"template rendering didn't match - please report:\n'{old_output}'\n'{new_output}'"
+ )
+ # import os
+ # open(os.path.expanduser("~/temp1.txt"), "w").write(old_output)
+ # open(os.path.expanduser("~/temp2.txt"), "w").write(new_output)
+ return new_output
+
+
+def render_flattened_template(
+ flattened: List[Union[str, TemplateReplacement]], fields: Dict[str, str]
+) -> str:
+ "Render a list of strings or replacements into a string."
+ res = ""
+ for node in flattened:
+ if isinstance(node, str):
+ res += node
+ else:
+ text = fields.get(node.field_name)
+ if text is None:
+ res += unknown_field_message(node)
+ continue
+ res += apply_field_filters(node.field_name, text, fields, node.filters)
+ return res
+
+
def field_is_not_empty(field_text: str) -> bool:
# fixme: this is an overkill way of preventing a field with only
# a
or