mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
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:
parent
836213e587
commit
f900f24f60
8 changed files with 180 additions and 240 deletions
|
@ -5,11 +5,12 @@ from __future__ import annotations
|
|||
|
||||
import pprint
|
||||
import time
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
from typing import List, Optional
|
||||
|
||||
import anki # pylint: disable=unused-import
|
||||
from anki import hooks
|
||||
from anki.consts import *
|
||||
from anki.models import NoteType, Template
|
||||
from anki.notes import Note
|
||||
from anki.sound import AVTag
|
||||
from anki.utils import intTime, joinFields, timestampID
|
||||
|
@ -27,7 +28,6 @@ from anki.utils import intTime, joinFields, timestampID
|
|||
|
||||
|
||||
class Card:
|
||||
_qa: Optional[Dict[str, Union[str, int, List[AVTag]]]]
|
||||
_note: Optional[Note]
|
||||
timerStarted: Optional[float]
|
||||
lastIvl: int
|
||||
|
@ -38,7 +38,7 @@ class Card:
|
|||
) -> None:
|
||||
self.col = col
|
||||
self.timerStarted = None
|
||||
self._qa = None
|
||||
self._render_output: Optional[anki.template.TemplateRenderOutput] = None
|
||||
self._note = None
|
||||
if id:
|
||||
self.id = id
|
||||
|
@ -81,7 +81,7 @@ class Card:
|
|||
self.flags,
|
||||
self.data,
|
||||
) = self.col.db.first("select * from cards where id = ?", self.id)
|
||||
self._qa = None
|
||||
self._render_output = None
|
||||
self._note = None
|
||||
|
||||
def flush(self) -> None:
|
||||
|
@ -144,54 +144,44 @@ lapses=?, left=?, odue=?, odid=?, did=? where id = ?""",
|
|||
)
|
||||
self.col.log(self)
|
||||
|
||||
def q(self, reload: bool = False, browser: bool = False) -> str:
|
||||
return self.css() + self._getQA(reload, browser)["q"]
|
||||
def question(self, reload: bool = False, browser: bool = False) -> str:
|
||||
return self.css() + self.render_output(reload, browser).question_text
|
||||
|
||||
def a(self) -> str:
|
||||
return self.css() + self._getQA()["a"]
|
||||
def answer(self) -> str:
|
||||
return self.css() + self.render_output().answer_text
|
||||
|
||||
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]:
|
||||
return self._qa["a_av_tags"] # type: ignore
|
||||
return self.render_output().answer_av_tags
|
||||
|
||||
def css(self) -> str:
|
||||
return "<style>%s</style>" % self.model()["css"]
|
||||
|
||||
def _getQA(self, reload: bool = False, browser: bool = False) -> Any:
|
||||
if not self._qa or reload:
|
||||
f = self.note(reload)
|
||||
m = self.model()
|
||||
t = self.template()
|
||||
if browser:
|
||||
args = [t.get("bqfmt"), t.get("bafmt")]
|
||||
else:
|
||||
args = []
|
||||
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 render_output(
|
||||
self, reload: bool = False, browser: bool = False
|
||||
) -> anki.template.TemplateRenderOutput:
|
||||
if not self._render_output or reload:
|
||||
note = self.note(reload)
|
||||
self._render_output = anki.template.render_card(
|
||||
self.col, self, note, browser
|
||||
)
|
||||
return self._render_output
|
||||
|
||||
def note(self, reload: bool = False) -> Any:
|
||||
def note(self, reload: bool = False) -> Note:
|
||||
if not self._note or reload:
|
||||
self._note = self.col.getNote(self.nid)
|
||||
return self._note
|
||||
|
||||
def model(self) -> Any:
|
||||
def note_type(self) -> NoteType:
|
||||
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()
|
||||
if m["type"] == MODEL_STD:
|
||||
return self.model()["tmpls"][self.ord]
|
||||
|
@ -201,16 +191,16 @@ lapses=?, left=?, odue=?, odid=?, did=? where id = ?""",
|
|||
def startTimer(self) -> None:
|
||||
self.timerStarted = time.time()
|
||||
|
||||
def timeLimit(self) -> Any:
|
||||
def timeLimit(self) -> int:
|
||||
"Time limit for answering in milliseconds."
|
||||
conf = self.col.decks.confForDid(self.odid or self.did)
|
||||
return conf["maxTaken"] * 1000
|
||||
|
||||
def shouldShowTimer(self) -> Any:
|
||||
def shouldShowTimer(self) -> bool:
|
||||
conf = self.col.decks.confForDid(self.odid or self.did)
|
||||
return conf["timer"]
|
||||
|
||||
def timeTaken(self) -> Any:
|
||||
def timeTaken(self) -> int:
|
||||
"Time taken to answer card, in integer MS."
|
||||
total = int((time.time() - self.timerStarted) * 1000)
|
||||
return min(total, self.timeLimit())
|
||||
|
@ -225,12 +215,12 @@ lapses=?, left=?, odue=?, odid=?, did=? where id = ?""",
|
|||
d = dict(self.__dict__)
|
||||
# remove non-useful elements
|
||||
del d["_note"]
|
||||
del d["_qa"]
|
||||
del d["_render_output"]
|
||||
del d["col"]
|
||||
del d["timerStarted"]
|
||||
return pprint.pformat(d, width=300)
|
||||
|
||||
def userFlag(self) -> Any:
|
||||
def userFlag(self) -> int:
|
||||
return self.flags & 0b111
|
||||
|
||||
def setUserFlag(self, flag: int) -> None:
|
||||
|
|
|
@ -22,7 +22,6 @@ from anki.consts import *
|
|||
from anki.db import DB
|
||||
from anki.decks import DeckManager
|
||||
from anki.errors import AnkiError
|
||||
from anki.hooks import runFilter
|
||||
from anki.lang import _, ngettext
|
||||
from anki.media import MediaManager
|
||||
from anki.models import ModelManager, NoteType, Template
|
||||
|
@ -30,9 +29,7 @@ from anki.notes import Note
|
|||
from anki.rsbackend import RustBackend
|
||||
from anki.sched import Scheduler as V1Scheduler
|
||||
from anki.schedv2 import Scheduler as V2Scheduler
|
||||
from anki.sound import AVTag
|
||||
from anki.tags import TagManager
|
||||
from anki.template import QAData, RenderOutput, TemplateRenderContext, render_card
|
||||
from anki.utils import (
|
||||
devMode,
|
||||
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
|
||||
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
|
||||
##########################################################################
|
||||
|
||||
|
|
|
@ -18,7 +18,6 @@ import decorator
|
|||
|
||||
import anki
|
||||
from anki.cards import Card
|
||||
from anki.template import TemplateRenderContext
|
||||
|
||||
# New hook/filter handling
|
||||
##############################################################################
|
||||
|
@ -54,39 +53,60 @@ class _CardDidLeechHook:
|
|||
card_did_leech = _CardDidLeechHook()
|
||||
|
||||
|
||||
class _CardDidRenderFilter:
|
||||
class _CardDidRenderHook:
|
||||
"""Can modify the resulting text after rendering completes."""
|
||||
|
||||
_hooks: List[
|
||||
Callable[[Tuple[str, str], TemplateRenderContext], Tuple[str, str]]
|
||||
Callable[
|
||||
[
|
||||
"anki.template.TemplateRenderOutput",
|
||||
"anki.template.TemplateRenderContext",
|
||||
],
|
||||
None,
|
||||
]
|
||||
] = []
|
||||
|
||||
def append(
|
||||
self, cb: Callable[[Tuple[str, str], TemplateRenderContext], Tuple[str, str]]
|
||||
self,
|
||||
cb: Callable[
|
||||
[
|
||||
"anki.template.TemplateRenderOutput",
|
||||
"anki.template.TemplateRenderContext",
|
||||
],
|
||||
None,
|
||||
],
|
||||
) -> None:
|
||||
"""(text: Tuple[str, str], ctx: TemplateRenderContext)"""
|
||||
"""(output: anki.template.TemplateRenderOutput, ctx: anki.template.TemplateRenderContext)"""
|
||||
self._hooks.append(cb)
|
||||
|
||||
def remove(
|
||||
self, cb: Callable[[Tuple[str, str], TemplateRenderContext], Tuple[str, str]]
|
||||
self,
|
||||
cb: Callable[
|
||||
[
|
||||
"anki.template.TemplateRenderOutput",
|
||||
"anki.template.TemplateRenderContext",
|
||||
],
|
||||
None,
|
||||
],
|
||||
) -> None:
|
||||
if cb in self._hooks:
|
||||
self._hooks.remove(cb)
|
||||
|
||||
def __call__(
|
||||
self, text: Tuple[str, str], ctx: TemplateRenderContext
|
||||
) -> Tuple[str, str]:
|
||||
for filter in self._hooks:
|
||||
self,
|
||||
output: anki.template.TemplateRenderOutput,
|
||||
ctx: anki.template.TemplateRenderContext,
|
||||
) -> None:
|
||||
for hook in self._hooks:
|
||||
try:
|
||||
text = filter(text, ctx)
|
||||
hook(output, ctx)
|
||||
except:
|
||||
# if the hook fails, remove it
|
||||
self._hooks.remove(filter)
|
||||
self._hooks.remove(hook)
|
||||
raise
|
||||
return text
|
||||
|
||||
|
||||
card_did_render = _CardDidRenderFilter()
|
||||
card_did_render = _CardDidRenderHook()
|
||||
|
||||
|
||||
class _CardOdueWasInvalidHook:
|
||||
|
@ -171,13 +191,19 @@ class _FieldFilterFilter:
|
|||
Your add-on can check filter_name to decide whether it should modify
|
||||
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:
|
||||
"""(field_text: str, field_name: str, filter_name: str, ctx: TemplateRenderContext)"""
|
||||
def append(
|
||||
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)
|
||||
|
||||
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:
|
||||
self._hooks.remove(cb)
|
||||
|
||||
|
@ -186,7 +212,7 @@ class _FieldFilterFilter:
|
|||
field_text: str,
|
||||
field_name: str,
|
||||
filter_name: str,
|
||||
ctx: TemplateRenderContext,
|
||||
ctx: anki.template.TemplateRenderContext,
|
||||
) -> str:
|
||||
for filter in self._hooks:
|
||||
try:
|
||||
|
|
|
@ -7,13 +7,13 @@ import html
|
|||
import os
|
||||
import re
|
||||
import shutil
|
||||
from typing import Any, Optional, Tuple
|
||||
from typing import Any, Optional
|
||||
|
||||
import anki
|
||||
from anki import hooks
|
||||
from anki.lang import _
|
||||
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
|
||||
|
||||
pngCommands = [
|
||||
|
@ -48,15 +48,11 @@ def stripLatex(text) -> Any:
|
|||
return text
|
||||
|
||||
|
||||
def on_card_did_render(
|
||||
text: Tuple[str, str], ctx: TemplateRenderContext
|
||||
) -> Tuple[str, str]:
|
||||
qtext, atext = text
|
||||
|
||||
qtext = render_latex(qtext, ctx.note_type(), ctx.col())
|
||||
atext = render_latex(atext, ctx.note_type(), ctx.col())
|
||||
|
||||
return (qtext, atext)
|
||||
def on_card_did_render(output: TemplateRenderOutput, ctx: TemplateRenderContext):
|
||||
output.question_text = render_latex(
|
||||
output.question_text, ctx.note_type(), ctx.col()
|
||||
)
|
||||
output.answer_text = render_latex(output.answer_text, ctx.note_type(), ctx.col())
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
# setup q/a filter - type ignored due to import cycle
|
||||
hooks.card_did_render.append(on_card_did_render) # type: ignore
|
||||
hooks.card_did_render.append(on_card_did_render)
|
||||
|
|
|
@ -267,7 +267,7 @@ and notes.mid = ? and cards.ord = ?""",
|
|||
f["name"] = name
|
||||
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)."
|
||||
return dict((f["name"], (f["ord"], f)) for f in m["flds"])
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ from __future__ import annotations
|
|||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import anki # pylint: disable=unused-import
|
||||
from anki.models import NoteType
|
||||
from anki.models import Field, NoteType
|
||||
from anki.utils import (
|
||||
fieldChecksum,
|
||||
guid64,
|
||||
|
@ -29,7 +29,7 @@ class Note:
|
|||
fields: List[str]
|
||||
flags: int
|
||||
data: str
|
||||
_fmap: Dict[str, Tuple[Any, Any]]
|
||||
_fmap: Dict[str, Tuple[int, Field]]
|
||||
scm: int
|
||||
|
||||
def __init__(
|
||||
|
|
|
@ -34,30 +34,14 @@ from typing import Any, Dict, List, Optional, Tuple
|
|||
|
||||
import anki
|
||||
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.notes import Note
|
||||
from anki.rsbackend import TemplateReplacementList
|
||||
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:
|
||||
"""Holds information for the duration of one card render.
|
||||
|
@ -66,15 +50,20 @@ class TemplateRenderContext:
|
|||
using the _private fields directly."""
|
||||
|
||||
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:
|
||||
self._col = col
|
||||
self._qadata = qadata
|
||||
self._card = card
|
||||
self._note = note
|
||||
self._fields = fields
|
||||
|
||||
self._note_type: Optional[NoteType] = None
|
||||
self._card: Optional[anki.cards.Card] = None
|
||||
self._note: Optional[anki.notes.Note] = None
|
||||
self._qfmt = qfmt
|
||||
self._afmt = afmt
|
||||
|
||||
# if you need to store extra state to share amongst rendering
|
||||
# hooks, you can insert it into this dictionary
|
||||
|
@ -86,46 +75,29 @@ class TemplateRenderContext:
|
|||
def fields(self) -> Dict[str, str]:
|
||||
return self._fields
|
||||
|
||||
def card_id(self) -> int:
|
||||
return self._qadata[0]
|
||||
|
||||
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.
|
||||
def card(self) -> Card:
|
||||
"""Returns the card being rendered.
|
||||
|
||||
Be careful not to call .q() or .a() on the card, or you'll create an
|
||||
infinite loop."""
|
||||
if not self._card:
|
||||
try:
|
||||
self._card = self.col().getCard(self.card_id())
|
||||
except:
|
||||
return None
|
||||
|
||||
return self._card
|
||||
|
||||
def note(self) -> anki.notes.Note:
|
||||
if not self._note:
|
||||
self._note = self.col().getNote(self.note_id())
|
||||
|
||||
def note(self) -> Note:
|
||||
return self._note
|
||||
|
||||
def note_type(self) -> NoteType:
|
||||
if not self._note_type:
|
||||
self._note_type = self.col().models.get(self._qadata[2])
|
||||
return self.card().note_type()
|
||||
|
||||
return self._note_type
|
||||
def qfmt(self) -> str:
|
||||
return self._qfmt
|
||||
|
||||
def afmt(self) -> str:
|
||||
return self._afmt
|
||||
|
||||
|
||||
@dataclass
|
||||
class RenderOutput:
|
||||
class TemplateRenderOutput:
|
||||
"Stores the rendered templates and extracted AV tags."
|
||||
question_text: str
|
||||
answer_text: str
|
||||
question_av_tags: List[AVTag]
|
||||
|
@ -133,12 +105,71 @@ class RenderOutput:
|
|||
|
||||
|
||||
def render_card(
|
||||
col: anki.storage._Collection, qfmt: str, afmt: str, ctx: TemplateRenderContext
|
||||
) -> RenderOutput:
|
||||
col: anki.storage._Collection, card: Card, note: Note, browser: bool
|
||||
) -> 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.
|
||||
|
||||
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, 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, a_avtags = col.backend.extract_av_tags(atext, False)
|
||||
|
||||
return RenderOutput(
|
||||
return TemplateRenderOutput(
|
||||
question_text=qtext,
|
||||
answer_text=atext,
|
||||
question_av_tags=q_avtags,
|
||||
|
|
|
@ -59,7 +59,7 @@ hooks = [
|
|||
"field_text: str",
|
||||
"field_name: str",
|
||||
"filter_name: str",
|
||||
"ctx: TemplateRenderContext",
|
||||
"ctx: anki.template.TemplateRenderContext",
|
||||
],
|
||||
return_type="str",
|
||||
doc="""Allows you to define custom {{filters:..}}
|
||||
|
@ -69,8 +69,10 @@ hooks = [
|
|||
),
|
||||
Hook(
|
||||
name="card_did_render",
|
||||
args=["text: Tuple[str, str]", "ctx: TemplateRenderContext",],
|
||||
return_type="Tuple[str, str]",
|
||||
args=[
|
||||
"output: anki.template.TemplateRenderOutput",
|
||||
"ctx: anki.template.TemplateRenderContext",
|
||||
],
|
||||
doc="Can modify the resulting text after rendering completes.",
|
||||
),
|
||||
]
|
||||
|
|
Loading…
Reference in a new issue