mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 06:22:22 -04:00
Template err improvements (#1953)
* Throw error for unknown condition fields as well So if 'foo' is not a field, refuse to save a template containing `{{#foo}}bar{{/foo}}`. Previously, only `{{foo}}` would be checked. As a side effect, templates which *only* contain fields as conditions may be saved. Meh. * Display template errors in q/a columns only So the affected browser row remains active and the user can fix the template more easily. * Specify if error occured in a browser template * Minor wording tweak (dae) There's an argument for using the exact wording as well, but this just reads a little more naturally to me.
This commit is contained in:
parent
ca69198097
commit
8c515e316e
5 changed files with 97 additions and 96 deletions
|
@ -6,6 +6,8 @@
|
||||||
card-template-rendering-more-info = More information
|
card-template-rendering-more-info = More information
|
||||||
card-template-rendering-front-side-problem = Front template has a problem:
|
card-template-rendering-front-side-problem = Front template has a problem:
|
||||||
card-template-rendering-back-side-problem = Back template has a problem:
|
card-template-rendering-back-side-problem = Back template has a problem:
|
||||||
|
card-template-rendering-browser-front-side-problem = Browser-specific front template has a problem:
|
||||||
|
card-template-rendering-browser-back-side-problem = Browser-specific back template has a problem:
|
||||||
# when the user forgot to close a field reference,
|
# when the user forgot to close a field reference,
|
||||||
# eg, Missing '}}' in '{{Field'
|
# eg, Missing '}}' in '{{Field'
|
||||||
card-template-rendering-no-closing-brackets = Missing '{ $missing }' in '{ $tag }'
|
card-template-rendering-no-closing-brackets = Missing '{ $missing }' in '{ $tag }'
|
||||||
|
|
|
@ -64,14 +64,18 @@ struct RowContext {
|
||||||
original_deck: Option<Arc<Deck>>,
|
original_deck: Option<Arc<Deck>>,
|
||||||
tr: I18n,
|
tr: I18n,
|
||||||
timing: SchedTimingToday,
|
timing: SchedTimingToday,
|
||||||
render_context: Option<RenderContext>,
|
render_context: RenderContext,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The answer string needs the question string but not the other way around, so only build the
|
enum RenderContext {
|
||||||
/// answer string when needed.
|
// The answer string needs the question string, but not the other way around,
|
||||||
struct RenderContext {
|
// so only build the answer string when needed.
|
||||||
|
Ok {
|
||||||
question: String,
|
question: String,
|
||||||
answer_nodes: Vec<RenderedNode>,
|
answer_nodes: Vec<RenderedNode>,
|
||||||
|
},
|
||||||
|
Err(String),
|
||||||
|
Unset,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn card_render_required(columns: &[Column]) -> bool {
|
fn card_render_required(columns: &[Column]) -> bool {
|
||||||
|
@ -251,33 +255,49 @@ impl Collection {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderContext {
|
impl RenderContext {
|
||||||
fn new(col: &mut Collection, card: &Card, note: &Note, notetype: &Notetype) -> Result<Self> {
|
fn new(col: &mut Collection, card: &Card, note: &Note, notetype: &Notetype) -> Self {
|
||||||
let render = col.render_card(
|
match notetype
|
||||||
note,
|
.get_template(card.template_idx)
|
||||||
card,
|
.and_then(|template| col.render_card(note, card, notetype, template, true))
|
||||||
notetype,
|
{
|
||||||
notetype.get_template(card.template_idx)?,
|
Ok(render) => RenderContext::Ok {
|
||||||
true,
|
question: rendered_nodes_to_str(&render.qnodes),
|
||||||
)?;
|
answer_nodes: render.anodes,
|
||||||
let qnodes_text = render
|
},
|
||||||
.qnodes
|
Err(err) => RenderContext::Err(err.localized_description(&col.tr)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn side_str(&self, is_answer: bool) -> String {
|
||||||
|
let back;
|
||||||
|
let html = match self {
|
||||||
|
Self::Ok {
|
||||||
|
question,
|
||||||
|
answer_nodes,
|
||||||
|
} => {
|
||||||
|
if is_answer {
|
||||||
|
back = rendered_nodes_to_str(answer_nodes);
|
||||||
|
back.strip_prefix(question).unwrap_or(&back)
|
||||||
|
} else {
|
||||||
|
question
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self::Err(err) => err,
|
||||||
|
Self::Unset => "Invalid input: RenderContext unset",
|
||||||
|
};
|
||||||
|
html_to_text_line(html, true).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rendered_nodes_to_str(nodes: &[RenderedNode]) -> String {
|
||||||
|
let txt = nodes
|
||||||
.iter()
|
.iter()
|
||||||
.map(|node| match node {
|
.map(|node| match node {
|
||||||
RenderedNode::Text { text } => text,
|
RenderedNode::Text { text } => text,
|
||||||
RenderedNode::Replacement {
|
RenderedNode::Replacement { current_text, .. } => current_text,
|
||||||
field_name: _,
|
|
||||||
current_text,
|
|
||||||
filters: _,
|
|
||||||
} => current_text,
|
|
||||||
})
|
})
|
||||||
.join("");
|
.join("");
|
||||||
let question = prettify_av_tags(qnodes_text);
|
prettify_av_tags(txt)
|
||||||
|
|
||||||
Ok(RenderContext {
|
|
||||||
question,
|
|
||||||
answer_nodes: render.anodes,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RowContext {
|
impl RowContext {
|
||||||
|
@ -324,9 +344,9 @@ impl RowContext {
|
||||||
};
|
};
|
||||||
let timing = col.timing_today()?;
|
let timing = col.timing_today()?;
|
||||||
let render_context = if with_card_render {
|
let render_context = if with_card_render {
|
||||||
Some(RenderContext::new(col, &cards[0], ¬e, ¬etype)?)
|
RenderContext::new(col, &cards[0], ¬e, ¬etype)
|
||||||
} else {
|
} else {
|
||||||
None
|
RenderContext::Unset
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(RowContext {
|
Ok(RowContext {
|
||||||
|
@ -363,8 +383,8 @@ impl RowContext {
|
||||||
|
|
||||||
fn get_cell_text(&self, column: Column) -> Result<String> {
|
fn get_cell_text(&self, column: Column) -> Result<String> {
|
||||||
Ok(match column {
|
Ok(match column {
|
||||||
Column::Question => self.question_str(),
|
Column::Question => self.render_context.side_str(false),
|
||||||
Column::Answer => self.answer_str(),
|
Column::Answer => self.render_context.side_str(true),
|
||||||
Column::Deck => self.deck_str(),
|
Column::Deck => self.deck_str(),
|
||||||
Column::Due => self.due_str(),
|
Column::Due => self.due_str(),
|
||||||
Column::Ease => self.ease_str(),
|
Column::Ease => self.ease_str(),
|
||||||
|
@ -405,32 +425,6 @@ impl RowContext {
|
||||||
self.notetype.get_template(self.cards[0].template_idx)
|
self.notetype.get_template(self.cards[0].template_idx)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn answer_str(&self) -> String {
|
|
||||||
let render_context = self.render_context.as_ref().unwrap();
|
|
||||||
let answer = render_context
|
|
||||||
.answer_nodes
|
|
||||||
.iter()
|
|
||||||
.map(|node| match node {
|
|
||||||
RenderedNode::Text { text } => text,
|
|
||||||
RenderedNode::Replacement {
|
|
||||||
field_name: _,
|
|
||||||
current_text,
|
|
||||||
filters: _,
|
|
||||||
} => current_text,
|
|
||||||
})
|
|
||||||
.join("");
|
|
||||||
let answer = prettify_av_tags(answer);
|
|
||||||
html_to_text_line(
|
|
||||||
if let Some(stripped) = answer.strip_prefix(&render_context.question) {
|
|
||||||
stripped
|
|
||||||
} else {
|
|
||||||
&answer
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
.to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn due_str(&self) -> String {
|
fn due_str(&self) -> String {
|
||||||
if self.notes_mode {
|
if self.notes_mode {
|
||||||
self.note_due_str()
|
self.note_due_str()
|
||||||
|
@ -514,7 +508,7 @@ impl RowContext {
|
||||||
.iter()
|
.iter()
|
||||||
.map(|c| c.mtime)
|
.map(|c| c.mtime)
|
||||||
.max()
|
.max()
|
||||||
.unwrap()
|
.expect("cards missing from RowContext")
|
||||||
.date_string()
|
.date_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -545,10 +539,6 @@ impl RowContext {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn question_str(&self) -> String {
|
|
||||||
html_to_text_line(&self.render_context.as_ref().unwrap().question, true).to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_row_font_name(&self) -> Result<String> {
|
fn get_row_font_name(&self) -> Result<String> {
|
||||||
Ok(self.template()?.config.browser_font_name.to_owned())
|
Ok(self.template()?.config.browser_font_name.to_owned())
|
||||||
}
|
}
|
||||||
|
|
|
@ -391,10 +391,12 @@ impl Notetype {
|
||||||
if let Some((invalid_index, details)) =
|
if let Some((invalid_index, details)) =
|
||||||
templates.iter().enumerate().find_map(|(index, sides)| {
|
templates.iter().enumerate().find_map(|(index, sides)| {
|
||||||
if let (Some(q), Some(a)) = sides {
|
if let (Some(q), Some(a)) = sides {
|
||||||
let q_fields = q.fields();
|
let q_fields = q.all_referenced_field_names();
|
||||||
if q_fields.is_empty() {
|
if q_fields.is_empty() {
|
||||||
Some((index, CardTypeErrorDetails::NoFrontField))
|
Some((index, CardTypeErrorDetails::NoFrontField))
|
||||||
} else if self.unknown_field_name(q_fields.union(&a.fields())) {
|
} else if self
|
||||||
|
.unknown_field_name(q_fields.union(&a.all_referenced_field_names()))
|
||||||
|
{
|
||||||
Some((index, CardTypeErrorDetails::NoSuchField))
|
Some((index, CardTypeErrorDetails::NoSuchField))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
@ -601,7 +603,7 @@ impl Notetype {
|
||||||
HashSet::new()
|
HashSet::new()
|
||||||
} else if let Some((Some(front), _)) = self.parsed_templates().get(0) {
|
} else if let Some((Some(front), _)) = self.parsed_templates().get(0) {
|
||||||
front
|
front
|
||||||
.cloze_fields()
|
.all_referenced_cloze_field_names()
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|name| self.get_field_ord(name))
|
.filter_map(|name| self.get_field_ord(name))
|
||||||
.collect()
|
.collect()
|
||||||
|
@ -624,7 +626,7 @@ fn missing_cloze_filter(
|
||||||
fn has_cloze(template: &Option<ParsedTemplate>) -> bool {
|
fn has_cloze(template: &Option<ParsedTemplate>) -> bool {
|
||||||
template
|
template
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map_or(false, |t| !t.cloze_fields().is_empty())
|
.map_or(false, |t| !t.all_referenced_cloze_field_names().is_empty())
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Notetype> for NotetypeProto {
|
impl From<Notetype> for NotetypeProto {
|
||||||
|
|
|
@ -118,6 +118,7 @@ impl Collection {
|
||||||
&field_map,
|
&field_map,
|
||||||
card.template_idx,
|
card.template_idx,
|
||||||
nt.is_cloze(),
|
nt.is_cloze(),
|
||||||
|
browser,
|
||||||
&self.tr,
|
&self.tr,
|
||||||
)?;
|
)?;
|
||||||
Ok(RenderCardOutput {
|
Ok(RenderCardOutput {
|
||||||
|
@ -172,7 +173,7 @@ fn flag_name(n: u8) -> &'static str {
|
||||||
|
|
||||||
fn fill_empty_fields(note: &mut Note, qfmt: &str, nt: &Notetype, tr: &I18n) {
|
fn fill_empty_fields(note: &mut Note, qfmt: &str, nt: &Notetype, tr: &I18n) {
|
||||||
if let Ok(tmpl) = ParsedTemplate::from_text(qfmt) {
|
if let Ok(tmpl) = ParsedTemplate::from_text(qfmt) {
|
||||||
let cloze_fields = tmpl.cloze_fields();
|
let cloze_fields = tmpl.all_referenced_cloze_field_names();
|
||||||
|
|
||||||
for (val, field) in note.fields_mut().iter_mut().zip(nt.fields.iter()) {
|
for (val, field) in note.fields_mut().iter_mut().zip(nt.fields.iter()) {
|
||||||
if field_is_empty(val) {
|
if field_is_empty(val) {
|
||||||
|
|
|
@ -254,11 +254,17 @@ fn parse_inner<'a, I: Iterator<Item = TemplateResult<Token<'a>>>>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn template_error_to_anki_error(err: TemplateError, q_side: bool, tr: &I18n) -> AnkiError {
|
fn template_error_to_anki_error(
|
||||||
let header = if q_side {
|
err: TemplateError,
|
||||||
tr.card_template_rendering_front_side_problem()
|
q_side: bool,
|
||||||
} else {
|
browser: bool,
|
||||||
tr.card_template_rendering_back_side_problem()
|
tr: &I18n,
|
||||||
|
) -> AnkiError {
|
||||||
|
let header = match (q_side, browser) {
|
||||||
|
(true, false) => tr.card_template_rendering_front_side_problem(),
|
||||||
|
(false, false) => tr.card_template_rendering_back_side_problem(),
|
||||||
|
(true, true) => tr.card_template_rendering_browser_front_side_problem(),
|
||||||
|
(false, true) => tr.card_template_rendering_browser_back_side_problem(),
|
||||||
};
|
};
|
||||||
let details = htmlescape::encode_minimal(&localized_template_error(tr, err));
|
let details = htmlescape::encode_minimal(&localized_template_error(tr, err));
|
||||||
let more_info = tr.card_template_rendering_more_info();
|
let more_info = tr.card_template_rendering_more_info();
|
||||||
|
@ -569,6 +575,7 @@ pub fn render_card(
|
||||||
field_map: &HashMap<&str, Cow<str>>,
|
field_map: &HashMap<&str, Cow<str>>,
|
||||||
card_ord: u16,
|
card_ord: u16,
|
||||||
is_cloze: bool,
|
is_cloze: bool,
|
||||||
|
browser: bool,
|
||||||
tr: &I18n,
|
tr: &I18n,
|
||||||
) -> Result<(Vec<RenderedNode>, Vec<RenderedNode>)> {
|
) -> Result<(Vec<RenderedNode>, Vec<RenderedNode>)> {
|
||||||
// prepare context
|
// prepare context
|
||||||
|
@ -582,7 +589,7 @@ pub fn render_card(
|
||||||
// question side
|
// question side
|
||||||
let (mut qnodes, qtmpl) = ParsedTemplate::from_text(qfmt)
|
let (mut qnodes, qtmpl) = ParsedTemplate::from_text(qfmt)
|
||||||
.and_then(|tmpl| Ok((tmpl.render(&context, tr)?, tmpl)))
|
.and_then(|tmpl| Ok((tmpl.render(&context, tr)?, tmpl)))
|
||||||
.map_err(|e| template_error_to_anki_error(e, true, tr))?;
|
.map_err(|e| template_error_to_anki_error(e, true, browser, tr))?;
|
||||||
|
|
||||||
// check if the front side was empty
|
// check if the front side was empty
|
||||||
let empty_message = if is_cloze && cloze_is_empty(field_map, card_ord) {
|
let empty_message = if is_cloze && cloze_is_empty(field_map, card_ord) {
|
||||||
|
@ -611,7 +618,7 @@ pub fn render_card(
|
||||||
context.question_side = false;
|
context.question_side = false;
|
||||||
let anodes = ParsedTemplate::from_text(afmt)
|
let anodes = ParsedTemplate::from_text(afmt)
|
||||||
.and_then(|tmpl| tmpl.render(&context, tr))
|
.and_then(|tmpl| tmpl.render(&context, tr))
|
||||||
.map_err(|e| template_error_to_anki_error(e, false, tr))?;
|
.map_err(|e| template_error_to_anki_error(e, false, browser, tr))?;
|
||||||
|
|
||||||
Ok((qnodes, anodes))
|
Ok((qnodes, anodes))
|
||||||
}
|
}
|
||||||
|
@ -790,42 +797,41 @@ fn nodes_to_string(buf: &mut String, nodes: &[ParsedNode]) {
|
||||||
//----------------------------------------
|
//----------------------------------------
|
||||||
|
|
||||||
impl ParsedTemplate {
|
impl ParsedTemplate {
|
||||||
/// A set of all field names. Field names may not be valid.
|
|
||||||
pub(crate) fn fields(&self) -> HashSet<&str> {
|
|
||||||
let mut set = HashSet::new();
|
|
||||||
find_fields_with_filter(&self.0, &mut set, None);
|
|
||||||
set
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A set of field names with a cloze filter attached.
|
|
||||||
/// Field names may not be valid.
|
/// Field names may not be valid.
|
||||||
pub(crate) fn cloze_fields(&self) -> HashSet<&str> {
|
pub(crate) fn all_referenced_field_names(&self) -> HashSet<&str> {
|
||||||
let mut set = HashSet::new();
|
let mut set = HashSet::new();
|
||||||
find_fields_with_filter(&self.0, &mut set, Some("cloze"));
|
find_field_references(&self.0, &mut set, false, true);
|
||||||
|
set
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Field names may not be valid.
|
||||||
|
pub(crate) fn all_referenced_cloze_field_names(&self) -> HashSet<&str> {
|
||||||
|
let mut set = HashSet::new();
|
||||||
|
find_field_references(&self.0, &mut set, true, false);
|
||||||
set
|
set
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Insert all fields in 'nodes' with 'filter' into 'fields'. If 'filter' is None,
|
fn find_field_references<'a>(
|
||||||
/// all fields are collected.
|
|
||||||
fn find_fields_with_filter<'a>(
|
|
||||||
nodes: &'a [ParsedNode],
|
nodes: &'a [ParsedNode],
|
||||||
fields: &mut HashSet<&'a str>,
|
fields: &mut HashSet<&'a str>,
|
||||||
filter: Option<&str>,
|
cloze_only: bool,
|
||||||
|
with_conditionals: bool,
|
||||||
) {
|
) {
|
||||||
for node in nodes {
|
for node in nodes {
|
||||||
match node {
|
match node {
|
||||||
ParsedNode::Text(_) => {}
|
ParsedNode::Text(_) => {}
|
||||||
ParsedNode::Replacement { key, filters } => {
|
ParsedNode::Replacement { key, filters } => {
|
||||||
if filter.is_none() || filters.iter().any(|f| f == filter.unwrap()) {
|
if !cloze_only || filters.iter().any(|f| f == "cloze") {
|
||||||
fields.insert(key);
|
fields.insert(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ParsedNode::Conditional { children, .. } => {
|
ParsedNode::Conditional { key, children }
|
||||||
find_fields_with_filter(children, fields, filter);
|
| ParsedNode::NegatedConditional { key, children } => {
|
||||||
|
if with_conditionals {
|
||||||
|
fields.insert(key);
|
||||||
}
|
}
|
||||||
ParsedNode::NegatedConditional { children, .. } => {
|
find_field_references(children, fields, cloze_only, with_conditionals);
|
||||||
find_fields_with_filter(children, fields, filter);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1168,7 +1174,7 @@ mod test {
|
||||||
let tr = I18n::template_only();
|
let tr = I18n::template_only();
|
||||||
use crate::template::RenderedNode as FN;
|
use crate::template::RenderedNode as FN;
|
||||||
|
|
||||||
let qnodes = super::render_card("test{{E}}", "", &map, 1, false, &tr)
|
let qnodes = super::render_card("test{{E}}", "", &map, 1, false, false, &tr)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.0;
|
.0;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
Loading…
Reference in a new issue