add "fill empty" checkbox

This commit is contained in:
Damien Elmes 2020-05-14 20:58:45 +10:00
parent 9a222e620a
commit 782911471b
11 changed files with 198 additions and 99 deletions

View file

@ -275,6 +275,7 @@ message RenderUncommittedCardIn {
Note note = 1; Note note = 1;
uint32 card_ord = 2; uint32 card_ord = 2;
bytes template = 3; bytes template = 3;
bool fill_empty = 4;
} }
message RenderCardOut { message RenderCardOut {

View file

@ -313,13 +313,16 @@ class RustBackend:
return PartiallyRenderedCard(qnodes, anodes) return PartiallyRenderedCard(qnodes, anodes)
def render_uncommitted_card( def render_uncommitted_card(
self, note: BackendNote, card_ord: int, template: Dict self, note: BackendNote, card_ord: int, template: Dict, fill_empty: bool
) -> PartiallyRenderedCard: ) -> PartiallyRenderedCard:
template_json = orjson.dumps(template) template_json = orjson.dumps(template)
out = self._run_command( out = self._run_command(
pb.BackendInput( pb.BackendInput(
render_uncommitted_card=pb.RenderUncommittedCardIn( render_uncommitted_card=pb.RenderUncommittedCardIn(
note=note, template=template_json, card_ord=card_ord note=note,
template=template_json,
card_ord=card_ord,
fill_empty=fill_empty,
) )
) )
).render_uncommitted_card ).render_uncommitted_card

View file

@ -56,10 +56,20 @@ class TemplateRenderContext:
@classmethod @classmethod
def from_card_layout( def from_card_layout(
cls, note: Note, card: Card, notetype: NoteType, template: Dict cls,
note: Note,
card: Card,
notetype: NoteType,
template: Dict,
fill_empty: bool,
) -> TemplateRenderContext: ) -> TemplateRenderContext:
return TemplateRenderContext( return TemplateRenderContext(
note.col, card, note, notetype=notetype, template=template note.col,
card,
note,
notetype=notetype,
template=template,
fill_empty=fill_empty,
) )
def __init__( def __init__(
@ -70,12 +80,14 @@ class TemplateRenderContext:
browser: bool = False, browser: bool = False,
notetype: NoteType = None, notetype: NoteType = None,
template: Optional[Dict] = None, template: Optional[Dict] = None,
fill_empty: bool = False,
) -> None: ) -> None:
self._col = col.weakref() self._col = col.weakref()
self._card = card self._card = card
self._note = note self._note = note
self._browser = browser self._browser = browser
self._template = template self._template = template
self._fill_empty = fill_empty
if not notetype: if not notetype:
self._note_type = note.model() self._note_type = note.model()
else: else:
@ -148,7 +160,10 @@ class TemplateRenderContext:
if self._template: if self._template:
# card layout screen # card layout screen
return self._col.backend.render_uncommitted_card( return self._col.backend.render_uncommitted_card(
self._note.to_backend_note(), self._card.ord, self._template self._note.to_backend_note(),
self._card.ord,
self._template,
self._fill_empty,
) )
else: else:
# existing card (eg study mode) # existing card (eg study mode)

View file

@ -14,26 +14,26 @@ from anki.lang import _, ngettext
from anki.notes import Note from anki.notes import Note
from anki.rsbackend import TemplateError from anki.rsbackend import TemplateError
from anki.template import TemplateRenderContext from anki.template import TemplateRenderContext
from anki.utils import isMac, isWin
from aqt import AnkiQt, gui_hooks from aqt import AnkiQt, gui_hooks
from aqt.qt import * from aqt.qt import *
from aqt.sound import av_player, play_clicked_audio from aqt.sound import av_player, play_clicked_audio
from aqt.theme import theme_manager from aqt.theme import theme_manager
from aqt.utils import ( from aqt.utils import (
TR,
askUser, askUser,
downArrow, downArrow,
getOnlyText, getOnlyText,
openHelp, openHelp,
restoreGeom, restoreGeom,
saveGeom, saveGeom,
shortcut,
showInfo, showInfo,
showWarning, showWarning,
tooltip, tooltip,
TR, tr, shortcut tr,
) )
from aqt.webview import AnkiWebView from aqt.webview import AnkiWebView
# fixme: previewing with empty fields
# fixme: deck name on new cards # fixme: deck name on new cards
# fixme: card count when removing # fixme: card count when removing
# fixme: change tracking and tooltip in fields # fixme: change tracking and tooltip in fields
@ -42,7 +42,12 @@ from aqt.webview import AnkiWebView
class CardLayout(QDialog): class CardLayout(QDialog):
def __init__( def __init__(
self, mw: AnkiQt, note: Note, ord=0, parent: Optional[QWidget] = None, self,
mw: AnkiQt,
note: Note,
ord=0,
parent: Optional[QWidget] = None,
fill_empty: bool = False,
): ):
QDialog.__init__(self, parent or mw, Qt.Window) QDialog.__init__(self, parent or mw, Qt.Window)
mw.setupDialogGC(self) mw.setupDialogGC(self)
@ -53,6 +58,7 @@ class CardLayout(QDialog):
self.mm = self.mw.col.models self.mm = self.mw.col.models
self.model = note.model() self.model = note.model()
self.templates = self.model["tmpls"] self.templates = self.model["tmpls"]
self._want_fill_empty_on = fill_empty
self.mm._remove_from_cache(self.model["id"]) self.mm._remove_from_cache(self.model["id"])
self.mw.checkpoint(_("Card Types")) self.mw.checkpoint(_("Card Types"))
self.changed = False self.changed = False
@ -160,17 +166,20 @@ class CardLayout(QDialog):
QShortcut( QShortcut(
QKeySequence("Ctrl+1"), QKeySequence("Ctrl+1"),
self, self,
activated=lambda: self.tform.front_button.setChecked(True)) activated=lambda: self.tform.front_button.setChecked(True),
) # type: ignore
QShortcut( QShortcut(
QKeySequence("Ctrl+2"), QKeySequence("Ctrl+2"),
self, self,
activated=lambda: self.tform.back_button.setChecked(True)) activated=lambda: self.tform.back_button.setChecked(True),
) # type: ignore
QShortcut( QShortcut(
QKeySequence("Ctrl+3"), QKeySequence("Ctrl+3"),
self, self,
activated=lambda: self.tform.style_button.setChecked(True)) activated=lambda: self.tform.style_button.setChecked(True),
) # type: ignore
# Main area # Main area setup
########################################################################## ##########################################################################
def setupMainArea(self): def setupMainArea(self):
@ -179,56 +188,42 @@ class CardLayout(QDialog):
l.setContentsMargins(0, 0, 0, 0) l.setContentsMargins(0, 0, 0, 0)
l.setSpacing(3) l.setSpacing(3)
left = QWidget() left = QWidget()
# template area
tform = self.tform = aqt.forms.template.Ui_Form() tform = self.tform = aqt.forms.template.Ui_Form()
tform.setupUi(left) tform.setupUi(left)
qconnect(tform.edit_area.textChanged, self.write_edits_to_template_and_redraw)
tform.front_button.setText(tr(TR.CARD_TEMPLATES_FRONT_TEMPLATE))
qconnect(tform.front_button.toggled, self.on_editor_toggled)
tform.back_button.setText(tr(TR.CARD_TEMPLATES_BACK_TEMPLATE))
qconnect(tform.back_button.toggled, self.on_editor_toggled)
tform.style_button.setText(tr(TR.CARD_TEMPLATES_TEMPLATE_STYLING))
qconnect(tform.style_button.toggled, self.on_editor_toggled)
tform.groupBox.setTitle(tr(TR.CARD_TEMPLATES_TEMPLATE_BOX))
cnt = self.mw.col.models.useCount(self.model)
self.tform.changes_affect_label.setText(self.col.tr(
TR.CARD_TEMPLATES_CHANGES_WILL_AFFECT_NOTES, count=cnt))
l.addWidget(left, 5) l.addWidget(left, 5)
self.setup_edit_area()
widg = tform.search_edit
widg.setPlaceholderText("Search")
qconnect(widg.textChanged, self.on_search_changed)
qconnect(widg.returnPressed, self.on_search_next)
# preview area
right = QWidget() right = QWidget()
self.pform: Any = aqt.forms.preview.Ui_Form() self.pform = aqt.forms.preview.Ui_Form()
pform = self.pform pform = self.pform
pform.setupUi(right) pform.setupUi(right)
pform.preview_front.setText(tr(TR.CARD_TEMPLATES_FRONT_PREVIEW)) pform.preview_front.setText(tr(TR.CARD_TEMPLATES_FRONT_PREVIEW))
pform.preview_back.setText(tr(TR.CARD_TEMPLATES_BACK_PREVIEW)) pform.preview_back.setText(tr(TR.CARD_TEMPLATES_BACK_PREVIEW))
pform.preview_box.setTitle(tr(TR.CARD_TEMPLATES_PREVIEW_BOX)) pform.preview_box.setTitle(tr(TR.CARD_TEMPLATES_PREVIEW_BOX))
if self._isCloze(): self.setup_edit_area()
nums = self.note.cloze_numbers_in_fields() self.setup_preview()
if self.ord + 1 not in nums:
# current card is empty
nums.append(self.ord + 1)
self.cloze_numbers = sorted(nums)
self.setup_cloze_number_box()
else:
self.cloze_numbers = []
self.pform.cloze_number_combo.setHidden(True)
self.setupWebviews()
l.addWidget(right, 5) l.addWidget(right, 5)
w.setLayout(l) w.setLayout(l)
def setup_edit_area(self): def setup_edit_area(self):
tform = self.tform
tform.front_button.setText(tr(TR.CARD_TEMPLATES_FRONT_TEMPLATE))
tform.back_button.setText(tr(TR.CARD_TEMPLATES_BACK_TEMPLATE))
tform.style_button.setText(tr(TR.CARD_TEMPLATES_TEMPLATE_STYLING))
tform.groupBox.setTitle(tr(TR.CARD_TEMPLATES_TEMPLATE_BOX))
cnt = self.mw.col.models.useCount(self.model)
self.tform.changes_affect_label.setText(
self.col.tr(TR.CARD_TEMPLATES_CHANGES_WILL_AFFECT_NOTES, count=cnt)
)
qconnect(tform.edit_area.textChanged, self.write_edits_to_template_and_redraw)
qconnect(tform.front_button.toggled, self.on_editor_toggled)
qconnect(tform.back_button.toggled, self.on_editor_toggled)
qconnect(tform.style_button.toggled, self.on_editor_toggled)
self.current_editor_index = 0 self.current_editor_index = 0
self.tform.edit_area.setAcceptRichText(False) self.tform.edit_area.setAcceptRichText(False)
if qtminor < 10: if qtminor < 10:
@ -237,6 +232,11 @@ class CardLayout(QDialog):
tab_width = self.fontMetrics().width(" " * 4) tab_width = self.fontMetrics().width(" " * 4)
self.tform.edit_area.setTabStopDistance(tab_width) self.tform.edit_area.setTabStopDistance(tab_width)
widg = tform.search_edit
widg.setPlaceholderText("Search")
qconnect(widg.textChanged, self.on_search_changed)
qconnect(widg.returnPressed, self.on_search_next)
def setup_cloze_number_box(self): def setup_cloze_number_box(self):
names = (_("Cloze %d") % n for n in self.cloze_numbers) names = (_("Cloze %d") % n for n in self.cloze_numbers)
self.pform.cloze_number_combo.addItems(names) self.pform.cloze_number_combo.addItems(names)
@ -282,14 +282,20 @@ class CardLayout(QDialog):
text = self.tform.search_edit.text() text = self.tform.search_edit.text()
self.on_search_changed(text) self.on_search_changed(text)
def setupWebviews(self): def setup_preview(self):
pform = self.pform pform = self.pform
pform.frontWeb = AnkiWebView(title="card layout") self.preview_web = AnkiWebView(title="card layout")
pform.verticalLayout.addWidget(pform.frontWeb) pform.verticalLayout.addWidget(self.preview_web)
pform.verticalLayout.setStretch(1, 99) pform.verticalLayout.setStretch(1, 99)
pform.preview_front.isChecked() pform.preview_front.isChecked()
qconnect(pform.preview_front.toggled, self.on_preview_toggled) qconnect(pform.preview_front.toggled, self.on_preview_toggled)
qconnect(pform.preview_back.toggled, self.on_preview_toggled) qconnect(pform.preview_back.toggled, self.on_preview_toggled)
if self._want_fill_empty_on:
pform.fill_empty.setChecked(True)
qconnect(pform.fill_empty.toggled, self.on_preview_toggled)
if not self.note_has_empty_field():
pform.fill_empty.setHidden(True)
pform.fill_empty.setText(tr(TR.CARD_TEMPLATES_FILL_EMPTY))
jsinc = [ jsinc = [
"jquery.js", "jquery.js",
"browsersel.js", "browsersel.js",
@ -297,10 +303,21 @@ class CardLayout(QDialog):
"mathjax/MathJax.js", "mathjax/MathJax.js",
"reviewer.js", "reviewer.js",
] ]
pform.frontWeb.stdHtml( self.preview_web.stdHtml(
self.mw.reviewer.revHtml(), css=["reviewer.css"], js=jsinc, context=self, self.mw.reviewer.revHtml(), css=["reviewer.css"], js=jsinc, context=self,
) )
pform.frontWeb.set_bridge_command(self._on_bridge_cmd, self) self.preview_web.set_bridge_command(self._on_bridge_cmd, self)
if self._isCloze():
nums = self.note.cloze_numbers_in_fields()
if self.ord + 1 not in nums:
# current card is empty
nums.append(self.ord + 1)
self.cloze_numbers = sorted(nums)
self.setup_cloze_number_box()
else:
self.cloze_numbers = []
self.pform.cloze_number_combo.setHidden(True)
def on_preview_toggled(self): def on_preview_toggled(self):
self._renderPreview() self._renderPreview()
@ -309,18 +326,12 @@ class CardLayout(QDialog):
if cmd.startswith("play:"): if cmd.startswith("play:"):
play_clicked_audio(cmd, self.rendered_card) play_clicked_audio(cmd, self.rendered_card)
def ephemeral_card_for_rendering(self) -> Card: def note_has_empty_field(self) -> bool:
card = Card(self.col) for field in self.note.fields:
card.ord = self.ord if not field.strip():
template = copy.copy(self.current_template()) # ignores HTML, but this should suffice
# may differ in cloze case return True
template["ord"] = card.ord return False
# this fetches notetype, we should pass it in
output = TemplateRenderContext.from_card_layout(
self.note, card, notetype=self.model, template=template
).render()
card.set_render_output(output)
return card
# Buttons # Buttons
########################################################################## ##########################################################################
@ -383,9 +394,9 @@ class CardLayout(QDialog):
text = self.tform.edit_area.toPlainText() text = self.tform.edit_area.toPlainText()
if self.current_editor_index == 0: if self.current_editor_index == 0:
self.current_template()['qfmt'] = text self.current_template()["qfmt"] = text
elif self.current_editor_index == 1: elif self.current_editor_index == 1:
self.current_template()['afmt'] = text self.current_template()["afmt"] = text
else: else:
self.model["css"] = text self.model["css"] = text
@ -427,9 +438,7 @@ class CardLayout(QDialog):
audio = c.answer_av_tags() audio = c.answer_av_tags()
# use _showAnswer to avoid the longer delay # use _showAnswer to avoid the longer delay
self.pform.frontWeb.eval( self.preview_web.eval("_showAnswer(%s,'%s');" % (json.dumps(text), bodyclass))
"_showAnswer(%s,'%s');" % (json.dumps(text), bodyclass)
)
if c.id not in self.playedAudio: if c.id not in self.playedAudio:
av_player.play_tags(audio) av_player.play_tags(audio)
@ -459,6 +468,22 @@ class CardLayout(QDialog):
repl = answerRepl repl = answerRepl
return re.sub(r"\[\[type:.+?\]\]", repl, txt) return re.sub(r"\[\[type:.+?\]\]", repl, txt)
def ephemeral_card_for_rendering(self) -> Card:
card = Card(self.col)
card.ord = self.ord
template = copy.copy(self.current_template())
# may differ in cloze case
template["ord"] = card.ord
output = TemplateRenderContext.from_card_layout(
self.note,
card,
notetype=self.model,
template=template,
fill_empty=self.pform.fill_empty.isChecked(),
).render()
card.set_render_output(output)
return card
# Card operations # Card operations
###################################################################### ######################################################################
@ -673,9 +698,7 @@ Enter deck to place new %s cards in, or leave blank:"""
row = form.fields.currentIndex().row() row = form.fields.currentIndex().row()
if row >= 0: if row >= 0:
self._addField( self._addField(
fields[row], fields[row], form.font.currentFont().family(), form.size.value(),
form.font.currentFont().family(),
form.size.value(),
) )
def _addField(self, field, font, size): def _addField(self, field, font, size):
@ -720,7 +743,7 @@ Enter deck to place new %s cards in, or leave blank:"""
self.cancelPreviewTimer() self.cancelPreviewTimer()
av_player.stop_and_clear_queue() av_player.stop_and_clear_queue()
saveGeom(self, "CardLayout") saveGeom(self, "CardLayout")
self.pform.frontWeb = None self.preview_web = None
self.model = None self.model = None
self.rendered_card = None self.rendered_card = None
self.mw = None self.mw = None

View file

@ -353,7 +353,11 @@ class Editor:
else: else:
ord = 0 ord = 0
CardLayout( CardLayout(
self.mw, self.note, ord=ord, parent=self.parentWindow, self.mw,
self.note,
ord=ord,
parent=self.parentWindow,
fill_empty=self.addMode,
) )
if isWin: if isWin:
self.parentWindow.activateWindow() self.parentWindow.activateWindow()

View file

@ -1,7 +1,7 @@
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import collections import collections
import re
from operator import itemgetter from operator import itemgetter
from typing import List, Optional from typing import List, Optional
@ -9,6 +9,7 @@ import aqt.clayout
from anki import stdmodels from anki import stdmodels
from anki.lang import _, ngettext from anki.lang import _, ngettext
from anki.models import NoteType from anki.models import NoteType
from anki.notes import Note
from anki.rsbackend import pb from anki.rsbackend import pb
from aqt import AnkiQt, gui_hooks from aqt import AnkiQt, gui_hooks
from aqt.qt import * from aqt.qt import *
@ -160,24 +161,7 @@ class Models(QDialog):
def _tmpNote(self): def _tmpNote(self):
nt = self.current_notetype() nt = self.current_notetype()
self.mm.setCurrent(nt) return Note(self.col, nt)
n = self.col.newNote(forDeck=False)
field_names = list(n.keys())
for name in field_names:
n[name] = "(" + name + ")"
cloze_re = re.compile(r"{{(?:[^}:]*:)*cloze:(?:[^}:]*:)*([^}]+)}}")
q_template = nt["tmpls"][0]["qfmt"]
a_template = nt["tmpls"][0]["afmt"]
used_cloze_fields = []
used_cloze_fields.extend(cloze_re.findall(q_template))
used_cloze_fields.extend(cloze_re.findall(a_template))
for field in used_cloze_fields:
if field in field_names:
n[field] = f"{field}: " + _("This is a {{c1::sample}} cloze deletion.")
return n
def onFields(self): def onFields(self):
from aqt.fields import FieldDialog from aqt.fields import FieldDialog
@ -188,7 +172,7 @@ class Models(QDialog):
from aqt.clayout import CardLayout from aqt.clayout import CardLayout
n = self._tmpNote() n = self._tmpNote()
CardLayout(self.mw, n, ord=0, parent=self) CardLayout(self.mw, n, ord=0, parent=self, fill_empty=True)
# Cleanup # Cleanup
########################################################################## ##########################################################################

View file

@ -64,6 +64,13 @@
</property> </property>
</spacer> </spacer>
</item> </item>
<item>
<widget class="QCheckBox" name="fill_empty">
<property name="text">
<string notr="true">FILL_EMPTY</string>
</property>
</widget>
</item>
<item> <item>
<widget class="QComboBox" name="cloze_number_combo"/> <widget class="QComboBox" name="cloze_number_combo"/>
</item> </item>

View file

@ -11,3 +11,5 @@ card-templates-front-preview = Front Preview
card-templates-back-preview = Back Preview card-templates-back-preview = Back Preview
card-templates-preview-box = Preview card-templates-preview-box = Preview
card-templates-template-box = Template card-templates-template-box = Template
card-templates-sample-cloze = This is a {"{{c1::"}sample{"}}"} cloze deletion.
card-templates-fill-empty = Fill Empty Fields

View file

@ -472,13 +472,14 @@ impl Backend {
) -> Result<pb::RenderCardOut> { ) -> Result<pb::RenderCardOut> {
let schema11: CardTemplateSchema11 = serde_json::from_slice(&input.template)?; let schema11: CardTemplateSchema11 = serde_json::from_slice(&input.template)?;
let template = schema11.into(); let template = schema11.into();
let note = input let mut note = input
.note .note
.ok_or_else(|| AnkiError::invalid_input("missing note"))? .ok_or_else(|| AnkiError::invalid_input("missing note"))?
.into(); .into();
let ord = input.card_ord as u16; let ord = input.card_ord as u16;
let fill_empty = input.fill_empty;
self.with_col(|col| { self.with_col(|col| {
col.render_uncommitted_card(&note, &template, ord) col.render_uncommitted_card(&mut note, &template, ord, fill_empty)
.map(Into::into) .map(Into::into)
}) })
} }

View file

@ -6,8 +6,9 @@ use crate::{
card::{Card, CardID}, card::{Card, CardID},
collection::Collection, collection::Collection,
err::{AnkiError, Result}, err::{AnkiError, Result},
i18n::{I18n, TR},
notes::{Note, NoteID}, notes::{Note, NoteID},
template::{render_card, RenderedNode}, template::{field_is_empty, render_card, ParsedTemplate, RenderedNode},
}; };
use std::{borrow::Cow, collections::HashMap}; use std::{borrow::Cow, collections::HashMap};
@ -41,17 +42,23 @@ impl Collection {
/// Render a card that may not yet have been added. /// Render a card that may not yet have been added.
/// The provided ordinal will be used if the template has not yet been saved. /// The provided ordinal will be used if the template has not yet been saved.
/// If fill_empty is set, note will be mutated.
pub fn render_uncommitted_card( pub fn render_uncommitted_card(
&mut self, &mut self,
note: &Note, note: &mut Note,
template: &CardTemplate, template: &CardTemplate,
card_ord: u16, card_ord: u16,
fill_empty: bool,
) -> Result<RenderCardOutput> { ) -> Result<RenderCardOutput> {
let card = self.existing_or_synthesized_card(note.id, template.ord, card_ord)?; let card = self.existing_or_synthesized_card(note.id, template.ord, card_ord)?;
let nt = self let nt = self
.get_notetype(note.ntid)? .get_notetype(note.ntid)?
.ok_or_else(|| AnkiError::invalid_input("no such notetype"))?; .ok_or_else(|| AnkiError::invalid_input("no such notetype"))?;
if fill_empty {
fill_empty_fields(note, &template.config.q_format, &nt, &self.i18n);
}
self.render_card_inner(note, &card, &nt, template, false) self.render_card_inner(note, &card, &nt, template, false)
} }
@ -145,3 +152,19 @@ fn flag_name(n: u8) -> &'static str {
_ => "", _ => "",
} }
} }
fn fill_empty_fields(note: &mut Note, qfmt: &str, nt: &NoteType, i18n: &I18n) {
if let Ok(tmpl) = ParsedTemplate::from_text(qfmt) {
let cloze_fields = tmpl.cloze_fields();
for (val, field) in note.fields.iter_mut().zip(nt.fields.iter()) {
if field_is_empty(val) {
if cloze_fields.contains(&field.name.as_str()) {
*val = i18n.tr(TR::CardTemplatesSampleCloze).into();
} else {
*val = format!("({})", field.name);
}
}
}
}
}

