handle legacy syntax in parser instead of modifying original template

Simplifies things for the caller, and ensures legacy handling doesn't
get accidentally forgotten
This commit is contained in:
Damien Elmes 2020-04-16 20:04:55 +10:00
parent f24dc05c8d
commit 5d4f9dc3c0
3 changed files with 88 additions and 83 deletions

View file

@ -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 {

View file

@ -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,

View file

@ -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<dyn Iterator<Item = TemplateResult<Token>> + '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<Item = TemplateResult<Token>> {
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<Item = TemplateResult<Token>> {
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<Item = TemplateResult<Token>> {
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<ParsedNode<'a>>);
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<ParsedTemplate> {
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<str> {
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]