Pronounce "[...]" as "blank" with TTS
This commit is contained in:
parent
363548e8a2
commit
b5c29fb498
@ -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.
|
||||||
|
@ -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(),
|
||||||
|
@ -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()
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user