more changes to the template code

- _renderQA() has moved to template.py:render_card()
- dropped QAData in favour of a properly typed dict
- render_card() returns a TemplateRenderOutput struct instead of a dict
- card_did_render now takes that output as the first arg, and can
mutate it
- TemplateRenderContext now stores the original card, so it can return
a card even in the add screen case

The old mungeFields and mungeQA hook have been removed as part of this
change. mungeQA can be replaced with the card_did_render hook.
In the mungeFields case, please switch to using field_filter instead.
This commit is contained in:
Damien Elmes 2020-01-24 13:44:13 +10:00
parent 836213e587
commit f900f24f60
8 changed files with 180 additions and 240 deletions

View file

@ -5,11 +5,12 @@ from __future__ import annotations
import pprint import pprint
import time import time
from typing import Any, Dict, List, Optional, Union from typing import List, Optional
import anki # pylint: disable=unused-import import anki # pylint: disable=unused-import
from anki import hooks from anki import hooks
from anki.consts import * from anki.consts import *
from anki.models import NoteType, Template
from anki.notes import Note from anki.notes import Note
from anki.sound import AVTag from anki.sound import AVTag
from anki.utils import intTime, joinFields, timestampID from anki.utils import intTime, joinFields, timestampID
@ -27,7 +28,6 @@ from anki.utils import intTime, joinFields, timestampID
class Card: class Card:
_qa: Optional[Dict[str, Union[str, int, List[AVTag]]]]
_note: Optional[Note] _note: Optional[Note]
timerStarted: Optional[float] timerStarted: Optional[float]
lastIvl: int lastIvl: int
@ -38,7 +38,7 @@ class Card:
) -> None: ) -> None:
self.col = col self.col = col
self.timerStarted = None self.timerStarted = None
self._qa = None self._render_output: Optional[anki.template.TemplateRenderOutput] = None
self._note = None self._note = None
if id: if id:
self.id = id self.id = id
@ -81,7 +81,7 @@ class Card:
self.flags, self.flags,
self.data, self.data,
) = self.col.db.first("select * from cards where id = ?", self.id) ) = self.col.db.first("select * from cards where id = ?", self.id)
self._qa = None self._render_output = None
self._note = None self._note = None
def flush(self) -> None: def flush(self) -> None:
@ -144,54 +144,44 @@ lapses=?, left=?, odue=?, odid=?, did=? where id = ?""",
) )
self.col.log(self) self.col.log(self)
def q(self, reload: bool = False, browser: bool = False) -> str: def question(self, reload: bool = False, browser: bool = False) -> str:
return self.css() + self._getQA(reload, browser)["q"] return self.css() + self.render_output(reload, browser).question_text
def a(self) -> str: def answer(self) -> str:
return self.css() + self._getQA()["a"] return self.css() + self.render_output().answer_text
def question_av_tags(self) -> List[AVTag]: def question_av_tags(self) -> List[AVTag]:
return self._qa["q_av_tags"] # type: ignore return self.render_output().question_av_tags
def answer_av_tags(self) -> List[AVTag]: def answer_av_tags(self) -> List[AVTag]:
return self._qa["a_av_tags"] # type: ignore return self.render_output().answer_av_tags
def css(self) -> str: def css(self) -> str:
return "<style>%s</style>" % self.model()["css"] return "<style>%s</style>" % self.model()["css"]
def _getQA(self, reload: bool = False, browser: bool = False) -> Any: def render_output(
if not self._qa or reload: self, reload: bool = False, browser: bool = False
f = self.note(reload) ) -> anki.template.TemplateRenderOutput:
m = self.model() if not self._render_output or reload:
t = self.template() note = self.note(reload)
if browser: self._render_output = anki.template.render_card(
args = [t.get("bqfmt"), t.get("bafmt")] self.col, self, note, browser
else: )
args = [] return self._render_output
self._qa = self.col._renderQA(
(
self.id,
f.id,
m["id"],
self.odid or self.did,
self.ord,
f.stringTags(),
f.joinedFields(),
self.flags,
),
*args,
) # type: ignore
return self._qa
def note(self, reload: bool = False) -> Any: def note(self, reload: bool = False) -> Note:
if not self._note or reload: if not self._note or reload:
self._note = self.col.getNote(self.nid) self._note = self.col.getNote(self.nid)
return self._note return self._note
def model(self) -> Any: def note_type(self) -> NoteType:
return self.col.models.get(self.note().mid) return self.col.models.get(self.note().mid)
def template(self) -> Any: q = question
a = answer
model = note_type
def template(self) -> Template:
m = self.model() m = self.model()
if m["type"] == MODEL_STD: if m["type"] == MODEL_STD:
return self.model()["tmpls"][self.ord] return self.model()["tmpls"][self.ord]
@ -201,16 +191,16 @@ lapses=?, left=?, odue=?, odid=?, did=? where id = ?""",
def startTimer(self) -> None: def startTimer(self) -> None:
self.timerStarted = time.time() self.timerStarted = time.time()
def timeLimit(self) -> Any: def timeLimit(self) -> int:
"Time limit for answering in milliseconds." "Time limit for answering in milliseconds."
conf = self.col.decks.confForDid(self.odid or self.did) conf = self.col.decks.confForDid(self.odid or self.did)
return conf["maxTaken"] * 1000 return conf["maxTaken"] * 1000
def shouldShowTimer(self) -> Any: def shouldShowTimer(self) -> bool:
conf = self.col.decks.confForDid(self.odid or self.did) conf = self.col.decks.confForDid(self.odid or self.did)
return conf["timer"] return conf["timer"]
def timeTaken(self) -> Any: def timeTaken(self) -> int:
"Time taken to answer card, in integer MS." "Time taken to answer card, in integer MS."
total = int((time.time() - self.timerStarted) * 1000) total = int((time.time() - self.timerStarted) * 1000)
return min(total, self.timeLimit()) return min(total, self.timeLimit())
@ -225,12 +215,12 @@ lapses=?, left=?, odue=?, odid=?, did=? where id = ?""",
d = dict(self.__dict__) d = dict(self.__dict__)
# remove non-useful elements # remove non-useful elements
del d["_note"] del d["_note"]
del d["_qa"] del d["_render_output"]
del d["col"] del d["col"]
del d["timerStarted"] del d["timerStarted"]
return pprint.pformat(d, width=300) return pprint.pformat(d, width=300)
def userFlag(self) -> Any: def userFlag(self) -> int:
return self.flags & 0b111 return self.flags & 0b111
def setUserFlag(self, flag: int) -> None: def setUserFlag(self, flag: int) -> None:

View file

@ -22,7 +22,6 @@ from anki.consts import *
from anki.db import DB from anki.db import DB
from anki.decks import DeckManager from anki.decks import DeckManager
from anki.errors import AnkiError from anki.errors import AnkiError
from anki.hooks import runFilter
from anki.lang import _, ngettext from anki.lang import _, ngettext
from anki.media import MediaManager from anki.media import MediaManager
from anki.models import ModelManager, NoteType, Template from anki.models import ModelManager, NoteType, Template
@ -30,9 +29,7 @@ from anki.notes import Note
from anki.rsbackend import RustBackend from anki.rsbackend import RustBackend
from anki.sched import Scheduler as V1Scheduler from anki.sched import Scheduler as V1Scheduler
from anki.schedv2 import Scheduler as V2Scheduler from anki.schedv2 import Scheduler as V2Scheduler
from anki.sound import AVTag
from anki.tags import TagManager from anki.tags import TagManager
from anki.template import QAData, RenderOutput, TemplateRenderContext, render_card
from anki.utils import ( from anki.utils import (
devMode, devMode,
fieldChecksum, fieldChecksum,
@ -614,107 +611,6 @@ where c.nid = n.id and c.id in %s group by nid"""
# apply, relying on calling code to bump usn+mod # apply, relying on calling code to bump usn+mod
self.db.executemany("update notes set sfld=?, csum=? where id=?", r) self.db.executemany("update notes set sfld=?, csum=? where id=?", r)
# Q/A generation
##########################################################################
# data is [cid, nid, mid, did, ord, tags, flds, cardFlags]
def _renderQA(
self, data: QAData, qfmt: Optional[str] = None, afmt: Optional[str] = None
) -> Dict[str, Union[str, int, List[AVTag]]]:
# extract info from data
split_fields = splitFields(data[6])
card_ord = data[4]
model = self.models.get(data[2])
if model["type"] == MODEL_STD:
template = model["tmpls"][data[4]]
else:
template = model["tmpls"][0]
flag = data[7]
deck_id = data[3]
card_id = data[0]
tags = data[5]
qfmt = qfmt or template["qfmt"]
afmt = afmt or template["afmt"]
# create map of field names -> field content
fields: Dict[str, str] = {}
for (name, (idx, conf)) in list(self.models.fieldMap(model).items()):
fields[name] = split_fields[idx]
# add special fields
fields["Tags"] = tags.strip()
fields["Type"] = model["name"]
fields["Deck"] = self.decks.name(deck_id)
fields["Subdeck"] = fields["Deck"].split("::")[-1]
fields["Card"] = template["name"]
fields["CardFlag"] = self._flagNameFromCardFlags(flag)
fields["c%d" % (card_ord + 1)] = "1"
# legacy hook
fields = runFilter("mungeFields", fields, model, data, self)
ctx = TemplateRenderContext(self, data, fields)
# render fields. if any custom filters are encountered,
# the field_filter hook will be called.
try:
output = render_card(self, qfmt, afmt, ctx)
except anki.rsbackend.BackendException as e:
errmsg = _("Card template has a problem:") + f"<br>{e}"
output = RenderOutput(
question_text=errmsg,
answer_text=errmsg,
question_av_tags=[],
answer_av_tags=[],
)
# avoid showing the user a confusing blank card if they've
# forgotten to add a cloze deletion
if model["type"] == MODEL_CLOZE:
if not self.models._availClozeOrds(model, data[6], False):
output.question_text += "<p>" + _(
"Please edit this note and add some cloze deletions. (%s)"
) % ("<a href=%s#cloze>%s</a>" % (HELP_SITE, _("help")))
# allow add-ons to modify the generated result
(output.question_text, output.answer_text) = hooks.card_did_render(
(output.question_text, output.answer_text), ctx
)
# legacy hook
output.question_text = runFilter(
"mungeQA", output.question_text, "q", fields, model, data, self
)
output.answer_text = runFilter(
"mungeQA", output.answer_text, "a", fields, model, data, self
)
return dict(
q=output.question_text,
a=output.answer_text,
id=card_id,
q_av_tags=output.question_av_tags,
a_av_tags=output.answer_av_tags,
)
def _qaData(self, where="") -> Any:
"Return [cid, nid, mid, did, ord, tags, flds, cardFlags] db query"
# NOTE: order selected from database must match order of QAData fields.
return self.db.execute(
"""
select c.id, f.id, f.mid, c.did, c.ord, f.tags, f.flds, c.flags
from cards c, notes f
where c.nid == f.id
%s"""
% where
)
def _flagNameFromCardFlags(self, flags: int) -> str:
flag = flags & 0b111
if not flag:
return ""
return "flag%d" % flag
# Finding cards # Finding cards
########################################################################## ##########################################################################