View file

@ -480,7 +480,7 @@ fn append_str_to_nodes(nodes: &mut Vec<RenderedNode>, text: &str) {
} }
/// True if provided text contains only whitespace and/or empty BR/DIV tags. /// True if provided text contains only whitespace and/or empty BR/DIV tags.
fn field_is_empty(text: &str) -> bool { pub(crate) fn field_is_empty(text: &str) -> bool {
lazy_static! { lazy_static! {
static ref RE: Regex = Regex::new( static ref RE: Regex = Regex::new(
r#"(?xsi) r#"(?xsi)
@ -721,6 +721,42 @@ fn nodes_to_string(buf: &mut String, nodes: &[ParsedNode]) {
} }
} }
// Detecting cloze fields
//----------------------------------------
impl ParsedTemplate {
/// A set of field names with a cloze filter attached.
/// Field names may not be valid.
pub(crate) fn cloze_fields(&self) -> HashSet<&str> {
let mut set = HashSet::new();
find_fields_with_filter(&self.0, &mut set, "cloze");
set
}
}
fn find_fields_with_filter<'a>(
nodes: &'a [ParsedNode],
fields: &mut HashSet<&'a str>,
filter: &str,
) {
for node in nodes {
match node {
ParsedNode::Text(_) => {}
ParsedNode::Replacement { key, filters } => {
if filters.iter().any(|f| f == filter) {
fields.insert(key);
}
}
ParsedNode::Conditional { children, .. } => {
find_fields_with_filter(&children, fields, filter);
}
ParsedNode::NegatedConditional { children, .. } => {
find_fields_with_filter(&children, fields, filter);
}
}
}
}
// Tests // Tests
//--------------------------------------- //---------------------------------------