diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 6fbf317f8..0a7f33b8e 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -25,10 +25,7 @@ use crate::{ sched::cutoff::{local_minutes_west_for_stamp, sched_timing_today}, sched::timespan::{answer_button_time, learning_congrats, studied_today, time_span}, search::SortMode, - template::{ - render_card, without_legacy_template_directives, FieldMap, FieldRequirements, - ParsedTemplate, RenderedNode, - }, + template::{render_card, FieldMap, FieldRequirements, ParsedTemplate, RenderedNode}, text::{extract_av_tags, strip_av_tags, AVTag}, timestamp::TimestampSecs, types::Usn, @@ -405,8 +402,7 @@ impl Backend { .template_front .into_iter() .map(|template| { - let normalized = without_legacy_template_directives(&template); - if let Ok(tmpl) = ParsedTemplate::from_text(normalized.as_ref()) { + if let Ok(tmpl) = ParsedTemplate::from_text(&template) { // convert the rust structure into a protobuf one let val = match tmpl.requirements(&map) { FieldRequirements::Any(ords) => Value::Any(pb::TemplateRequirementAny { diff --git a/rslib/src/notetype/mod.rs b/rslib/src/notetype/mod.rs index 22a59b9fa..7b0254b5d 100644 --- a/rslib/src/notetype/mod.rs +++ b/rslib/src/notetype/mod.rs @@ -21,7 +21,7 @@ use crate::{ define_newtype, err::{AnkiError, Result}, notes::Note, - template::{without_legacy_template_directives, FieldRequirements, ParsedTemplate}, + template::{FieldRequirements, ParsedTemplate}, text::ensure_string_in_nfc, timestamp::TimestampSecs, types::Usn, @@ -102,8 +102,7 @@ impl NoteType { .enumerate() .map(|(ord, tmpl)| { let conf = &tmpl.config; - let normalized = without_legacy_template_directives(&conf.q_format); - if let Ok(tmpl) = ParsedTemplate::from_text(normalized.as_ref()) { + if let Ok(tmpl) = ParsedTemplate::from_text(&conf.q_format) { let mut req = match tmpl.requirements(&field_map) { FieldRequirements::Any(ords) => CardRequirement { card_ord: ord as u32, diff --git a/rslib/src/template.rs b/rslib/src/template.rs index acdbff54d..569c1a433 100644 --- a/rslib/src/template.rs +++ b/rslib/src/template.rs @@ -6,11 +6,12 @@ use crate::i18n::{tr_strs, I18n, TR}; use crate::template_filters::apply_filters; use lazy_static::lazy_static; use nom::branch::alt; -use nom::bytes::complete::tag; -use nom::error::ErrorKind; -use nom::sequence::delimited; +use nom::bytes::complete::{tag, take_until}; +use nom::{ + combinator::{map, rest}, + sequence::delimited, +}; use regex::Regex; -use std::borrow::Cow; use std::collections::{HashMap, HashSet}; use std::iter; @@ -34,41 +35,47 @@ pub enum Token<'a> { CloseConditional(&'a str), } -/// a span of text, terminated by {{ or end of string -pub(crate) fn text_until_open_handlebars(s: &str) -> nom::IResult<&str, &str> { - let end = s.len(); - - let limited_end = end.min(s.find("{{").unwrap_or(end)); - let (output, input) = s.split_at(limited_end); - if output.is_empty() { - Err(nom::Err::Error((input, ErrorKind::TakeUntil))) - } else { - Ok((input, output)) - } -} - -/// a span of text, terminated by }} or end of string -pub(crate) fn text_until_close_handlebars(s: &str) -> nom::IResult<&str, &str> { - let end = s.len(); - - let limited_end = end.min(s.find("}}").unwrap_or(end)); - let (output, input) = s.split_at(limited_end); - if output.is_empty() { - Err(nom::Err::Error((input, ErrorKind::TakeUntil))) - } else { - Ok((input, output)) - } -} - /// text outside handlebars fn text_token(s: &str) -> nom::IResult<&str, Token> { - text_until_open_handlebars(s).map(|(input, output)| (input, Token::Text(output))) + map(alt((take_until("{{"), rest)), Token::Text)(s) } /// text wrapped in handlebars -fn handle_token(s: &str) -> nom::IResult<&str, Token> { - delimited(tag("{{"), text_until_close_handlebars, tag("}}"))(s) - .map(|(input, output)| (input, classify_handle(output))) +fn handlebar_token(s: &str) -> nom::IResult<&str, Token> { + map(delimited(tag("{{"), take_until("}}"), tag("}}")), |out| { + classify_handle(out) + })(s) +} + +fn next_token(input: &str) -> nom::IResult<&str, Token> { + alt((handlebar_token, text_token))(input) +} + +fn tokens<'a>(template: &'a str) -> Box> + 'a> { + if template.trim_start().starts_with(ALT_HANDLEBAR_DIRECTIVE) { + Box::new(legacy_tokens( + template + .trim_start() + .trim_start_matches(ALT_HANDLEBAR_DIRECTIVE), + )) + } else { + Box::new(new_tokens(template)) + } +} + +fn new_tokens(mut data: &str) -> impl Iterator> { + std::iter::from_fn(move || { + if data.is_empty() { + return None; + } + match next_token(data) { + Ok((i, o)) => { + data = i; + Some(Ok(o)) + } + Err(_e) => Some(Err(TemplateError::NoClosingBrackets(data.to_string()))), + } + }) } /// classify handle based on leading character @@ -88,18 +95,38 @@ fn classify_handle(s: &str) -> Token { } } -fn next_token(input: &str) -> nom::IResult<&str, Token> { - alt((handle_token, text_token))(input) +// Legacy support +//---------------------------------------- + +static ALT_HANDLEBAR_DIRECTIVE: &str = "{{=<% %>=}}"; + +fn legacy_text_token(s: &str) -> nom::IResult<&str, Token> { + map(alt((take_until("{{"), take_until("<%"), rest)), |out| { + Token::Text(out) + })(s) } -fn tokens(template: &str) -> impl Iterator> { - let mut data = template; +fn legacy_next_token(input: &str) -> nom::IResult<&str, Token> { + alt(( + handlebar_token, + alternate_handlebar_token, + legacy_text_token, + ))(input) +} +/// text wrapped in <% %> +fn alternate_handlebar_token(s: &str) -> nom::IResult<&str, Token> { + map(delimited(tag("<%"), take_until("%>"), tag("%>")), |out| { + classify_handle(out) + })(s) +} + +fn legacy_tokens(mut data: &str) -> impl Iterator> { std::iter::from_fn(move || { if data.is_empty() { return None; } - match next_token(data) { + match legacy_next_token(data) { Ok((i, o)) => { data = i; Some(Ok(o)) @@ -134,9 +161,6 @@ pub struct ParsedTemplate<'a>(Vec>); impl ParsedTemplate<'_> { /// Create a template from the provided text. - /// - /// 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) -> TemplateResult { let mut iter = tokens(template); Ok(Self(parse_inner(&mut iter, None)?)) @@ -251,24 +275,6 @@ fn localized_template_error(i18n: &I18n, err: TemplateError) -> String { } } -// Legacy support -//---------------------------------------- - -static ALT_HANDLEBAR_DIRECTIVE: &str = "{{=<% %>=}}"; - -/// Convert legacy alternate syntax to standard syntax. -pub fn without_legacy_template_directives(text: &str) -> Cow { - if text.trim_start().starts_with(ALT_HANDLEBAR_DIRECTIVE) { - text.trim_start() - .trim_start_matches(ALT_HANDLEBAR_DIRECTIVE) - .replace("<%", "{{") - .replace("%>", "}}") - .into() - } else { - text.into() - } -} - // Checking if template is empty //---------------------------------------- @@ -495,8 +501,7 @@ pub fn render_card( }; // question side - let qnorm = without_legacy_template_directives(qfmt); - let (qnodes, qtmpl) = ParsedTemplate::from_text(qnorm.as_ref()) + let (qnodes, qtmpl) = ParsedTemplate::from_text(qfmt) .and_then(|tmpl| Ok((tmpl.render(&context)?, tmpl))) .map_err(|e| template_error_to_anki_error(e, true, i18n))?; @@ -513,8 +518,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()) + let anodes = ParsedTemplate::from_text(afmt) .and_then(|tmpl| tmpl.render(&context)) .map_err(|e| template_error_to_anki_error(e, false, i18n))?; @@ -581,10 +585,7 @@ impl ParsedTemplate<'_> { mod test { use super::{FieldMap, ParsedNode::*, ParsedTemplate as PT}; use crate::err::TemplateError; - use crate::template::{ - field_is_empty, nonempty_fields, without_legacy_template_directives, FieldRequirements, - RenderContext, - }; + use crate::template::{field_is_empty, nonempty_fields, FieldRequirements, RenderContext}; use std::collections::{HashMap, HashSet}; use std::iter::FromIterator; @@ -722,12 +723,21 @@ mod test { <%Front%> <% #Back %> <%/Back%>"; - let output = " -{{Front}} -{{ #Back }} -{{/Back}}"; - - assert_eq!(without_legacy_template_directives(input), output); + assert_eq!( + PT::from_text(input).unwrap().0, + vec![ + Text("\n"), + Replacement { + key: "Front", + filters: vec![] + }, + Text("\n"), + Conditional { + key: "Back", + children: vec![Text("\n")] + } + ] + ); } #[test]