mirror of
https://github.com/ankitects/anki.git
synced 2025-12-10 21:36:55 -05:00
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:
parent
f24dc05c8d
commit
5d4f9dc3c0
3 changed files with 88 additions and 83 deletions
|
|
@ -25,10 +25,7 @@ use crate::{
|
||||||
sched::cutoff::{local_minutes_west_for_stamp, sched_timing_today},
|
sched::cutoff::{local_minutes_west_for_stamp, sched_timing_today},
|
||||||
sched::timespan::{answer_button_time, learning_congrats, studied_today, time_span},
|
sched::timespan::{answer_button_time, learning_congrats, studied_today, time_span},
|
||||||
search::SortMode,
|
search::SortMode,
|
||||||
template::{
|
template::{render_card, FieldMap, FieldRequirements, ParsedTemplate, RenderedNode},
|
||||||
render_card, without_legacy_template_directives, FieldMap, FieldRequirements,
|
|
||||||
ParsedTemplate, RenderedNode,
|
|
||||||
},
|
|
||||||
text::{extract_av_tags, strip_av_tags, AVTag},
|
text::{extract_av_tags, strip_av_tags, AVTag},
|
||||||
timestamp::TimestampSecs,
|
timestamp::TimestampSecs,
|
||||||
types::Usn,
|
types::Usn,
|
||||||
|
|
@ -405,8 +402,7 @@ impl Backend {
|
||||||
.template_front
|
.template_front
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|template| {
|
.map(|template| {
|
||||||
let normalized = without_legacy_template_directives(&template);
|
if let Ok(tmpl) = ParsedTemplate::from_text(&template) {
|
||||||
if let Ok(tmpl) = ParsedTemplate::from_text(normalized.as_ref()) {
|
|
||||||
// convert the rust structure into a protobuf one
|
// convert the rust structure into a protobuf one
|
||||||
let val = match tmpl.requirements(&map) {
|
let val = match tmpl.requirements(&map) {
|
||||||
FieldRequirements::Any(ords) => Value::Any(pb::TemplateRequirementAny {
|
FieldRequirements::Any(ords) => Value::Any(pb::TemplateRequirementAny {
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ use crate::{
|
||||||
define_newtype,
|
define_newtype,
|
||||||
err::{AnkiError, Result},
|
err::{AnkiError, Result},
|
||||||
notes::Note,
|
notes::Note,
|
||||||
template::{without_legacy_template_directives, FieldRequirements, ParsedTemplate},
|
template::{FieldRequirements, ParsedTemplate},
|
||||||
text::ensure_string_in_nfc,
|
text::ensure_string_in_nfc,
|
||||||
timestamp::TimestampSecs,
|
timestamp::TimestampSecs,
|
||||||
types::Usn,
|
types::Usn,
|
||||||
|
|
@ -102,8 +102,7 @@ impl NoteType {
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(ord, tmpl)| {
|
.map(|(ord, tmpl)| {
|
||||||
let conf = &tmpl.config;
|
let conf = &tmpl.config;
|
||||||
let normalized = without_legacy_template_directives(&conf.q_format);
|
if let Ok(tmpl) = ParsedTemplate::from_text(&conf.q_format) {
|
||||||
if let Ok(tmpl) = ParsedTemplate::from_text(normalized.as_ref()) {
|
|
||||||
let mut req = match tmpl.requirements(&field_map) {
|
let mut req = match tmpl.requirements(&field_map) {
|
||||||
FieldRequirements::Any(ords) => CardRequirement {
|
FieldRequirements::Any(ords) => CardRequirement {
|
||||||
card_ord: ord as u32,
|
card_ord: ord as u32,
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,12 @@ use crate::i18n::{tr_strs, I18n, TR};
|
||||||
use crate::template_filters::apply_filters;
|
use crate::template_filters::apply_filters;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use nom::branch::alt;
|
use nom::branch::alt;
|
||||||
use nom::bytes::complete::tag;
|
use nom::bytes::complete::{tag, take_until};
|
||||||
use nom::error::ErrorKind;
|
use nom::{
|
||||||
use nom::sequence::delimited;
|
combinator::{map, rest},
|
||||||
|
sequence::delimited,
|
||||||
|
};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use std::borrow::Cow;
|
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::iter;
|
use std::iter;
|
||||||
|
|
||||||
|
|
@ -34,41 +35,47 @@ pub enum Token<'a> {
|
||||||
CloseConditional(&'a str),
|
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
|
/// text outside handlebars
|
||||||
fn text_token(s: &str) -> nom::IResult<&str, Token> {
|
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
|
/// text wrapped in handlebars
|
||||||
fn handle_token(s: &str) -> nom::IResult<&str, Token> {
|
fn handlebar_token(s: &str) -> nom::IResult<&str, Token> {
|
||||||
delimited(tag("{{"), text_until_close_handlebars, tag("}}"))(s)
|
map(delimited(tag("{{"), take_until("}}"), tag("}}")), |out| {
|
||||||
.map(|(input, output)| (input, classify_handle(output)))
|
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
|
/// 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> {
|
// Legacy support
|
||||||
alt((handle_token, text_token))(input)
|
//----------------------------------------
|
||||||
|
|
||||||
|
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>> {
|
fn legacy_next_token(input: &str) -> nom::IResult<&str, Token> {
|
||||||
let mut data = template;
|
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 || {
|
std::iter::from_fn(move || {
|
||||||
if data.is_empty() {
|
if data.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
match next_token(data) {
|
match legacy_next_token(data) {
|
||||||
Ok((i, o)) => {
|
Ok((i, o)) => {
|
||||||
data = i;
|
data = i;
|
||||||
Some(Ok(o))
|
Some(Ok(o))
|
||||||
|
|
@ -134,9 +161,6 @@ pub struct ParsedTemplate<'a>(Vec<ParsedNode<'a>>);
|
||||||
|
|
||||||
impl ParsedTemplate<'_> {
|
impl ParsedTemplate<'_> {
|
||||||
/// Create a template from the provided text.
|
/// 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> {
|
pub fn from_text(template: &str) -> TemplateResult<ParsedTemplate> {
|
||||||
let mut iter = tokens(template);
|
let mut iter = tokens(template);
|
||||||
Ok(Self(parse_inner(&mut iter, None)?))
|
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
|
// Checking if template is empty
|
||||||
//----------------------------------------
|
//----------------------------------------
|
||||||
|
|
||||||
|
|
@ -495,8 +501,7 @@ pub fn render_card(
|
||||||
};
|
};
|
||||||
|
|
||||||
// question side
|
// question side
|
||||||
let qnorm = without_legacy_template_directives(qfmt);
|
let (qnodes, qtmpl) = ParsedTemplate::from_text(qfmt)
|
||||||
let (qnodes, qtmpl) = ParsedTemplate::from_text(qnorm.as_ref())
|
|
||||||
.and_then(|tmpl| Ok((tmpl.render(&context)?, tmpl)))
|
.and_then(|tmpl| Ok((tmpl.render(&context)?, tmpl)))
|
||||||
.map_err(|e| template_error_to_anki_error(e, true, i18n))?;
|
.map_err(|e| template_error_to_anki_error(e, true, i18n))?;
|
||||||
|
|
||||||
|
|
@ -513,8 +518,7 @@ pub fn render_card(
|
||||||
|
|
||||||
// answer side
|
// answer side
|
||||||
context.question_side = false;
|
context.question_side = false;
|
||||||
let anorm = without_legacy_template_directives(afmt);
|
let anodes = ParsedTemplate::from_text(afmt)
|
||||||
let anodes = ParsedTemplate::from_text(anorm.as_ref())
|
|
||||||
.and_then(|tmpl| tmpl.render(&context))
|
.and_then(|tmpl| tmpl.render(&context))
|
||||||
.map_err(|e| template_error_to_anki_error(e, false, i18n))?;
|
.map_err(|e| template_error_to_anki_error(e, false, i18n))?;
|
||||||
|
|
||||||
|
|
@ -581,10 +585,7 @@ impl ParsedTemplate<'_> {
|
||||||
mod test {
|
mod test {
|
||||||
use super::{FieldMap, ParsedNode::*, ParsedTemplate as PT};
|
use super::{FieldMap, ParsedNode::*, ParsedTemplate as PT};
|
||||||
use crate::err::TemplateError;
|
use crate::err::TemplateError;
|
||||||
use crate::template::{
|
use crate::template::{field_is_empty, nonempty_fields, FieldRequirements, RenderContext};
|
||||||
field_is_empty, nonempty_fields, without_legacy_template_directives, FieldRequirements,
|
|
||||||
RenderContext,
|
|
||||||
};
|
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::iter::FromIterator;
|
use std::iter::FromIterator;
|
||||||
|
|
||||||
|
|
@ -722,12 +723,21 @@ mod test {
|
||||||
<%Front%>
|
<%Front%>
|
||||||
<% #Back %>
|
<% #Back %>
|
||||||
<%/Back%>";
|
<%/Back%>";
|
||||||
let output = "
|
assert_eq!(
|
||||||
{{Front}}
|
PT::from_text(input).unwrap().0,
|
||||||
{{ #Back }}
|
vec![
|
||||||
{{/Back}}";
|
Text("\n"),
|
||||||
|
Replacement {
|
||||||
assert_eq!(without_legacy_template_directives(input), output);
|
key: "Front",
|
||||||
|
filters: vec![]
|
||||||
|
},
|
||||||
|
Text("\n"),
|
||||||
|
Conditional {
|
||||||
|
key: "Back",
|
||||||
|
children: vec![Text("\n")]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue