more useful template error message

This commit is contained in:
Damien Elmes 2020-01-16 16:37:44 +10:00
parent 81f7e634d1
commit b56c9591c0
6 changed files with 54 additions and 40 deletions

View File

@ -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:<br>{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

View File

@ -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}"

View File

@ -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)

View File

@ -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<pt::RenderCardOut> {
// 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),
}
})
}
}

View File

@ -16,11 +16,30 @@ pub enum AnkiError {
// error helpers
impl AnkiError {
pub(crate) fn parse<S: Into<String>>(s: S) -> AnkiError {
AnkiError::TemplateParseError { info: s.into() }
}
pub(crate) fn invalid_input<S: Into<String>>(s: S) -> AnkiError {
AnkiError::InvalidInput { info: s.into() }
}
}
#[derive(Debug)]
pub enum TemplateError {
NoClosingBrackets(String),
ConditionalNotClosed(String),
ConditionalNotOpen(String),
}
impl From<TemplateError> 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)
}
},
}
}
}

View File

@ -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<Item = Result<Token>> {
fn tokens(template: &str) -> impl Iterator<Item = std::result::Result<Token, TemplateError>> {
let mut data = template;
std::iter::from_fn(move || {
@ -99,7 +99,7 @@ fn tokens(template: &str) -> impl Iterator<Item = Result<Token>> {
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<ParsedTemplate> {
pub fn from_text(template: &str) -> std::result::Result<ParsedTemplate, TemplateError> {
let mut iter = tokens(template);
Ok(Self(parse_inner(&mut iter, None)?))
}
}
fn parse_inner<'a, I: Iterator<Item = Result<Token<'a>>>>(
fn parse_inner<'a, I: Iterator<Item = std::result::Result<Token<'a>, TemplateError>>>(
iter: &mut I,
open_tag: Option<&'a str>,
) -> Result<Vec<ParsedNode<'a>>> {
) -> std::result::Result<Vec<ParsedNode<'a>>, TemplateError> {
let mut nodes = vec![];
while let Some(token) = iter.next() {
@ -170,16 +170,13 @@ fn parse_inner<'a, I: Iterator<Item = Result<Token<'a>>>>(
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<RenderedNode>, Vec<RenderedNode>) {
) -> Result<(Vec<RenderedNode>, Vec<RenderedNode>)> {
// 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)
}
}