View file

@ -18,7 +18,6 @@ import decorator
import anki import anki
from anki.cards import Card from anki.cards import Card
from anki.template import TemplateRenderContext
# New hook/filter handling # New hook/filter handling
############################################################################## ##############################################################################
@ -54,39 +53,60 @@ class _CardDidLeechHook:
card_did_leech = _CardDidLeechHook() card_did_leech = _CardDidLeechHook()
class _CardDidRenderFilter: class _CardDidRenderHook:
"""Can modify the resulting text after rendering completes.""" """Can modify the resulting text after rendering completes."""
_hooks: List[ _hooks: List[
Callable[[Tuple[str, str], TemplateRenderContext], Tuple[str, str]] Callable[
[
"anki.template.TemplateRenderOutput",
"anki.template.TemplateRenderContext",
],
None,
]
] = [] ] = []
def append( def append(
self, cb: Callable[[Tuple[str, str], TemplateRenderContext], Tuple[str, str]] self,
cb: Callable[
[
"anki.template.TemplateRenderOutput",
"anki.template.TemplateRenderContext",
],
None,
],
) -> None: ) -> None:
"""(text: Tuple[str, str], ctx: TemplateRenderContext)""" """(output: anki.template.TemplateRenderOutput, ctx: anki.template.TemplateRenderContext)"""
self._hooks.append(cb) self._hooks.append(cb)
def remove( def remove(
self, cb: Callable[[Tuple[str, str], TemplateRenderContext], Tuple[str, str]] self,
cb: Callable[
[
"anki.template.TemplateRenderOutput",
"anki.template.TemplateRenderContext",
],
None,
],
) -> None: ) -> None:
if cb in self._hooks: if cb in self._hooks:
self._hooks.remove(cb) self._hooks.remove(cb)
def __call__( def __call__(
self, text: Tuple[str, str], ctx: TemplateRenderContext self,
) -> Tuple[str, str]: output: anki.template.TemplateRenderOutput,
for filter in self._hooks: ctx: anki.template.TemplateRenderContext,
) -> None:
for hook in self._hooks:
try: try:
text = filter(text, ctx) hook(output, ctx)
except: except:
# if the hook fails, remove it # if the hook fails, remove it
self._hooks.remove(filter) self._hooks.remove(hook)
raise raise
return text
card_did_render = _CardDidRenderFilter() card_did_render = _CardDidRenderHook()
class _CardOdueWasInvalidHook: class _CardOdueWasInvalidHook:
@ -171,13 +191,19 @@ class _FieldFilterFilter:
Your add-on can check filter_name to decide whether it should modify Your add-on can check filter_name to decide whether it should modify
field_text or not before returning it.""" field_text or not before returning it."""
_hooks: List[Callable[[str, str, str, TemplateRenderContext], str]] = [] _hooks: List[
Callable[[str, str, str, "anki.template.TemplateRenderContext"], str]
] = []
def append(self, cb: Callable[[str, str, str, TemplateRenderContext], str]) -> None: def append(
"""(field_text: str, field_name: str, filter_name: str, ctx: TemplateRenderContext)""" self, cb: Callable[[str, str, str, "anki.template.TemplateRenderContext"], str]
) -> None:
"""(field_text: str, field_name: str, filter_name: str, ctx: anki.template.TemplateRenderContext)"""
self._hooks.append(cb) self._hooks.append(cb)
def remove(self, cb: Callable[[str, str, str, TemplateRenderContext], str]) -> None: def remove(
self, cb: Callable[[str, str, str, "anki.template.TemplateRenderContext"], str]
) -> None:
if cb in self._hooks: if cb in self._hooks:
self._hooks.remove(cb) self._hooks.remove(cb)
@ -186,7 +212,7 @@ class _FieldFilterFilter:
field_text: str, field_text: str,
field_name: str, field_name: str,
filter_name: str, filter_name: str,
ctx: TemplateRenderContext, ctx: anki.template.TemplateRenderContext,
) -> str: ) -> str:
for filter in self._hooks: for filter in self._hooks:
try: try:

