Pronounce "[...]" as "blank" with TTS

This commit is contained in:
RumovZ 2021-05-20 09:42:38 +02:00
parent 363548e8a2
commit b5c29fb498
3 changed files with 40 additions and 29 deletions

View File

@ -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 = card-templates-changes-will-affect-notes =
{ $count -> { $count ->
[one] Changes below will affect the { $count } note that uses this card type. [one] Changes below will affect the { $count } note that uses this card type.

View File

@ -385,10 +385,10 @@ impl ParsedTemplate {
/// Replacements that use only standard filters will become part of /// Replacements that use only standard filters will become part of
/// a text node. If a non-standard filter is encountered, a partially /// a text node. If a non-standard filter is encountered, a partially
/// rendered Replacement is returned for the calling code to complete. /// rendered Replacement is returned for the calling code to complete.
fn render(&self, context: &RenderContext) -> TemplateResult<Vec<RenderedNode>> { fn render(&self, context: &RenderContext, tr: &I18n) -> TemplateResult<Vec<RenderedNode>> {
let mut rendered = vec![]; 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) Ok(rendered)
} }
@ -398,6 +398,7 @@ fn render_into(
rendered_nodes: &mut Vec<RenderedNode>, rendered_nodes: &mut Vec<RenderedNode>,
nodes: &[ParsedNode], nodes: &[ParsedNode],
context: &RenderContext, context: &RenderContext,
tr: &I18n,
) -> TemplateResult<()> { ) -> TemplateResult<()> {
use ParsedNode::*; use ParsedNode::*;
for node in nodes { for node in nodes {
@ -436,6 +437,7 @@ fn render_into(
.as_slice(), .as_slice(),
key, key,
context, context,
tr,
), ),
None => { None => {
// unknown field encountered // unknown field encountered
@ -466,12 +468,12 @@ fn render_into(
} }
Conditional { key, children } => { Conditional { key, children } => {
if context.nonempty_fields.contains(key.as_str()) { 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 } => { NegatedConditional { key, children } => {
if !context.nonempty_fields.contains(key.as_str()) { 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 // question side
let (mut qnodes, qtmpl) = ParsedTemplate::from_text(qfmt) 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))?; .map_err(|e| template_error_to_anki_error(e, true, tr))?;
// check if the front side was empty // check if the front side was empty
@ -580,7 +582,7 @@ pub fn render_card(
// answer side // answer side
context.question_side = false; context.question_side = false;
let anodes = ParsedTemplate::from_text(afmt) 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))?; .map_err(|e| template_error_to_anki_error(e, false, tr))?;
Ok((qnodes, anodes)) Ok((qnodes, anodes))
@ -1021,8 +1023,9 @@ mod test {
use crate::template::RenderedNode as FN; use crate::template::RenderedNode as FN;
let mut tmpl = PT::from_text("{{B}}A{{F}}").unwrap(); let mut tmpl = PT::from_text("{{B}}A{{F}}").unwrap();
let tr = I18n::template_only();
assert_eq!( assert_eq!(
tmpl.render(&ctx).unwrap(), tmpl.render(&ctx, &tr).unwrap(),
vec![FN::Text { vec![FN::Text {
text: "bAf".to_owned() text: "bAf".to_owned()
},] },]
@ -1030,12 +1033,12 @@ mod test {
// empty // empty
tmpl = PT::from_text("{{#E}}A{{/E}}").unwrap(); tmpl = PT::from_text("{{#E}}A{{/E}}").unwrap();
assert_eq!(tmpl.render(&ctx).unwrap(), vec![]); assert_eq!(tmpl.render(&ctx, &tr).unwrap(), vec![]);
// missing // missing
tmpl = PT::from_text("{{^M}}A{{/M}}").unwrap(); tmpl = PT::from_text("{{^M}}A{{/M}}").unwrap();
assert_eq!( assert_eq!(
tmpl.render(&ctx).unwrap(), tmpl.render(&ctx, &tr).unwrap(),
vec![FN::Text { vec![FN::Text {
text: "A".to_owned() text: "A".to_owned()
},] },]
@ -1044,7 +1047,7 @@ mod test {
// nested // nested
tmpl = PT::from_text("{{^E}}1{{#F}}2{{#B}}{{F}}{{/B}}{{/F}}{{/E}}").unwrap(); tmpl = PT::from_text("{{^E}}1{{#F}}2{{#B}}{{F}}{{/B}}{{/F}}{{/E}}").unwrap();
assert_eq!( assert_eq!(
tmpl.render(&ctx).unwrap(), tmpl.render(&ctx, &tr).unwrap(),
vec![FN::Text { vec![FN::Text {
text: "12f".to_owned() text: "12f".to_owned()
},] },]
@ -1053,7 +1056,7 @@ mod test {
// unknown filters // unknown filters
tmpl = PT::from_text("{{one:two:B}}").unwrap(); tmpl = PT::from_text("{{one:two:B}}").unwrap();
assert_eq!( assert_eq!(
tmpl.render(&ctx).unwrap(), tmpl.render(&ctx, &tr).unwrap(),
vec![FN::Replacement { vec![FN::Replacement {
field_name: "B".to_owned(), field_name: "B".to_owned(),
filters: vec!["two".to_string(), "one".to_string()], filters: vec!["two".to_string(), "one".to_string()],
@ -1065,7 +1068,7 @@ mod test {
// excess colons are ignored // excess colons are ignored
tmpl = PT::from_text("{{one::text:B}}").unwrap(); tmpl = PT::from_text("{{one::text:B}}").unwrap();
assert_eq!( assert_eq!(
tmpl.render(&ctx).unwrap(), tmpl.render(&ctx, &tr).unwrap(),
vec![FN::Replacement { vec![FN::Replacement {
field_name: "B".to_owned(), field_name: "B".to_owned(),
filters: vec!["one".to_string()], filters: vec!["one".to_string()],
@ -1076,7 +1079,7 @@ mod test {
// known filter // known filter
tmpl = PT::from_text("{{text:B}}").unwrap(); tmpl = PT::from_text("{{text:B}}").unwrap();
assert_eq!( assert_eq!(
tmpl.render(&ctx).unwrap(), tmpl.render(&ctx, &tr).unwrap(),
vec![FN::Text { vec![FN::Text {
text: "b".to_owned() text: "b".to_owned()
}] }]
@ -1085,7 +1088,7 @@ mod test {
// unknown field // unknown field
tmpl = PT::from_text("{{X}}").unwrap(); tmpl = PT::from_text("{{X}}").unwrap();
assert_eq!( assert_eq!(
tmpl.render(&ctx).unwrap_err(), tmpl.render(&ctx, &tr).unwrap_err(),
TemplateError::FieldNotFound { TemplateError::FieldNotFound {
field: "X".to_owned(), field: "X".to_owned(),
filters: "".to_owned() filters: "".to_owned()
@ -1095,7 +1098,7 @@ mod test {
// unknown field with filters // unknown field with filters
tmpl = PT::from_text("{{foo:text:X}}").unwrap(); tmpl = PT::from_text("{{foo:text:X}}").unwrap();
assert_eq!( assert_eq!(
tmpl.render(&ctx).unwrap_err(), tmpl.render(&ctx, &tr).unwrap_err(),
TemplateError::FieldNotFound { TemplateError::FieldNotFound {
field: "X".to_owned(), field: "X".to_owned(),
filters: "foo:text:".to_owned() filters: "foo:text:".to_owned()
@ -1105,7 +1108,7 @@ mod test {
// a blank field is allowed if it has filters // a blank field is allowed if it has filters
tmpl = PT::from_text("{{filter:}}").unwrap(); tmpl = PT::from_text("{{filter:}}").unwrap();
assert_eq!( assert_eq!(
tmpl.render(&ctx).unwrap(), tmpl.render(&ctx, &tr).unwrap(),
vec![FN::Replacement { vec![FN::Replacement {
field_name: "".to_string(), field_name: "".to_string(),
current_text: "".to_string(), current_text: "".to_string(),

View File

@ -9,6 +9,7 @@ use regex::{Captures, Regex};
use crate::{ use crate::{
cloze::{cloze_filter, cloze_only_filter}, cloze::{cloze_filter, cloze_only_filter},
i18n::I18n,
template::RenderContext, template::RenderContext,
text::strip_html, text::strip_html,
}; };
@ -25,6 +26,7 @@ pub(crate) fn apply_filters<'a>(
filters: &[&str], filters: &[&str],
field_name: &str, field_name: &str,
context: &RenderContext, context: &RenderContext,
tr: &I18n,
) -> (Cow<'a, str>, Vec<String>) { ) -> (Cow<'a, str>, Vec<String>) {
let mut text: Cow<str> = text.into(); let mut text: Cow<str> = text.into();
@ -36,7 +38,7 @@ pub(crate) fn apply_filters<'a>(
}; };
for (idx, &filter_name) in filters.iter().enumerate() { 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) => { (true, None) => {
// filter did not change text // filter did not change text
} }
@ -67,6 +69,7 @@ fn apply_filter<'a>(
text: &'a str, text: &'a str,
field_name: &str, field_name: &str,
context: &RenderContext, context: &RenderContext,
tr: &I18n,
) -> (bool, Option<String>) { ) -> (bool, Option<String>) {
let output_text = match filter_name { let output_text = match filter_name {
"text" => strip_html(text), "text" => strip_html(text),
@ -82,7 +85,7 @@ fn apply_filter<'a>(
"" => text.into(), "" => text.into(),
_ => { _ => {
if filter_name.starts_with("tts ") { if filter_name.starts_with("tts ") {
tts_filter(filter_name, text) tts_filter(filter_name, text, tr)
} else { } else {
// unrecognized filter // unrecognized filter
return (false, None); return (false, None);
@ -191,8 +194,9 @@ return false;">
.into() .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 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() 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)] #[cfg(test)]
mod test { mod test {
use crate::{ use super::*;
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,
};
#[test] #[test]
fn furigana() { fn furigana() {
@ -238,6 +235,7 @@ field</a>
#[test] #[test]
fn typing() { fn typing() {
let tr = I18n::template_only();
assert_eq!(type_filter("Front"), "[[type:Front]]"); assert_eq!(type_filter("Front"), "[[type:Front]]");
assert_eq!(type_cloze_filter("Front"), "[[type:cloze:Front]]"); assert_eq!(type_cloze_filter("Front"), "[[type:cloze:Front]]");
let ctx = RenderContext { let ctx = RenderContext {
@ -247,7 +245,7 @@ field</a>
card_ord: 0, card_ord: 0,
}; };
assert_eq!( assert_eq!(
apply_filters("ignored", &["cloze", "type"], "Text", &ctx), apply_filters("ignored", &["cloze", "type"], "Text", &ctx, &tr),
("[[type:cloze:Text]]".into(), vec![]) ("[[type:cloze:Text]]".into(), vec![])
); );
} }
@ -282,9 +280,17 @@ field</a>
#[test] #[test]
fn tts() { fn tts() {
let tr = I18n::template_only();
assert_eq!( 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]" "[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()
)
);
} }
} }