switch to owned strings in ParsedTemplate

will make it easier to cache the parsed results in the future,
and handle field renames & other transformations
This commit is contained in:
Damien Elmes 2020-04-24 13:39:14 +10:00
parent ca5843acea
commit fb578a0c2d
3 changed files with 56 additions and 50 deletions

View file

@ -35,8 +35,8 @@ pub(crate) struct CardToGenerate {
/// Info required to determine whether a particular card ordinal should exist, /// Info required to determine whether a particular card ordinal should exist,
/// and which deck it should be placed in. /// and which deck it should be placed in.
pub(crate) struct SingleCardGenContext<'a> { pub(crate) struct SingleCardGenContext {
template: Option<ParsedTemplate<'a>>, template: Option<ParsedTemplate>,
target_deck_id: Option<DeckID>, target_deck_id: Option<DeckID>,
} }
@ -45,7 +45,7 @@ pub(crate) struct SingleCardGenContext<'a> {
pub(crate) struct CardGenContext<'a> { pub(crate) struct CardGenContext<'a> {
pub usn: Usn, pub usn: Usn,
pub notetype: &'a NoteType, pub notetype: &'a NoteType,
cards: Vec<SingleCardGenContext<'a>>, cards: Vec<SingleCardGenContext>,
} }
impl CardGenContext<'_> { impl CardGenContext<'_> {

View file

@ -19,7 +19,7 @@ pub struct CardTemplate {
} }
impl CardTemplate { impl CardTemplate {
pub(crate) fn parsed_question(&self) -> Option<ParsedTemplate<'_>> { pub(crate) fn parsed_question(&self) -> Option<ParsedTemplate> {
ParsedTemplate::from_text(&self.config.q_format).ok() ParsedTemplate::from_text(&self.config.q_format).ok()
} }

View file

