mirror of
https://github.com/ankitects/anki.git
synced 2025-09-21 07:22:23 -04:00
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:
parent
ca5843acea
commit
fb578a0c2d
3 changed files with 56 additions and 50 deletions
|
@ -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<'_> {
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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())]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in a new issue