View file

@ -7,13 +7,13 @@ import html
import os import os
import re import re
import shutil import shutil
from typing import Any, Optional, Tuple from typing import Any, Optional
import anki import anki
from anki import hooks from anki import hooks
from anki.lang import _ from anki.lang import _
from anki.models import NoteType from anki.models import NoteType
from anki.template import TemplateRenderContext from anki.template import TemplateRenderContext, TemplateRenderOutput
from anki.utils import call, checksum, isMac, namedtmp, stripHTML, tmpdir from anki.utils import call, checksum, isMac, namedtmp, stripHTML, tmpdir
pngCommands = [ pngCommands = [
@ -48,15 +48,11 @@ def stripLatex(text) -> Any:
return text return text
def on_card_did_render( def on_card_did_render(output: TemplateRenderOutput, ctx: TemplateRenderContext):
text: Tuple[str, str], ctx: TemplateRenderContext output.question_text = render_latex(
) -> Tuple[str, str]: output.question_text, ctx.note_type(), ctx.col()
qtext, atext = text )
output.answer_text = render_latex(output.answer_text, ctx.note_type(), ctx.col())
qtext = render_latex(qtext, ctx.note_type(), ctx.col())
atext = render_latex(atext, ctx.note_type(), ctx.col())
return (qtext, atext)
def render_latex(html: str, model: NoteType, col: anki.storage._Collection,) -> str: def render_latex(html: str, model: NoteType, col: anki.storage._Collection,) -> str:
@ -187,5 +183,4 @@ def _errMsg(type: str, texpath: str) -> Any:
return msg return msg
# setup q/a filter - type ignored due to import cycle hooks.card_did_render.append(on_card_did_render)
hooks.card_did_render.append(on_card_did_render) # type: ignore

View file

@ -267,7 +267,7 @@ and notes.mid = ? and cards.ord = ?""",
f["name"] = name f["name"] = name
return f return f
def fieldMap(self, m: NoteType) -> Dict[str, Tuple[Any, Any]]: def fieldMap(self, m: NoteType) -> Dict[str, Tuple[int, Field]]:
"Mapping of field name -> (ord, field)." "Mapping of field name -> (ord, field)."
return dict((f["name"], (f["ord"], f)) for f in m["flds"]) return dict((f["name"], (f["ord"], f)) for f in m["flds"])

View file

@ -6,7 +6,7 @@ from __future__ import annotations
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional, Tuple
import anki # pylint: disable=unused-import import anki # pylint: disable=unused-import
from anki.models import NoteType from anki.models import Field, NoteType
from anki.utils import ( from anki.utils import (
fieldChecksum, fieldChecksum,
guid64, guid64,
@ -29,7 +29,7 @@ class Note:
fields: List[str] fields: List[str]
flags: int flags: int
data: str data: str
_fmap: Dict[str, Tuple[Any, Any]] _fmap: Dict[str, Tuple[int, Field]]
scm: int scm: int
def __init__( def __init__(

View file

@ -34,30 +34,14 @@ from typing import Any, Dict, List, Optional, Tuple
import anki import anki
from anki import hooks from anki import hooks
from anki.cards import Card
from anki.consts import HELP_SITE
from anki.lang import _
from anki.models import NoteType from anki.models import NoteType
from anki.notes import Note
from anki.rsbackend import TemplateReplacementList from anki.rsbackend import TemplateReplacementList
from anki.sound import AVTag from anki.sound import AVTag
QAData = Tuple[
# Card ID this QA comes from. Corresponds to 'cid' column.
int,
# Note ID this QA comes from. Corresponds to 'nid' column.
int,
# ID of the model (i.e., NoteType) for this QA's note. Corresponds to 'mid' column.
int,
# Deck ID. Corresponds to 'did' column.
int,
# Index of the card template (within the NoteType) this QA was built
# from. Corresponds to 'ord' column.
int,
# Tags, separated by space. Corresponds to 'tags' column.
str,
# Corresponds to 'flds' column. TODO: document.
str,
# Corresponds to 'cardFlags' column. TODO: document
int,
]
class TemplateRenderContext: class TemplateRenderContext:
"""Holds information for the duration of one card render. """Holds information for the duration of one card render.
@ -66,15 +50,20 @@ class TemplateRenderContext:
using the _private fields directly.""" using the _private fields directly."""
def __init__( def __init__(
self, col: anki.storage._Collection, qadata: QAData, fields: Dict[str, str] self,
col: anki.storage._Collection,
card: Card,
note: Note,
fields: Dict[str, str],
qfmt: str,
afmt: str,
) -> None: ) -> None:
self._col = col self._col = col
self._qadata = qadata self._card = card
self._note = note
self._fields = fields self._fields = fields
self._qfmt = qfmt
self._note_type: Optional[NoteType] = None self._afmt = afmt
self._card: Optional[anki.cards.Card] = None
self._note: Optional[anki.notes.Note] = None
# if you need to store extra state to share amongst rendering # if you need to store extra state to share amongst rendering
# hooks, you can insert it into this dictionary # hooks, you can insert it into this dictionary
@ -86,46 +75,29 @@ class TemplateRenderContext:
def fields(self) -> Dict[str, str]: def fields(self) -> Dict[str, str]:
return self._fields return self._fields
def card_id(self) -> int: def card(self) -> Card:
return self._qadata[0] """Returns the card being rendered.
def note_id(self) -> int:
return self._qadata[1]
def deck_id(self) -> int:
return self._qadata[3]
def card_ord(self) -> int:
return self._qadata[4]
def card(self) -> Optional[anki.cards.Card]:
"""Returns the card being rendered. Will return None in the add screen.
Be careful not to call .q() or .a() on the card, or you'll create an Be careful not to call .q() or .a() on the card, or you'll create an
infinite loop.""" infinite loop."""
if not self._card:
try:
self._card = self.col().getCard(self.card_id())
except:
return None
return self._card return self._card
def note(self) -> anki.notes.Note: def note(self) -> Note:
if not self._note:
self._note = self.col().getNote(self.note_id())
return self._note return self._note
def note_type(self) -> NoteType: def note_type(self) -> NoteType:
if not self._note_type: return self.card().note_type()
self._note_type = self.col().models.get(self._qadata[2])
return self._note_type def qfmt(self) -> str:
return self._qfmt
def afmt(self) -> str:
return self._afmt
@dataclass @dataclass
class RenderOutput: class TemplateRenderOutput:
"Stores the rendered templates and extracted AV tags."
question_text: str question_text: str
answer_text: str answer_text: str
question_av_tags: List[AVTag] question_av_tags: List[AVTag]
@ -133,12 +105,71 @@ class RenderOutput:
def render_card( def render_card(
col: anki.storage._Collection, qfmt: str, afmt: str, ctx: TemplateRenderContext col: anki.storage._Collection, card: Card, note: Note, browser: bool
) -> RenderOutput: ) -> TemplateRenderOutput:
"Render a card."
# collect data
fields = fields_for_rendering(col, card, note)
qfmt, afmt = templates_for_card(card, browser)
ctx = TemplateRenderContext(
col=col, card=card, note=note, fields=fields, qfmt=qfmt, afmt=afmt
)
# render
try:
output = render_card_from_context(ctx)
except anki.rsbackend.BackendException as e:
errmsg = _("Card template has a problem:") + f"<br>{e}"
output = TemplateRenderOutput(
question_text=errmsg,
answer_text=errmsg,
question_av_tags=[],
answer_av_tags=[],
)
if not output.question_text.strip():
msg = _("The front of this card is blank.")
help = _("More info")
msg += f"<a href='{HELP_SITE}'>{help}</a>"
output.question_text = msg
hooks.card_did_render(output, ctx)
return output
def templates_for_card(card: Card, browser: bool) -> Tuple[str, str]:
template = card.template()
q, a = browser and ("bqfmt", "bafmt") or ("qfmt", "afmt")
return template.get(q), template.get(a) # type: ignore
def fields_for_rendering(col: anki.storage._Collection, card: Card, note: Note):
# fields from note
fields = dict(note.items())
# add special fields
fields["Tags"] = note.stringTags().strip()
fields["Type"] = card.note_type()["name"]
fields["Deck"] = col.decks.name(card.did)
fields["Subdeck"] = fields["Deck"].split("::")[-1]
fields["Card"] = card.template()["name"] # type: ignore
flag = card.userFlag()
fields["CardFlag"] = flag and f"flag{flag}" or ""
fields["c%d" % (card.ord + 1)] = "1"
return fields
def render_card_from_context(ctx: TemplateRenderContext) -> TemplateRenderOutput:
"""Renders the provided templates, returning rendered output. """Renders the provided templates, returning rendered output.
Will raise if the template is invalid.""" Will raise if the template is invalid."""
(qnodes, anodes) = col.backend.render_card(qfmt, afmt, ctx.fields(), ctx.card_ord()) col = ctx.col()
(qnodes, anodes) = col.backend.render_card(
ctx.qfmt(), ctx.afmt(), ctx.fields(), ctx.card().ord
)
qtext = apply_custom_filters(qnodes, ctx, front_side=None) qtext = apply_custom_filters(qnodes, ctx, front_side=None)
qtext, q_avtags = col.backend.extract_av_tags(qtext, True) qtext, q_avtags = col.backend.extract_av_tags(qtext, True)
@ -146,7 +177,7 @@ def render_card(
atext = apply_custom_filters(anodes, ctx, front_side=qtext) atext = apply_custom_filters(anodes, ctx, front_side=qtext)
atext, a_avtags = col.backend.extract_av_tags(atext, False) atext, a_avtags = col.backend.extract_av_tags(atext, False)
return RenderOutput( return TemplateRenderOutput(
question_text=qtext, question_text=qtext,
answer_text=atext, answer_text=atext,
question_av_tags=q_avtags, question_av_tags=q_avtags,

View file

@ -59,7 +59,7 @@ hooks = [
"field_text: str", "field_text: str",
"field_name: str", "field_name: str",
"filter_name: str", "filter_name: str",
"ctx: TemplateRenderContext", "ctx: anki.template.TemplateRenderContext",
], ],
return_type="str", return_type="str",
doc="""Allows you to define custom {{filters:..}} doc="""Allows you to define custom {{filters:..}}
@ -69,8 +69,10 @@ hooks = [
), ),
Hook( Hook(
name="card_did_render", name="card_did_render",
args=["text: Tuple[str, str]", "ctx: TemplateRenderContext",], args=[
return_type="Tuple[str, str]", "output: anki.template.TemplateRenderOutput",
"ctx: anki.template.TemplateRenderContext",
],
doc="Can modify the resulting text after rendering completes.", doc="Can modify the resulting text after rendering completes.",
), ),
] ]