@ -147,26 +147,26 @@ fn legacy_tokens(mut data: &str) -> impl Iterator<Item = TemplateResult<Token>>
//---------------------------------------- //----------------------------------------
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
enum ParsedNode<'a> { enum ParsedNode {
Text(&'a str), Text(String),
Replacement { Replacement {
key: &'a str, key: String,
filters: Vec<&'a str>, filters: Vec<String>,
}, },
Conditional { Conditional {
key: &'a str, key: String,
children: Vec<ParsedNode<'a>>, children: Vec<ParsedNode>,
}, },
NegatedConditional { NegatedConditional {
key: &'a str, key: String,
children: Vec<ParsedNode<'a>>, children: Vec<ParsedNode>,
}, },
} }
#[derive(Debug)] #[derive(Debug)]
pub struct ParsedTemplate<'a>(Vec<ParsedNode<'a>>); pub struct ParsedTemplate(Vec<ParsedNode>);
impl ParsedTemplate<'_> { impl ParsedTemplate {
/// Create a template from the provided text. /// Create a template from the provided text.
pub fn from_text(template: &str) -> TemplateResult<ParsedTemplate> { pub fn from_text(template: &str) -> TemplateResult<ParsedTemplate> {
let mut iter = tokens(template); let mut iter = tokens(template);
@ -177,26 +177,26 @@ impl ParsedTemplate<'_> {
fn parse_inner<'a, I: Iterator<Item = TemplateResult<Token<'a>>>>( 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>,
) -> TemplateResult<Vec<ParsedNode<'a>>> { ) -> TemplateResult<Vec<ParsedNode>> {
let mut nodes = vec![]; let mut nodes = vec![];
while let Some(token) = iter.next() { while let Some(token) = iter.next() {
use Token::*; use Token::*;
nodes.push(match token? { nodes.push(match token? {
Text(t) => ParsedNode::Text(t), Text(t) => ParsedNode::Text(t.into()),
Replacement(t) => { Replacement(t) => {
let mut it = t.rsplit(':'); let mut it = t.rsplit(':');
ParsedNode::Replacement { ParsedNode::Replacement {
key: it.next().unwrap(), key: it.next().unwrap().into(),
filters: it.collect(), filters: it.map(Into::into).collect(),
} }
} }
OpenConditional(t) => ParsedNode::Conditional { OpenConditional(t) => ParsedNode::Conditional {
key: t, key: t.into(),
children: parse_inner(iter, Some(t))?, children: parse_inner(iter, Some(t))?,
}, },
OpenNegated(t) => ParsedNode::NegatedConditional { OpenNegated(t) => ParsedNode::NegatedConditional {
key: t, key: t.into(),
children: parse_inner(iter, Some(t))?, children: parse_inner(iter, Some(t))?,
}, },
CloseConditional(t) => { CloseConditional(t) => {
@ -285,27 +285,27 @@ fn localized_template_error(i18n: &I18n, err: TemplateError) -> String {
// Checking if template is empty // Checking if template is empty
//---------------------------------------- //----------------------------------------
impl ParsedTemplate<'_> { impl ParsedTemplate {
/// true if provided fields are sufficient to render the template /// true if provided fields are sufficient to render the template
pub fn renders_with_fields(&self, nonempty_fields: &HashSet<&str>) -> bool { pub fn renders_with_fields(&self, nonempty_fields: &HashSet<&str>) -> bool {
!template_is_empty(nonempty_fields, &self.0) !template_is_empty(nonempty_fields, &self.0)
} }
} }
fn template_is_empty<'a>(nonempty_fields: &HashSet<&str>, nodes: &[ParsedNode<'a>]) -> bool { fn template_is_empty(nonempty_fields: &HashSet<&str>, nodes: &[ParsedNode]) -> bool {
use ParsedNode::*; use ParsedNode::*;
for node in nodes { for node in nodes {
match node { match node {
// ignore normal text // ignore normal text
Text(_) => (), Text(_) => (),
Replacement { key, .. } => { Replacement { key, .. } => {
if nonempty_fields.contains(*key) { if nonempty_fields.contains(key.as_str()) {
// a single replacement is enough // a single replacement is enough
return false; return false;
} }
} }
Conditional { key, children } => { Conditional { key, children } => {
if !nonempty_fields.contains(*key) { if !nonempty_fields.contains(key.as_str()) {
continue; continue;
} }
if !template_is_empty(nonempty_fields, children) { if !template_is_empty(nonempty_fields, children) {
@ -347,7 +347,7 @@ pub(crate) struct RenderContext<'a> {
pub card_ord: u16, pub card_ord: u16,
} }
impl ParsedTemplate<'_> { impl ParsedTemplate {
/// Render the template with the provided fields. /// Render the template with the provided fields.
/// ///
/// Replacements that use only standard filters will become part of /// Replacements that use only standard filters will become part of
@ -373,10 +373,7 @@ fn render_into(
Text(text) => { Text(text) => {
append_str_to_nodes(rendered_nodes, text); append_str_to_nodes(rendered_nodes, text);
} }
Replacement { Replacement { key, .. } if key == "FrontSide" => {
key: key @ "FrontSide",
..
} => {
// defer FrontSide rendering to Python, as extra // defer FrontSide rendering to Python, as extra
// filters may be required // filters may be required
rendered_nodes.push(RenderedNode::Replacement { rendered_nodes.push(RenderedNode::Replacement {
@ -385,27 +382,36 @@ fn render_into(
current_text: "".into(), current_text: "".into(),
}); });
} }
Replacement { key: "", filters } if !filters.is_empty() => { Replacement { key, filters } if key == "" && !filters.is_empty() => {
// if a filter is provided, we accept an empty field name to // if a filter is provided, we accept an empty field name to
// mean 'pass an empty string to the filter, and it will add // mean 'pass an empty string to the filter, and it will add
// its own text' // its own text'
rendered_nodes.push(RenderedNode::Replacement { rendered_nodes.push(RenderedNode::Replacement {
field_name: "".to_string(), field_name: "".to_string(),
current_text: "".to_string(), current_text: "".to_string(),
filters: filters.iter().map(|&f| f.to_string()).collect(), filters: filters.clone(),
}) })
} }
Replacement { key, filters } => { Replacement { key, filters } => {
// 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.as_str()) {
Some(text) => apply_filters(text, filters, key, context), Some(text) => apply_filters(
text,
filters
.iter()
.map(|s| s.as_str())
.collect::<Vec<_>>()
.as_slice(),
key,
context,
),
None => { None => {
// unknown field encountered // unknown field encountered
let filters_str = filters let filters_str = filters
.iter() .iter()
.rev() .rev()
.cloned() .cloned()
.chain(iter::once("")) .chain(iter::once("".into()))
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(":"); .join(":");
return Err(TemplateError::FieldNotFound { return Err(TemplateError::FieldNotFound {
@ -427,12 +433,12 @@ fn render_into(
} }
} }
Conditional { key, children } => { Conditional { key, children } => {
if context.nonempty_fields.contains(key) { if context.nonempty_fields.contains(key.as_str()) {
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.as_str()) {
render_into(rendered_nodes, children.as_ref(), context)?; render_into(rendered_nodes, children.as_ref(), context)?;
} }
} }
@ -542,7 +548,7 @@ pub enum FieldRequirements {
None, None,
} }
impl ParsedTemplate<'_> { impl ParsedTemplate {
/// Return fields required by template. /// Return fields required by template.
/// ///
/// This is not able to represent negated expressions or combinations of /// This is not able to represent negated expressions or combinations of
@ -613,15 +619,15 @@ mod test {
assert_eq!( assert_eq!(
tmpl.0, tmpl.0,
vec![ vec![
Text("foo "), Text("foo ".into()),
Replacement { Replacement {
key: "bar", key: "bar".into(),
filters: vec![] filters: vec![]
}, },
Text(" "), Text(" ".into()),
Conditional { Conditional {
key: "baz", key: "baz".into(),
children: vec![Text(" quux ")] children: vec![Text(" quux ".into())]
} }
] ]
); );
@ -630,7 +636,7 @@ mod test {
assert_eq!( assert_eq!(
tmpl.0, tmpl.0,
vec![NegatedConditional { vec![NegatedConditional {
key: "baz", key: "baz".into(),
children: vec![] children: vec![]
}] }]
); );
@ -643,7 +649,7 @@ mod test {
assert_eq!( assert_eq!(
PT::from_text("{{ tag }}").unwrap().0, PT::from_text("{{ tag }}").unwrap().0,
vec![Replacement { vec![Replacement {
key: "tag", key: "tag".into(),
filters: vec![] filters: vec![]
}] }]
); );
@ -651,7 +657,7 @@ mod test {
// stray closing characters (like in javascript) are ignored // stray closing characters (like in javascript) are ignored
assert_eq!( assert_eq!(
PT::from_text("text }} more").unwrap().0, PT::from_text("text }} more").unwrap().0,
vec![Text("text }} more")] vec![Text("text }} more".into())]
); );
PT::from_text("{{").unwrap_err(); PT::from_text("{{").unwrap_err();
@ -737,15 +743,15 @@ mod test {
assert_eq!( assert_eq!(
PT::from_text(input).unwrap().0, PT::from_text(input).unwrap().0,
vec![ vec![
Text("\n"), Text("\n".into()),
Replacement { Replacement {
key: "Front", key: "Front".into(),
filters: vec![] filters: vec![]
}, },
Text("\n"), Text("\n".into()),
Conditional { Conditional {
key: "Back", key: "Back".into(),
children: vec![Text("\n")] children: vec![Text("\n".into())]
} }
] ]
); );