mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
more useful template error message
This commit is contained in:
parent
81f7e634d1
commit
b56c9591c0
6 changed files with 54 additions and 40 deletions
|
@ -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)
|
(qfmt, afmt) = hooks.card_will_render((qfmt, afmt), fields, model, data)
|
||||||
|
|
||||||
# render fields
|
# render fields
|
||||||
|
try:
|
||||||
qatext = render_card(self, qfmt, afmt, fields, card_ord)
|
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)
|
ret: Dict[str, Any] = dict(q=qatext[0], a=qatext[1], id=card_id)
|
||||||
|
|
||||||
# allow add-ons to modify the generated result
|
# allow add-ons to modify the generated result
|
||||||
|
|
|
@ -23,7 +23,7 @@ class BackendException(Exception):
|
||||||
if kind == "invalid_input":
|
if kind == "invalid_input":
|
||||||
return f"invalid input: {err.invalid_input.info}"
|
return f"invalid input: {err.invalid_input.info}"
|
||||||
elif kind == "template_parse":
|
elif kind == "template_parse":
|
||||||
return f"template parse: {err.template_parse.info}"
|
return err.template_parse.info
|
||||||
else:
|
else:
|
||||||
return f"unhandled error: {err}"
|
return f"unhandled error: {err}"
|
||||||
|
|
||||||
|
|
|
@ -44,7 +44,9 @@ def render_card(
|
||||||
fields: Dict[str, str],
|
fields: Dict[str, str],
|
||||||
card_ord: int,
|
card_ord: int,
|
||||||
) -> Tuple[str, str]:
|
) -> 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)
|
(qnodes, anodes) = col.backend.render_card(qfmt, afmt, fields, card_ord)
|
||||||
|
|
||||||
qtext = apply_custom_filters(qnodes, fields, front_side=None)
|
qtext = apply_custom_filters(qnodes, fields, front_side=None)
|
||||||
|
|
|
@ -97,7 +97,7 @@ impl Backend {
|
||||||
Value::DeckTree(_) => todo!(),
|
Value::DeckTree(_) => todo!(),
|
||||||
Value::FindCards(_) => todo!(),
|
Value::FindCards(_) => todo!(),
|
||||||
Value::BrowserRows(_) => 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
|
// convert string map to &str
|
||||||
let fields: HashMap<_, _> = input
|
let fields: HashMap<_, _> = input
|
||||||
.fields
|
.fields
|
||||||
|
@ -175,13 +175,13 @@ impl Backend {
|
||||||
&input.answer_template,
|
&input.answer_template,
|
||||||
&fields,
|
&fields,
|
||||||
input.card_ordinal as u16,
|
input.card_ordinal as u16,
|
||||||
);
|
)?;
|
||||||
|
|
||||||
// return
|
// return
|
||||||
pt::RenderCardOut {
|
Ok(pt::RenderCardOut {
|
||||||
question_nodes: rendered_nodes_to_proto(qnodes),
|
question_nodes: rendered_nodes_to_proto(qnodes),
|
||||||
answer_nodes: rendered_nodes_to_proto(anodes),
|
answer_nodes: rendered_nodes_to_proto(anodes),
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,11 +16,30 @@ pub enum AnkiError {
|
||||||
|
|
||||||
// error helpers
|
// error helpers
|
||||||
impl AnkiError {
|
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 {
|
pub(crate) fn invalid_input<S: Into<String>>(s: S) -> AnkiError {
|
||||||
AnkiError::InvalidInput { info: s.into() }
|
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)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
use crate::err::{AnkiError, Result};
|
use crate::err::{Result, TemplateError};
|
||||||
use crate::template_filters::apply_filters;
|
use crate::template_filters::apply_filters;
|
||||||
use crate::text::strip_sounds;
|
use crate::text::strip_sounds;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
|
@ -87,7 +87,7 @@ fn next_token(input: &str) -> nom::IResult<&str, Token> {
|
||||||
alt((handle_token, text_token))(input)
|
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;
|
let mut data = template;
|
||||||
|
|
||||||
std::iter::from_fn(move || {
|
std::iter::from_fn(move || {
|
||||||
|
@ -99,7 +99,7 @@ fn tokens(template: &str) -> impl Iterator<Item = Result<Token>> {
|
||||||
data = i;
|
data = i;
|
||||||
Some(Ok(o))
|
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
|
/// The legacy alternate syntax is not supported, so the provided text
|
||||||
/// should be run through without_legacy_template_directives() first.
|
/// 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);
|
let mut iter = tokens(template);
|
||||||
Ok(Self(parse_inner(&mut iter, None)?))
|
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,
|
iter: &mut I,
|
||||||
open_tag: Option<&'a str>,
|
open_tag: Option<&'a str>,
|
||||||
) -> Result<Vec<ParsedNode<'a>>> {
|
) -> std::result::Result<Vec<ParsedNode<'a>>, TemplateError> {
|
||||||
let mut nodes = vec![];
|
let mut nodes = vec![];
|
||||||
|
|
||||||
while let Some(token) = iter.next() {
|
while let Some(token) = iter.next() {
|
||||||
|
@ -170,16 +170,13 @@ fn parse_inner<'a, I: Iterator<Item = Result<Token<'a>>>>(
|
||||||
return Ok(nodes);
|
return Ok(nodes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Err(AnkiError::parse(format!(
|
return Err(TemplateError::ConditionalNotOpen(t.to_string()));
|
||||||
"unbalanced closing tag: {:?} / {}",
|
|
||||||
open_tag, t
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(open) = open_tag {
|
if let Some(open) = open_tag {
|
||||||
Err(AnkiError::parse(format!("unclosed conditional {}", open)))
|
Err(TemplateError::ConditionalNotClosed(open.to_string()))
|
||||||
} else {
|
} else {
|
||||||
Ok(nodes)
|
Ok(nodes)
|
||||||
}
|
}
|
||||||
|
@ -426,7 +423,7 @@ pub fn render_card(
|
||||||
afmt: &str,
|
afmt: &str,
|
||||||
field_map: &HashMap<&str, &str>,
|
field_map: &HashMap<&str, &str>,
|
||||||
card_ord: u16,
|
card_ord: u16,
|
||||||
) -> (Vec<RenderedNode>, Vec<RenderedNode>) {
|
) -> Result<(Vec<RenderedNode>, Vec<RenderedNode>)> {
|
||||||
// prepare context
|
// prepare context
|
||||||
let mut context = RenderContext {
|
let mut context = RenderContext {
|
||||||
fields: field_map,
|
fields: field_map,
|
||||||
|
@ -438,12 +435,7 @@ pub fn render_card(
|
||||||
|
|
||||||
// question side
|
// question side
|
||||||
let qnorm = without_legacy_template_directives(qfmt);
|
let qnorm = without_legacy_template_directives(qfmt);
|
||||||
let qnodes = match ParsedTemplate::from_text(qnorm.as_ref()) {
|
let qnodes = ParsedTemplate::from_text(qnorm.as_ref())?.render(&context);
|
||||||
Ok(tmpl) => tmpl.render(&context),
|
|
||||||
Err(e) => vec![RenderedNode::Text {
|
|
||||||
text: format!("{:?}", e),
|
|
||||||
}],
|
|
||||||
};
|
|
||||||
|
|
||||||
// if the question side didn't have any unknown filters, we can pass
|
// if the question side didn't have any unknown filters, we can pass
|
||||||
// FrontSide in now
|
// FrontSide in now
|
||||||
|
@ -454,14 +446,9 @@ pub fn render_card(
|
||||||
// answer side
|
// answer side
|
||||||
context.question_side = false;
|
context.question_side = false;
|
||||||
let anorm = without_legacy_template_directives(afmt);
|
let anorm = without_legacy_template_directives(afmt);
|
||||||
let anodes = match ParsedTemplate::from_text(anorm.as_ref()) {
|
let anodes = ParsedTemplate::from_text(anorm.as_ref())?.render(&context);
|
||||||
Ok(tmpl) => tmpl.render(&context),
|
|
||||||
Err(e) => vec![RenderedNode::Text {
|
|
||||||
text: format!("{:?}", e),
|
|
||||||
}],
|
|
||||||
};
|
|
||||||
|
|
||||||
(qnodes, anodes)
|
Ok((qnodes, anodes))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Field requirements
|
// Field requirements
|
||||||
|
@ -779,7 +766,7 @@ mod test {
|
||||||
let clozed_text = "{{c1::one}} {{c2::two::hint}}";
|
let clozed_text = "{{c1::one}} {{c2::two::hint}}";
|
||||||
let map: HashMap<_, _> = vec![("Text", clozed_text)].into_iter().collect();
|
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!(
|
assert_eq!(
|
||||||
strip_html(get_complete_template(&qnodes).unwrap()),
|
strip_html(get_complete_template(&qnodes).unwrap()),
|
||||||
"[...] two"
|
"[...] two"
|
||||||
|
@ -790,11 +777,12 @@ mod test {
|
||||||
);
|
);
|
||||||
|
|
||||||
// FrontSide should render if only standard modifiers were used
|
// 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);
|
assert_eq!(get_complete_template(&anodes).unwrap(), clozed_text);
|
||||||
|
|
||||||
// But if a custom modifier was used, it's deferred to the Python code
|
// 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)
|
assert_eq!(get_complete_template(&anodes).is_none(), true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue