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;
let value = match err {
AnkiError::InvalidInput { info } => V::InvalidInput(pt::InvalidInputError { info }),
AnkiError::TemplateParseError { info } => {
V::TemplateParse(pt::TemplateParseError { info })
}
AnkiError::TemplateError { info } => V::TemplateParse(pt::TemplateParseError { info }),
};
pt::BackendError { value: Some(value) }

View File

@ -11,7 +11,7 @@ pub enum AnkiError {
InvalidInput { info: String },
#[fail(display = "invalid card template: {}", info)]
TemplateParseError { info: String },
TemplateError { info: String },
}
// error helpers
@ -21,24 +21,29 @@ impl AnkiError {
}
}
#[derive(Debug)]
#[derive(Debug, PartialEq)]
pub enum TemplateError {
NoClosingBrackets(String),
ConditionalNotClosed(String),
ConditionalNotOpen(String),
FieldNotFound(String),
}
impl From<TemplateError> for AnkiError {
fn from(terr: TemplateError) -> Self {
AnkiError::TemplateParseError {
AnkiError::TemplateError {
info: match terr {
TemplateError::NoClosingBrackets(context) => {
format!("expected '{{{{field name}}}}', found '{}'", context)
format!("missing '}}}}' in '{}'", context)
}
TemplateError::ConditionalNotClosed(tag) => format!("missing '{{{{/{}}}}}'", tag),
TemplateError::ConditionalNotOpen(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;
pub type FieldMap<'a> = HashMap<&'a str, u16>;
type TemplateResult<T> = std::result::Result<T, TemplateError>;
// Lexing
//----------------------------------------
@ -87,7 +88,7 @@ fn next_token(input: &str) -> nom::IResult<&str, Token> {
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;
std::iter::from_fn(move || {
@ -132,16 +133,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) -> std::result::Result<ParsedTemplate, TemplateError> {
pub fn from_text(template: &str) -> TemplateResult<ParsedTemplate> {
let mut iter = tokens(template);
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,
open_tag: Option<&'a str>,
) -> std::result::Result<Vec<ParsedNode<'a>>, TemplateError> {
) -> TemplateResult<Vec<ParsedNode<'a>>> {
let mut nodes = vec![];
while let Some(token) = iter.next() {
@ -275,12 +276,12 @@ impl ParsedTemplate<'_> {
/// Replacements that use only standard filters will become part of
/// a text node. If a non-standard filter is encountered, a partially
/// 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![];
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>,
nodes: &[ParsedNode],
context: &RenderContext,
) {
) -> TemplateResult<()> {
use ParsedNode::*;
for node in nodes {
match node {
@ -327,7 +328,17 @@ fn render_into(
// apply built in filters if field exists
let (text, remaining_filters) = match context.fields.get(key) {
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?
@ -343,16 +354,18 @@ fn render_into(
}
Conditional { key, children } => {
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 } => {
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.
@ -401,19 +414,6 @@ fn nonempty_fields<'a>(fields: &'a HashMap<&str, &str>) -> HashSet<&'a str> {
.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
//----------------------------------------
@ -435,7 +435,7 @@ pub fn render_card(
// question side
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
// FrontSide in now
@ -446,7 +446,7 @@ pub fn render_card(
// answer side
context.question_side = false;
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))
}
@ -510,6 +510,7 @@ impl ParsedTemplate<'_> {
#[cfg(test)]
mod test {
use super::{FieldMap, ParsedNode::*, ParsedTemplate as PT};
use crate::err::TemplateError;
use crate::template::{
field_is_empty, nonempty_fields, render_card, without_legacy_template_directives,
FieldRequirements, RenderContext, RenderedNode,
@ -661,7 +662,7 @@ mod test {
use crate::template::RenderedNode as FN;
let mut tmpl = PT::from_text("{{B}}A{{F}}").unwrap();
assert_eq!(
tmpl.render(&ctx),
tmpl.render(&ctx).unwrap(),
vec![FN::Text {
text: "bAf".to_owned()
},]
@ -669,12 +670,12 @@ mod test {
// empty
tmpl = PT::from_text("{{#E}}A{{/E}}").unwrap();
assert_eq!(tmpl.render(&ctx), vec![]);
assert_eq!(tmpl.render(&ctx).unwrap(), vec![]);
// missing
tmpl = PT::from_text("{{^M}}A{{/M}}").unwrap();
assert_eq!(
tmpl.render(&ctx),
tmpl.render(&ctx).unwrap(),
vec![FN::Text {
text: "A".to_owned()
},]
@ -683,7 +684,7 @@ mod test {
// nested
tmpl = PT::from_text("{{^E}}1{{#F}}2{{#B}}{{F}}{{/B}}{{/F}}{{/E}}").unwrap();
assert_eq!(
tmpl.render(&ctx),
tmpl.render(&ctx).unwrap(),
vec![FN::Text {
text: "12f".to_owned()
},]
@ -692,7 +693,7 @@ mod test {
// unknown filters
tmpl = PT::from_text("{{one:two:B}}").unwrap();
assert_eq!(
tmpl.render(&ctx),
tmpl.render(&ctx).unwrap(),
vec![FN::Replacement {
field_name: "B".to_owned(),
filters: vec!["two".to_string(), "one".to_string()],
@ -704,7 +705,7 @@ mod test {
// excess colons are ignored
tmpl = PT::from_text("{{one::text:B}}").unwrap();
assert_eq!(
tmpl.render(&ctx),
tmpl.render(&ctx).unwrap(),
vec![FN::Replacement {
field_name: "B".to_owned(),
filters: vec!["one".to_string()],
@ -715,7 +716,7 @@ mod test {
// known filter
tmpl = PT::from_text("{{text:B}}").unwrap();
assert_eq!(
tmpl.render(&ctx),
tmpl.render(&ctx).unwrap(),
vec![FN::Text {
text: "b".to_owned()
}]
@ -724,25 +725,21 @@ mod test {
// unknown field
tmpl = PT::from_text("{{X}}").unwrap();
assert_eq!(
tmpl.render(&ctx),
vec![FN::Text {
text: "{unknown field X}".to_owned()
}]
tmpl.render(&ctx).unwrap_err(),
TemplateError::FieldNotFound("X".to_owned())
);
// unknown field with filters
tmpl = PT::from_text("{{foo:text:X}}").unwrap();
assert_eq!(
tmpl.render(&ctx),
vec![FN::Text {
text: "{unknown field foo:text:X}".to_owned()
}]
tmpl.render(&ctx).unwrap_err(),
TemplateError::FieldNotFound("foo:text:X".to_owned())
);
// a blank field is allowed if it has filters
tmpl = PT::from_text("{{filter:}}").unwrap();
assert_eq!(
tmpl.render(&ctx),
tmpl.render(&ctx).unwrap(),
vec![FN::Replacement {
field_name: "".to_string(),
current_text: "".to_string(),