mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02:21 -04:00
add "fill empty" checkbox
This commit is contained in:
parent
9a222e620a
commit
782911471b
11 changed files with 198 additions and 99 deletions
|
@ -275,6 +275,7 @@ message RenderUncommittedCardIn {
|
|||
Note note = 1;
|
||||
uint32 card_ord = 2;
|
||||
bytes template = 3;
|
||||
bool fill_empty = 4;
|
||||
}
|
||||
|
||||
message RenderCardOut {
|
||||
|
|
|
@ -313,13 +313,16 @@ class RustBackend:
|
|||
return PartiallyRenderedCard(qnodes, anodes)
|
||||
|
||||
def render_uncommitted_card(
|
||||
self, note: BackendNote, card_ord: int, template: Dict
|
||||
self, note: BackendNote, card_ord: int, template: Dict, fill_empty: bool
|
||||
) -> PartiallyRenderedCard:
|
||||
template_json = orjson.dumps(template)
|
||||
out = self._run_command(
|
||||
pb.BackendInput(
|
||||
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
|
||||
|
|
|
@ -56,10 +56,20 @@ class TemplateRenderContext:
|
|||
|
||||
@classmethod
|
||||
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:
|
||||
return TemplateRenderContext(
|
||||
note.col, card, note, notetype=notetype, template=template
|
||||
note.col,
|
||||
card,
|
||||
note,
|
||||
notetype=notetype,
|
||||
template=template,
|
||||
fill_empty=fill_empty,
|
||||
)
|
||||
|
||||
def __init__(
|
||||
|
@ -70,12 +80,14 @@ class TemplateRenderContext:
|
|||
browser: bool = False,
|
||||
notetype: NoteType = None,
|
||||
template: Optional[Dict] = None,
|
||||
fill_empty: bool = False,
|
||||
) -> None:
|
||||
self._col = col.weakref()
|
||||
self._card = card
|
||||
self._note = note
|
||||
self._browser = browser
|
||||
self._template = template
|
||||
self._fill_empty = fill_empty
|
||||
if not notetype:
|
||||
self._note_type = note.model()
|
||||
else:
|
||||
|
@ -148,7 +160,10 @@ class TemplateRenderContext:
|
|||
if self._template:
|
||||
# card layout screen
|
||||
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:
|
||||
# existing card (eg study mode)
|
||||
|
|
|
@ -14,26 +14,26 @@ from anki.lang import _, ngettext
|
|||
from anki.notes import Note
|
||||
from anki.rsbackend import TemplateError
|
||||
from anki.template import TemplateRenderContext
|
||||
from anki.utils import isMac, isWin
|
||||
from aqt import AnkiQt, gui_hooks
|
||||
from aqt.qt import *
|
||||
from aqt.sound import av_player, play_clicked_audio
|
||||
from aqt.theme import theme_manager
|
||||
from aqt.utils import (
|
||||
TR,
|
||||
askUser,
|
||||
downArrow,
|
||||
getOnlyText,
|
||||
openHelp,
|
||||
restoreGeom,
|
||||
saveGeom,
|
||||
shortcut,
|
||||
showInfo,
|
||||
showWarning,
|
||||
tooltip,
|
||||
TR, tr, shortcut
|
||||
tr,
|
||||
)
|
||||
from aqt.webview import AnkiWebView
|
||||
|
||||
# fixme: previewing with empty fields
|
||||
# fixme: deck name on new cards
|
||||
# fixme: card count when removing
|
||||
# fixme: change tracking and tooltip in fields
|
||||
|
@ -42,7 +42,12 @@ from aqt.webview import AnkiWebView
|
|||
|
||||
class CardLayout(QDialog):
|
||||
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)
|
||||
mw.setupDialogGC(self)
|
||||
|
@ -53,6 +58,7 @@ class CardLayout(QDialog):
|
|||
self.mm = self.mw.col.models
|
||||
self.model = note.model()
|
||||
self.templates = self.model["tmpls"]
|
||||
self._want_fill_empty_on = fill_empty
|
||||
self.mm._remove_from_cache(self.model["id"])
|
||||
self.mw.checkpoint(_("Card Types"))
|
||||
self.changed = False
|
||||
|
@ -160,17 +166,20 @@ class CardLayout(QDialog):
|
|||
QShortcut(
|
||||
QKeySequence("Ctrl+1"),
|
||||
self,
|
||||
activated=lambda: self.tform.front_button.setChecked(True))
|
||||
activated=lambda: self.tform.front_button.setChecked(True),
|
||||
) # type: ignore
|
||||
QShortcut(
|
||||
QKeySequence("Ctrl+2"),
|
||||
self,
|
||||
activated=lambda: self.tform.back_button.setChecked(True))
|
||||
activated=lambda: self.tform.back_button.setChecked(True),
|
||||
) # type: ignore
|
||||
QShortcut(
|
||||
QKeySequence("Ctrl+3"),
|
||||
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):
|
||||
|
@ -179,56 +188,42 @@ class CardLayout(QDialog):
|
|||
l.setContentsMargins(0, 0, 0, 0)
|
||||
l.setSpacing(3)
|
||||
left = QWidget()
|
||||
# template area
|
||||
tform = self.tform = aqt.forms.template.Ui_Form()
|
||||
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)
|
||||
|
||||
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()
|
||||
self.pform: Any = aqt.forms.preview.Ui_Form()
|
||||
self.pform = aqt.forms.preview.Ui_Form()
|
||||
pform = self.pform
|
||||
pform.setupUi(right)
|
||||
pform.preview_front.setText(tr(TR.CARD_TEMPLATES_FRONT_PREVIEW))
|
||||
pform.preview_back.setText(tr(TR.CARD_TEMPLATES_BACK_PREVIEW))
|
||||
pform.preview_box.setTitle(tr(TR.CARD_TEMPLATES_PREVIEW_BOX))
|
||||
|
||||
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)
|
||||
|
||||
self.setupWebviews()
|
||||
self.setup_edit_area()
|
||||
self.setup_preview()
|
||||
|
||||
l.addWidget(right, 5)
|
||||
w.setLayout(l)
|
||||
|
||||
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.tform.edit_area.setAcceptRichText(False)
|
||||
if qtminor < 10:
|
||||
|
@ -237,6 +232,11 @@ class CardLayout(QDialog):
|
|||
tab_width = self.fontMetrics().width(" " * 4)
|
||||
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):
|
||||
names = (_("Cloze %d") % n for n in self.cloze_numbers)
|
||||
self.pform.cloze_number_combo.addItems(names)
|
||||
|
@ -282,14 +282,20 @@ class CardLayout(QDialog):
|
|||
text = self.tform.search_edit.text()
|
||||
self.on_search_changed(text)
|
||||
|
||||
def setupWebviews(self):
|
||||
def setup_preview(self):
|
||||
pform = self.pform
|
||||
pform.frontWeb = AnkiWebView(title="card layout")
|
||||
pform.verticalLayout.addWidget(pform.frontWeb)
|
||||
self.preview_web = AnkiWebView(title="card layout")
|
||||
pform.verticalLayout.addWidget(self.preview_web)
|
||||
pform.verticalLayout.setStretch(1, 99)
|
||||
pform.preview_front.isChecked()
|
||||
qconnect(pform.preview_front.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 = [
|
||||
"jquery.js",
|
||||
"browsersel.js",
|
||||
|
@ -297,10 +303,21 @@ class CardLayout(QDialog):
|
|||
"mathjax/MathJax.js",
|
||||
"reviewer.js",
|
||||
]
|
||||
pform.frontWeb.stdHtml(
|
||||
self.preview_web.stdHtml(
|
||||
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):
|
||||
self._renderPreview()
|
||||
|
@ -309,18 +326,12 @@ class CardLayout(QDialog):
|
|||
if cmd.startswith("play:"):
|
||||
play_clicked_audio(cmd, self.rendered_card)
|
||||
|
||||
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
|
||||
# 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
|
||||
def note_has_empty_field(self) -> bool:
|
||||
for field in self.note.fields:
|
||||
if not field.strip():
|
||||
# ignores HTML, but this should suffice
|
||||
return True
|
||||
return False
|
||||
|
||||
# Buttons
|
||||
##########################################################################
|
||||
|
@ -383,9 +394,9 @@ class CardLayout(QDialog):
|
|||
text = self.tform.edit_area.toPlainText()
|
||||
|
||||
if self.current_editor_index == 0:
|
||||
self.current_template()['qfmt'] = text
|
||||
self.current_template()["qfmt"] = text
|
||||
elif self.current_editor_index == 1:
|
||||
self.current_template()['afmt'] = text
|
||||
self.current_template()["afmt"] = text
|
||||
else:
|
||||
self.model["css"] = text
|
||||
|
||||
|
@ -427,9 +438,7 @@ class CardLayout(QDialog):
|
|||
audio = c.answer_av_tags()
|
||||
|
||||
# use _showAnswer to avoid the longer delay
|
||||
self.pform.frontWeb.eval(
|
||||
"_showAnswer(%s,'%s');" % (json.dumps(text), bodyclass)
|
||||
)
|
||||
self.preview_web.eval("_showAnswer(%s,'%s');" % (json.dumps(text), bodyclass))
|
||||
|
||||
if c.id not in self.playedAudio:
|
||||
av_player.play_tags(audio)
|
||||
|
@ -459,6 +468,22 @@ class CardLayout(QDialog):
|
|||
repl = answerRepl
|
||||
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
|
||||
######################################################################
|
||||
|
||||
|
@ -673,9 +698,7 @@ Enter deck to place new %s cards in, or leave blank:"""
|
|||
row = form.fields.currentIndex().row()
|
||||
if row >= 0:
|
||||
self._addField(
|
||||
fields[row],
|
||||
form.font.currentFont().family(),
|
||||
form.size.value(),
|
||||
fields[row], form.font.currentFont().family(), form.size.value(),
|
||||
)
|
||||
|
||||
def _addField(self, field, font, size):
|
||||
|
@ -720,7 +743,7 @@ Enter deck to place new %s cards in, or leave blank:"""
|
|||
self.cancelPreviewTimer()
|
||||
av_player.stop_and_clear_queue()
|
||||
saveGeom(self, "CardLayout")
|
||||
self.pform.frontWeb = None
|
||||
self.preview_web = None
|
||||
self.model = None
|
||||
self.rendered_card = None
|
||||
self.mw = None
|
||||
|
|
|
@ -353,7 +353,11 @@ class Editor:
|
|||
else:
|
||||
ord = 0
|
||||
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:
|
||||
self.parentWindow.activateWindow()
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# Copyright: Ankitects Pty Ltd and contributors
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import collections
|
||||
import re
|
||||
from operator import itemgetter
|
||||
from typing import List, Optional
|
||||
|
||||
|
@ -9,6 +9,7 @@ import aqt.clayout
|
|||
from anki import stdmodels
|
||||
from anki.lang import _, ngettext
|
||||
from anki.models import NoteType
|
||||
from anki.notes import Note
|
||||
from anki.rsbackend import pb
|
||||
from aqt import AnkiQt, gui_hooks
|
||||
from aqt.qt import *
|
||||
|
@ -160,24 +161,7 @@ class Models(QDialog):
|
|||
|
||||
def _tmpNote(self):
|
||||
nt = self.current_notetype()
|
||||
self.mm.setCurrent(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
|
||||
return Note(self.col, nt)
|
||||
|
||||
def onFields(self):
|
||||
from aqt.fields import FieldDialog
|
||||
|
@ -188,7 +172,7 @@ class Models(QDialog):
|
|||
from aqt.clayout import CardLayout
|
||||
|
||||
n = self._tmpNote()
|
||||
CardLayout(self.mw, n, ord=0, parent=self)
|
||||
CardLayout(self.mw, n, ord=0, parent=self, fill_empty=True)
|
||||
|
||||
# Cleanup
|
||||
##########################################################################
|
||||
|
|
|
@ -64,6 +64,13 @@
|
|||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="fill_empty">
|
||||
<property name="text">
|
||||
<string notr="true">FILL_EMPTY</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="cloze_number_combo"/>
|
||||
</item>
|
||||
|
|
|
@ -11,3 +11,5 @@ card-templates-front-preview = Front Preview
|
|||
card-templates-back-preview = Back Preview
|
||||
card-templates-preview-box = Preview
|
||||
card-templates-template-box = Template
|
||||
card-templates-sample-cloze = This is a {"{{c1::"}sample{"}}"} cloze deletion.
|
||||
card-templates-fill-empty = Fill Empty Fields
|
||||
|
|
|
@ -472,13 +472,14 @@ impl Backend {
|
|||
) -> Result<pb::RenderCardOut> {
|
||||
let schema11: CardTemplateSchema11 = serde_json::from_slice(&input.template)?;
|
||||
let template = schema11.into();
|
||||
let note = input
|
||||
let mut note = input
|
||||
.note
|
||||
.ok_or_else(|| AnkiError::invalid_input("missing note"))?
|
||||
.into();
|
||||
let ord = input.card_ord as u16;
|
||||
let fill_empty = input.fill_empty;
|
||||
self.with_col(|col| {
|
||||
col.render_uncommitted_card(¬e, &template, ord)
|
||||
col.render_uncommitted_card(&mut note, &template, ord, fill_empty)
|
||||
.map(Into::into)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -6,8 +6,9 @@ use crate::{
|
|||
card::{Card, CardID},
|
||||
collection::Collection,
|
||||
err::{AnkiError, Result},
|
||||
i18n::{I18n, TR},
|
||||
notes::{Note, NoteID},
|
||||
template::{render_card, RenderedNode},
|
||||
template::{field_is_empty, render_card, ParsedTemplate, RenderedNode},
|
||||
};
|
||||
use std::{borrow::Cow, collections::HashMap};
|
||||
|
||||
|
@ -41,17 +42,23 @@ impl Collection {
|
|||
|
||||
/// Render a card that may not yet have been added.
|
||||
/// 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(
|
||||
&mut self,
|
||||
note: &Note,
|
||||
note: &mut Note,
|
||||
template: &CardTemplate,
|
||||
card_ord: u16,
|
||||
fill_empty: bool,
|
||||
) -> Result<RenderCardOutput> {
|
||||
let card = self.existing_or_synthesized_card(note.id, template.ord, card_ord)?;
|
||||
let nt = self
|
||||
.get_notetype(note.ntid)?
|
||||
.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)
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
fn field_is_empty(text: &str) -> bool {
|
||||
pub(crate) fn field_is_empty(text: &str) -> bool {
|
||||
lazy_static! {
|
||||
static ref RE: Regex = Regex::new(
|
||||
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
|
||||
//---------------------------------------
|
||||
|
||||
|
|
Loading…
Reference in a new issue