From b5c29fb4989837de8effcd5453a004775ab48098 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Thu, 20 May 2021 09:42:38 +0200 Subject: [PATCH] Pronounce "[...]" as "blank" with TTS --- ftl/core/card-templates.ftl | 2 ++ rslib/src/template.rs | 35 +++++++++++++++++++---------------- rslib/src/template_filters.rs | 32 +++++++++++++++++++------------- 3 files changed, 40 insertions(+), 29 deletions(-) diff --git a/ftl/core/card-templates.ftl b/ftl/core/card-templates.ftl index 6ee294f2c..544d6d3bd 100644 --- a/ftl/core/card-templates.ftl +++ b/ftl/core/card-templates.ftl @@ -1,3 +1,5 @@ +# This word is used by TTS voices instead of the elided part of a cloze. +card-templates-blank = blank card-templates-changes-will-affect-notes = { $count -> [one] Changes below will affect the { $count } note that uses this card type. diff --git a/rslib/src/template.rs b/rslib/src/template.rs index 719e10df0..338558139 100644 --- a/rslib/src/template.rs +++ b/rslib/src/template.rs @@ -385,10 +385,10 @@ impl ParsedTemplate { /// Replacements that use only standard filters will become part of /// a text node. If a non-standard filter is encountered, a partially /// rendered Replacement is returned for the calling code to complete. - fn render(&self, context: &RenderContext) -> TemplateResult> { + fn render(&self, context: &RenderContext, tr: &I18n) -> TemplateResult> { let mut rendered = vec![]; - render_into(&mut rendered, self.0.as_ref(), context)?; + render_into(&mut rendered, self.0.as_ref(), context, tr)?; Ok(rendered) } @@ -398,6 +398,7 @@ fn render_into( rendered_nodes: &mut Vec, nodes: &[ParsedNode], context: &RenderContext, + tr: &I18n, ) -> TemplateResult<()> { use ParsedNode::*; for node in nodes { @@ -436,6 +437,7 @@ fn render_into( .as_slice(), key, context, + tr, ), None => { // unknown field encountered @@ -466,12 +468,12 @@ fn render_into( } Conditional { key, children } => { if context.nonempty_fields.contains(key.as_str()) { - render_into(rendered_nodes, children.as_ref(), context)?; + render_into(rendered_nodes, children.as_ref(), context, tr)?; } } NegatedConditional { key, children } => { if !context.nonempty_fields.contains(key.as_str()) { - render_into(rendered_nodes, children.as_ref(), context)?; + render_into(rendered_nodes, children.as_ref(), context, tr)?; } } }; @@ -551,7 +553,7 @@ pub fn render_card( // question side let (mut qnodes, qtmpl) = ParsedTemplate::from_text(qfmt) - .and_then(|tmpl| Ok((tmpl.render(&context)?, tmpl))) + .and_then(|tmpl| Ok((tmpl.render(&context, tr)?, tmpl))) .map_err(|e| template_error_to_anki_error(e, true, tr))?; // check if the front side was empty @@ -580,7 +582,7 @@ pub fn render_card( // answer side context.question_side = false; let anodes = ParsedTemplate::from_text(afmt) - .and_then(|tmpl| tmpl.render(&context)) + .and_then(|tmpl| tmpl.render(&context, tr)) .map_err(|e| template_error_to_anki_error(e, false, tr))?; Ok((qnodes, anodes)) @@ -1021,8 +1023,9 @@ mod test { use crate::template::RenderedNode as FN; let mut tmpl = PT::from_text("{{B}}A{{F}}").unwrap(); + let tr = I18n::template_only(); assert_eq!( - tmpl.render(&ctx).unwrap(), + tmpl.render(&ctx, &tr).unwrap(), vec![FN::Text { text: "bAf".to_owned() },] @@ -1030,12 +1033,12 @@ mod test { // empty tmpl = PT::from_text("{{#E}}A{{/E}}").unwrap(); - assert_eq!(tmpl.render(&ctx).unwrap(), vec![]); + assert_eq!(tmpl.render(&ctx, &tr).unwrap(), vec![]); // missing tmpl = PT::from_text("{{^M}}A{{/M}}").unwrap(); assert_eq!( - tmpl.render(&ctx).unwrap(), + tmpl.render(&ctx, &tr).unwrap(), vec![FN::Text { text: "A".to_owned() },] @@ -1044,7 +1047,7 @@ mod test { // nested tmpl = PT::from_text("{{^E}}1{{#F}}2{{#B}}{{F}}{{/B}}{{/F}}{{/E}}").unwrap(); assert_eq!( - tmpl.render(&ctx).unwrap(), + tmpl.render(&ctx, &tr).unwrap(), vec![FN::Text { text: "12f".to_owned() },] @@ -1053,7 +1056,7 @@ mod test { // unknown filters tmpl = PT::from_text("{{one:two:B}}").unwrap(); assert_eq!( - tmpl.render(&ctx).unwrap(), + tmpl.render(&ctx, &tr).unwrap(), vec![FN::Replacement { field_name: "B".to_owned(), filters: vec!["two".to_string(), "one".to_string()], @@ -1065,7 +1068,7 @@ mod test { // excess colons are ignored tmpl = PT::from_text("{{one::text:B}}").unwrap(); assert_eq!( - tmpl.render(&ctx).unwrap(), + tmpl.render(&ctx, &tr).unwrap(), vec![FN::Replacement { field_name: "B".to_owned(), filters: vec!["one".to_string()], @@ -1076,7 +1079,7 @@ mod test { // known filter tmpl = PT::from_text("{{text:B}}").unwrap(); assert_eq!( - tmpl.render(&ctx).unwrap(), + tmpl.render(&ctx, &tr).unwrap(), vec![FN::Text { text: "b".to_owned() }] @@ -1085,7 +1088,7 @@ mod test { // unknown field tmpl = PT::from_text("{{X}}").unwrap(); assert_eq!( - tmpl.render(&ctx).unwrap_err(), + tmpl.render(&ctx, &tr).unwrap_err(), TemplateError::FieldNotFound { field: "X".to_owned(), filters: "".to_owned() @@ -1095,7 +1098,7 @@ mod test { // unknown field with filters tmpl = PT::from_text("{{foo:text:X}}").unwrap(); assert_eq!( - tmpl.render(&ctx).unwrap_err(), + tmpl.render(&ctx, &tr).unwrap_err(), TemplateError::FieldNotFound { field: "X".to_owned(), filters: "foo:text:".to_owned() @@ -1105,7 +1108,7 @@ mod test { // a blank field is allowed if it has filters tmpl = PT::from_text("{{filter:}}").unwrap(); assert_eq!( - tmpl.render(&ctx).unwrap(), + tmpl.render(&ctx, &tr).unwrap(), vec![FN::Replacement { field_name: "".to_string(), current_text: "".to_string(), diff --git a/rslib/src/template_filters.rs b/rslib/src/template_filters.rs index 892f70b66..49e540b8a 100644 --- a/rslib/src/template_filters.rs +++ b/rslib/src/template_filters.rs @@ -9,6 +9,7 @@ use regex::{Captures, Regex}; use crate::{ cloze::{cloze_filter, cloze_only_filter}, + i18n::I18n, template::RenderContext, text::strip_html, }; @@ -25,6 +26,7 @@ pub(crate) fn apply_filters<'a>( filters: &[&str], field_name: &str, context: &RenderContext, + tr: &I18n, ) -> (Cow<'a, str>, Vec) { let mut text: Cow = text.into(); @@ -36,7 +38,7 @@ pub(crate) fn apply_filters<'a>( }; for (idx, &filter_name) in filters.iter().enumerate() { - match apply_filter(filter_name, text.as_ref(), field_name, context) { + match apply_filter(filter_name, text.as_ref(), field_name, context, tr) { (true, None) => { // filter did not change text } @@ -67,6 +69,7 @@ fn apply_filter<'a>( text: &'a str, field_name: &str, context: &RenderContext, + tr: &I18n, ) -> (bool, Option) { let output_text = match filter_name { "text" => strip_html(text), @@ -82,7 +85,7 @@ fn apply_filter<'a>( "" => text.into(), _ => { if filter_name.starts_with("tts ") { - tts_filter(filter_name, text) + tts_filter(filter_name, text, tr) } else { // unrecognized filter return (false, None); @@ -191,8 +194,9 @@ return false;"> .into() } -fn tts_filter(filter_name: &str, text: &str) -> Cow<'static, str> { +fn tts_filter(filter_name: &str, text: &str, tr: &I18n) -> Cow<'static, str> { let args = filter_name.splitn(2, ' ').nth(1).unwrap_or(""); + let text = text.replace("[...]", &tr.card_templates_blank()); format!("[anki:tts][{}]{}[/anki:tts]", args, text).into() } @@ -201,14 +205,7 @@ fn tts_filter(filter_name: &str, text: &str) -> Cow<'static, str> { #[cfg(test)] mod test { - use crate::{ - template::RenderContext, - template_filters::{ - apply_filters, cloze_filter, furigana_filter, hint_filter, kana_filter, kanji_filter, - tts_filter, type_cloze_filter, type_filter, - }, - text::strip_html, - }; + use super::*; #[test] fn furigana() { @@ -238,6 +235,7 @@ field #[test] fn typing() { + let tr = I18n::template_only(); assert_eq!(type_filter("Front"), "[[type:Front]]"); assert_eq!(type_cloze_filter("Front"), "[[type:cloze:Front]]"); let ctx = RenderContext { @@ -247,7 +245,7 @@ field card_ord: 0, }; assert_eq!( - apply_filters("ignored", &["cloze", "type"], "Text", &ctx), + apply_filters("ignored", &["cloze", "type"], "Text", &ctx, &tr), ("[[type:cloze:Text]]".into(), vec![]) ); } @@ -282,9 +280,17 @@ field #[test] fn tts() { + let tr = I18n::template_only(); assert_eq!( - tts_filter("tts en_US voices=Bob,Jane", "foo"), + tts_filter("tts en_US voices=Bob,Jane", "foo", &tr), "[anki:tts][en_US voices=Bob,Jane]foo[/anki:tts]" ); + assert_eq!( + tts_filter("tts en_US", "foo [...]", &tr), + format!( + "[anki:tts][en_US]foo {}[/anki:tts]", + tr.card_templates_blank() + ) + ); } }