show missing field errors in the same way as the other errors

This commit is contained in:
Damien Elmes 2020-01-16 17:23:25 +10:00
parent b56c9591c0
commit bdac937802
3 changed files with 49 additions and 49 deletions

View file

@ -25,9 +25,7 @@ impl std::convert::From<AnkiError> for pt::BackendError {
use pt::backend_error::Value as V; use pt::backend_error::Value as V;
let value = match err { let value = match err {
AnkiError::InvalidInput { info } => V::InvalidInput(pt::InvalidInputError { info }), AnkiError::InvalidInput { info } => V::InvalidInput(pt::InvalidInputError { info }),
AnkiError::TemplateParseError { info } => { AnkiError::TemplateError { info } => V::TemplateParse(pt::TemplateParseError { info }),
V::TemplateParse(pt::TemplateParseError { info })
}
}; };
pt::BackendError { value: Some(value) } pt::BackendError { value: Some(value) }

View file

@ -11,7 +11,7 @@ pub enum AnkiError {
InvalidInput { info: String }, InvalidInput { info: String },
#[fail(display = "invalid card template: {}", info)] #[fail(display = "invalid card template: {}", info)]
TemplateParseError { info: String }, TemplateError { info: String },
} }
// error helpers // error helpers
@ -21,24 +21,29 @@ impl AnkiError {
} }
} }
#[derive(Debug)] #[derive(Debug, PartialEq)]
pub enum TemplateError { pub enum TemplateError {
NoClosingBrackets(String), NoClosingBrackets(String),
ConditionalNotClosed(String), ConditionalNotClosed(String),
ConditionalNotOpen(String), ConditionalNotOpen(String),
FieldNotFound(String),
} }
impl From<TemplateError> for AnkiError { impl From<TemplateError> for AnkiError {
fn from(terr: TemplateError) -> Self { fn from(terr: TemplateError) -> Self {
AnkiError::TemplateParseError { AnkiError::TemplateError {
info: match terr { info: match terr {
TemplateError::NoClosingBrackets(context) => { TemplateError::NoClosingBrackets(context) => {
format!("expected '{{{{field name}}}}', found '{}'", context) format!("missing '}}}}' in '{}'", context)
} }
TemplateError::ConditionalNotClosed(tag) => format!("missing '{{{{/{}}}}}'", tag), TemplateError::ConditionalNotClosed(tag) => format!("missing '{{{{/{}}}}}'", tag),
TemplateError::ConditionalNotOpen(tag) => { TemplateError::ConditionalNotOpen(tag) => {
format!("missing '{{{{#{}}}}}' or '{{{{^{}}}}}'", tag, tag) format!("missing '{{{{#{}}}}}' or '{{{{^{}}}}}'", tag, tag)
} }
TemplateError::FieldNotFound(field) => format!(
"found '{{{{{}}}}}', but there is no field called '{}'",
field, field
),
}, },
} }
} }

View file

@ -16,6 +16,7 @@ use std::collections::{HashMap, HashSet};
use std::iter; use std::iter;
pub type FieldMap<'a> = HashMap<&'a str, u16>; pub type FieldMap<'a> = HashMap<&'a str, u16>;
type TemplateResult<T> = std::result::Result<T, TemplateError>;
// Lexing // Lexing
//---------------------------------------- //----------------------------------------
@ -87,7 +88,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 = std::result::Result<Token, TemplateError>> { fn tokens(template: &str) -> impl Iterator<Item = TemplateResult<Token>> {
let mut data = template; let mut data = template;
std::iter::from_fn(move || { std::iter::from_fn(move || {
@ -132,16 +133,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) -> std::result::Result<ParsedTemplate, TemplateError> { pub fn from_text(template: &str) -> TemplateResult<ParsedTemplate> {
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 = std::result::Result<Token<'a>, TemplateError>>>( fn parse_inner<'a, I: Iterator<Item = TemplateResult<Token<'a>>>>(
iter: &mut I, iter: &mut I,
open_tag: Option<&'a str>, open_tag: Option<&'a str>,
) -> std::result::Result<Vec<ParsedNode<'a>>, TemplateError> { ) -> TemplateResult<Vec<ParsedNode<'a>>> {
let mut nodes = vec![]; let mut nodes = vec![];
while let Some(token) = iter.next() { while let Some(token) = iter.next() {
@ -275,12 +276,12 @@ impl ParsedTemplate<'_> {
/// Replacements that use only standard filters will become part of /// Replacements that use only standard filters will become part of
/// a text node. If a non-standard filter is encountered, a partially /// a text node. If a non-standard filter is encountered, a partially
/// rendered Replacement is returned for the calling code to complete. /// rendered Replacement is returned for the calling code to complete.
fn render(&self, context: &RenderContext) -> Vec<RenderedNode> { fn render(&self, context: &RenderContext) -> TemplateResult<Vec<RenderedNode>> {
let mut rendered = vec![]; let mut rendered = vec![];
render_into(&mut rendered, self.0.as_ref(), context); render_into(&mut rendered, self.0.as_ref(), context)?;
rendered Ok(rendered)
} }
} }
@ -288,7 +289,7 @@ fn render_into(
rendered_nodes: &mut Vec<RenderedNode>, rendered_nodes: &mut Vec<RenderedNode>,
nodes: &[ParsedNode], nodes: &[ParsedNode],
context: &RenderContext, context: &RenderContext,
) { ) -> TemplateResult<()> {
use ParsedNode::*; use ParsedNode::*;
for node in nodes { for node in nodes {
match node { match node {
@ -327,7 +328,17 @@ fn render_into(
// apply built in filters if field exists // apply built in filters if field exists
let (text, remaining_filters) = match context.fields.get(key) { let (text, remaining_filters) = match context.fields.get(key) {
Some(text) => apply_filters(text, filters, key, context), Some(text) => apply_filters(text, filters, key, context),
None => (unknown_field_message(key, filters).into(), vec![]), None => {
// unknown field encountered
let name_including_filters = filters
.iter()
.rev()
.cloned()
.chain(iter::once(*key))
.collect::<Vec<_>>()
.join(":");
return Err(TemplateError::FieldNotFound(name_including_filters));
}
}; };
// fully processed? // fully processed?
@ -343,16 +354,18 @@ fn render_into(
} }
Conditional { key, children } => { Conditional { key, children } => {
if context.nonempty_fields.contains(key) { if context.nonempty_fields.contains(key) {
render_into(rendered_nodes, children.as_ref(), context); render_into(rendered_nodes, children.as_ref(), context)?;
} }
} }
NegatedConditional { key, children } => { NegatedConditional { key, children } => {
if !context.nonempty_fields.contains(key) { if !context.nonempty_fields.contains(key) {
render_into(rendered_nodes, children.as_ref(), context); render_into(rendered_nodes, children.as_ref(), context)?;
} }
} }
}; };
} }
Ok(())
} }
/// Append to last node if last node is a string, else add new node. /// Append to last node if last node is a string, else add new node.
@ -401,19 +414,6 @@ fn nonempty_fields<'a>(fields: &'a HashMap<&str, &str>) -> HashSet<&'a str> {
.collect() .collect()
} }
fn unknown_field_message(field_name: &str, filters: &[&str]) -> String {
format!(
"{{unknown field {}}}",
filters
.iter()
.rev()
.cloned()
.chain(iter::once(field_name))
.collect::<Vec<_>>()
.join(":")
)
}
// Rendering both sides // Rendering both sides
//---------------------------------------- //----------------------------------------
@ -435,7 +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 = ParsedTemplate::from_text(qnorm.as_ref())?.render(&context); let qnodes = ParsedTemplate::from_text(qnorm.as_ref())?.render(&context)?;
// 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
@ -446,7 +446,7 @@ 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 = ParsedTemplate::from_text(anorm.as_ref())?.render(&context); let anodes = ParsedTemplate::from_text(anorm.as_ref())?.render(&context)?;
Ok((qnodes, anodes)) Ok((qnodes, anodes))
} }
@ -510,6 +510,7 @@ impl ParsedTemplate<'_> {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::{FieldMap, ParsedNode::*, ParsedTemplate as PT}; use super::{FieldMap, ParsedNode::*, ParsedTemplate as PT};
use crate::err::TemplateError;
use crate::template::{ use crate::template::{
field_is_empty, nonempty_fields, render_card, without_legacy_template_directives, field_is_empty, nonempty_fields, render_card, without_legacy_template_directives,
FieldRequirements, RenderContext, RenderedNode, FieldRequirements, RenderContext, RenderedNode,
@ -661,7 +662,7 @@ mod test {
use crate::template::RenderedNode as FN; use crate::template::RenderedNode as FN;
let mut tmpl = PT::from_text("{{B}}A{{F}}").unwrap(); let mut tmpl = PT::from_text("{{B}}A{{F}}").unwrap();
assert_eq!( assert_eq!(
tmpl.render(&ctx), tmpl.render(&ctx).unwrap(),
vec![FN::Text { vec![FN::Text {
text: "bAf".to_owned() text: "bAf".to_owned()
},] },]
@ -669,12 +670,12 @@ mod test {
// empty // empty
tmpl = PT::from_text("{{#E}}A{{/E}}").unwrap(); tmpl = PT::from_text("{{#E}}A{{/E}}").unwrap();
assert_eq!(tmpl.render(&ctx), vec![]); assert_eq!(tmpl.render(&ctx).unwrap(), vec![]);
// missing // missing
tmpl = PT::from_text("{{^M}}A{{/M}}").unwrap(); tmpl = PT::from_text("{{^M}}A{{/M}}").unwrap();
assert_eq!( assert_eq!(
tmpl.render(&ctx), tmpl.render(&ctx).unwrap(),
vec![FN::Text { vec![FN::Text {
text: "A".to_owned() text: "A".to_owned()
},] },]
@ -683,7 +684,7 @@ mod test {
// nested // nested
tmpl = PT::from_text("{{^E}}1{{#F}}2{{#B}}{{F}}{{/B}}{{/F}}{{/E}}").unwrap(); tmpl = PT::from_text("{{^E}}1{{#F}}2{{#B}}{{F}}{{/B}}{{/F}}{{/E}}").unwrap();
assert_eq!( assert_eq!(
tmpl.render(&ctx), tmpl.render(&ctx).unwrap(),
vec![FN::Text { vec![FN::Text {
text: "12f".to_owned() text: "12f".to_owned()
},] },]
@ -692,7 +693,7 @@ mod test {
// unknown filters // unknown filters
tmpl = PT::from_text("{{one:two:B}}").unwrap(); tmpl = PT::from_text("{{one:two:B}}").unwrap();
assert_eq!( assert_eq!(
tmpl.render(&ctx), tmpl.render(&ctx).unwrap(),
vec![FN::Replacement { vec![FN::Replacement {
field_name: "B".to_owned(), field_name: "B".to_owned(),
filters: vec!["two".to_string(), "one".to_string()], filters: vec!["two".to_string(), "one".to_string()],
@ -704,7 +705,7 @@ mod test {
// excess colons are ignored // excess colons are ignored
tmpl = PT::from_text("{{one::text:B}}").unwrap(); tmpl = PT::from_text("{{one::text:B}}").unwrap();
assert_eq!( assert_eq!(
tmpl.render(&ctx), tmpl.render(&ctx).unwrap(),
vec![FN::Replacement { vec![FN::Replacement {
field_name: "B".to_owned(), field_name: "B".to_owned(),
filters: vec!["one".to_string()], filters: vec!["one".to_string()],
@ -715,7 +716,7 @@ mod test {
// known filter // known filter
tmpl = PT::from_text("{{text:B}}").unwrap(); tmpl = PT::from_text("{{text:B}}").unwrap();
assert_eq!( assert_eq!(
tmpl.render(&ctx), tmpl.render(&ctx).unwrap(),
vec![FN::Text { vec![FN::Text {
text: "b".to_owned() text: "b".to_owned()
}] }]
@ -724,25 +725,21 @@ mod test {
// unknown field // unknown field
tmpl = PT::from_text("{{X}}").unwrap(); tmpl = PT::from_text("{{X}}").unwrap();
assert_eq!( assert_eq!(
tmpl.render(&ctx), tmpl.render(&ctx).unwrap_err(),
vec![FN::Text { TemplateError::FieldNotFound("X".to_owned())
text: "{unknown field X}".to_owned()
}]
); );
// unknown field with filters // unknown field with filters
tmpl = PT::from_text("{{foo:text:X}}").unwrap(); tmpl = PT::from_text("{{foo:text:X}}").unwrap();
assert_eq!( assert_eq!(
tmpl.render(&ctx), tmpl.render(&ctx).unwrap_err(),
vec![FN::Text { TemplateError::FieldNotFound("foo:text:X".to_owned())
text: "{unknown field foo:text:X}".to_owned()
}]
); );
// a blank field is allowed if it has filters // a blank field is allowed if it has filters
tmpl = PT::from_text("{{filter:}}").unwrap(); tmpl = PT::from_text("{{filter:}}").unwrap();
assert_eq!( assert_eq!(
tmpl.render(&ctx), tmpl.render(&ctx).unwrap(),
vec![FN::Replacement { vec![FN::Replacement {
field_name: "".to_string(), field_name: "".to_string(),
current_text: "".to_string(), current_text: "".to_string(),