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 =
{ $count ->
[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
/// 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<Vec<RenderedNode>> {
fn render(&self, context: &RenderContext, tr: &I18n) -> TemplateResult<Vec<RenderedNode>> {
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<RenderedNode>,
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(),

View File

@ -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<String>) {
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() {
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<String>) {
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</a>
#[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</a>
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</a>
#[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()
)
);
}
}