diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py
index dbaa6b281..40d1a501e 100644
--- a/pylib/anki/collection.py
+++ b/pylib/anki/collection.py
@@ -672,7 +672,12 @@ where c.nid = n.id and c.id in %s group by nid"""
(qfmt, afmt) = hooks.card_will_render((qfmt, afmt), fields, model, data)
# render fields
- qatext = render_card(self, qfmt, afmt, fields, card_ord)
+ try:
+ qatext = render_card(self, qfmt, afmt, fields, card_ord)
+ except anki.rsbackend.BackendException as e:
+ errmsg = f"Card template has a problem:
{e}"
+ qatext = (errmsg, errmsg)
+
ret: Dict[str, Any] = dict(q=qatext[0], a=qatext[1], id=card_id)
# allow add-ons to modify the generated result
diff --git a/pylib/anki/rsbackend.py b/pylib/anki/rsbackend.py
index a230b9731..50c0aff86 100644
--- a/pylib/anki/rsbackend.py
+++ b/pylib/anki/rsbackend.py
@@ -23,7 +23,7 @@ class BackendException(Exception):
if kind == "invalid_input":
return f"invalid input: {err.invalid_input.info}"
elif kind == "template_parse":
- return f"template parse: {err.template_parse.info}"
+ return err.template_parse.info
else:
return f"unhandled error: {err}"
diff --git a/pylib/anki/template.py b/pylib/anki/template.py
index 7352c79b9..febb145e1 100644
--- a/pylib/anki/template.py
+++ b/pylib/anki/template.py
@@ -44,7 +44,9 @@ def render_card(
fields: Dict[str, str],
card_ord: int,
) -> Tuple[str, str]:
- "Renders the provided templates, returning rendered q & a text."
+ """Renders the provided templates, returning rendered q & a text.
+
+ Will raise if the template is invalid."""
(qnodes, anodes) = col.backend.render_card(qfmt, afmt, fields, card_ord)
qtext = apply_custom_filters(qnodes, fields, front_side=None)
diff --git a/rslib/src/backend.rs b/rslib/src/backend.rs
index 34a2b8387..44f20e2ec 100644
--- a/rslib/src/backend.rs
+++ b/rslib/src/backend.rs
@@ -97,7 +97,7 @@ impl Backend {
Value::DeckTree(_) => todo!(),
Value::FindCards(_) => todo!(),
Value::BrowserRows(_) => todo!(),
- Value::RenderCard(input) => OValue::RenderCard(self.render_template(input)),
+ Value::RenderCard(input) => OValue::RenderCard(self.render_template(input)?),
})
}
@@ -161,7 +161,7 @@ impl Backend {
}
}
- fn render_template(&self, input: pt::RenderCardIn) -> pt::RenderCardOut {
+ fn render_template(&self, input: pt::RenderCardIn) -> Result {
// convert string map to &str
let fields: HashMap<_, _> = input
.fields
@@ -175,13 +175,13 @@ impl Backend {
&input.answer_template,
&fields,
input.card_ordinal as u16,
- );
+ )?;
// return
- pt::RenderCardOut {
+ Ok(pt::RenderCardOut {
question_nodes: rendered_nodes_to_proto(qnodes),
answer_nodes: rendered_nodes_to_proto(anodes),
- }
+ })
}
}
diff --git a/rslib/src/err.rs b/rslib/src/err.rs
index 6f3328386..919b1da8c 100644
--- a/rslib/src/err.rs
+++ b/rslib/src/err.rs
@@ -16,11 +16,30 @@ pub enum AnkiError {
// error helpers
impl AnkiError {
- pub(crate) fn parse>(s: S) -> AnkiError {
- AnkiError::TemplateParseError { info: s.into() }
- }
-
pub(crate) fn invalid_input>(s: S) -> AnkiError {
AnkiError::InvalidInput { info: s.into() }
}
}
+
+#[derive(Debug)]
+pub enum TemplateError {
+ NoClosingBrackets(String),
+ ConditionalNotClosed(String),
+ ConditionalNotOpen(String),
+}
+
+impl From for AnkiError {
+ fn from(terr: TemplateError) -> Self {
+ AnkiError::TemplateParseError {
+ info: match terr {
+ TemplateError::NoClosingBrackets(context) => {
+ format!("expected '{{{{field name}}}}', found '{}'", context)
+ }
+ TemplateError::ConditionalNotClosed(tag) => format!("missing '{{{{/{}}}}}'", tag),
+ TemplateError::ConditionalNotOpen(tag) => {
+ format!("missing '{{{{#{}}}}}' or '{{{{^{}}}}}'", tag, tag)
+ }
+ },
+ }
+ }
+}
diff --git a/rslib/src/template.rs b/rslib/src/template.rs
index 86ebc4704..3c0b98b2d 100644
--- a/rslib/src/template.rs
+++ b/rslib/src/template.rs
@@ -1,7 +1,7 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-use crate::err::{AnkiError, Result};
+use crate::err::{Result, TemplateError};
use crate::template_filters::apply_filters;
use crate::text::strip_sounds;
use lazy_static::lazy_static;
@@ -87,7 +87,7 @@ fn next_token(input: &str) -> nom::IResult<&str, Token> {
alt((handle_token, text_token))(input)
}
-fn tokens(template: &str) -> impl Iterator- > {
+fn tokens(template: &str) -> impl Iterator
- > {
let mut data = template;
std::iter::from_fn(move || {
@@ -99,7 +99,7 @@ fn tokens(template: &str) -> impl Iterator
- > {
data = i;
Some(Ok(o))
}
- Err(e) => Some(Err(AnkiError::parse(format!("{:?}", e)))),
+ Err(_e) => Some(Err(TemplateError::NoClosingBrackets(data.to_string()))),
}
})
}
@@ -132,16 +132,16 @@ impl ParsedTemplate<'_> {
///
/// The legacy alternate syntax is not supported, so the provided text
/// should be run through without_legacy_template_directives() first.
- pub fn from_text(template: &str) -> Result {
+ pub fn from_text(template: &str) -> std::result::Result {
let mut iter = tokens(template);
Ok(Self(parse_inner(&mut iter, None)?))
}
}
-fn parse_inner<'a, I: Iterator
- >>>(
+fn parse_inner<'a, I: Iterator
- , TemplateError>>>(
iter: &mut I,
open_tag: Option<&'a str>,
-) -> Result>> {
+) -> std::result::Result>, TemplateError> {
let mut nodes = vec![];
while let Some(token) = iter.next() {
@@ -170,16 +170,13 @@ fn parse_inner<'a, I: Iterator
- >>>(
return Ok(nodes);
}
}
- return Err(AnkiError::parse(format!(
- "unbalanced closing tag: {:?} / {}",
- open_tag, t
- )));
+ return Err(TemplateError::ConditionalNotOpen(t.to_string()));
}
});
}
if let Some(open) = open_tag {
- Err(AnkiError::parse(format!("unclosed conditional {}", open)))
+ Err(TemplateError::ConditionalNotClosed(open.to_string()))
} else {
Ok(nodes)
}
@@ -426,7 +423,7 @@ pub fn render_card(
afmt: &str,
field_map: &HashMap<&str, &str>,
card_ord: u16,
-) -> (Vec, Vec) {
+) -> Result<(Vec, Vec)> {
// prepare context
let mut context = RenderContext {
fields: field_map,
@@ -438,12 +435,7 @@ pub fn render_card(
// question side
let qnorm = without_legacy_template_directives(qfmt);
- let qnodes = match ParsedTemplate::from_text(qnorm.as_ref()) {
- Ok(tmpl) => tmpl.render(&context),
- Err(e) => vec![RenderedNode::Text {
- text: format!("{:?}", e),
- }],
- };
+ let qnodes = ParsedTemplate::from_text(qnorm.as_ref())?.render(&context);
// if the question side didn't have any unknown filters, we can pass
// FrontSide in now
@@ -454,14 +446,9 @@ pub fn render_card(
// answer side
context.question_side = false;
let anorm = without_legacy_template_directives(afmt);
- let anodes = match ParsedTemplate::from_text(anorm.as_ref()) {
- Ok(tmpl) => tmpl.render(&context),
- Err(e) => vec![RenderedNode::Text {
- text: format!("{:?}", e),
- }],
- };
+ let anodes = ParsedTemplate::from_text(anorm.as_ref())?.render(&context);
- (qnodes, anodes)
+ Ok((qnodes, anodes))
}
// Field requirements
@@ -779,7 +766,7 @@ mod test {
let clozed_text = "{{c1::one}} {{c2::two::hint}}";
let map: HashMap<_, _> = vec![("Text", clozed_text)].into_iter().collect();
- let (qnodes, anodes) = render_card(fmt, fmt, &map, 0);
+ let (qnodes, anodes) = render_card(fmt, fmt, &map, 0).unwrap();
assert_eq!(
strip_html(get_complete_template(&qnodes).unwrap()),
"[...] two"
@@ -790,11 +777,12 @@ mod test {
);
// FrontSide should render if only standard modifiers were used
- let (_qnodes, anodes) = render_card("{{kana:text:Text}}", "{{FrontSide}}", &map, 1);
+ let (_qnodes, anodes) =
+ render_card("{{kana:text:Text}}", "{{FrontSide}}", &map, 1).unwrap();
assert_eq!(get_complete_template(&anodes).unwrap(), clozed_text);
// But if a custom modifier was used, it's deferred to the Python code
- let (_qnodes, anodes) = render_card("{{custom:Text}}", "{{FrontSide}}", &map, 1);
+ let (_qnodes, anodes) = render_card("{{custom:Text}}", "{{FrontSide}}", &map, 1).unwrap();
assert_eq!(get_complete_template(&anodes).is_none(), true)
}
}