diff --git a/ftl/core/card-templates.ftl b/ftl/core/card-templates.ftl index f1b543220..248a34a06 100644 --- a/ftl/core/card-templates.ftl +++ b/ftl/core/card-templates.ftl @@ -56,3 +56,4 @@ card-templates-this-will-create-card-proceed = [one] This will create { $count } card. Proceed? *[other] This will create { $count } cards. Proceed? } +card-templates-type-boxes-warning = Only one typing box per card template is supported. diff --git a/ftl/core/media-check.ftl b/ftl/core/media-check.ftl index d4a0dd9c5..e9431859f 100644 --- a/ftl/core/media-check.ftl +++ b/ftl/core/media-check.ftl @@ -22,6 +22,10 @@ media-check-oversize-header = Files over 100MB can not be synced with AnkiWeb. media-check-subfolder-header = Folders inside the media folder are not supported. media-check-missing-header = The following files are referenced by cards, but were not found in the media folder: media-check-unused-header = The following files were found in the media folder, but do not appear to be used on any cards: +media-check-template-references-field-header = + Anki can not detect used files when you use { "{{Field}}" } references in media/LaTeX tags. The media/LaTeX tags should be placed on individual notes instead. + + Referencing templates: ## Shown once for each file @@ -31,6 +35,11 @@ media-check-subfolder-file = Folder: { $filename } media-check-missing-file = Missing: { $filename } media-check-unused-file = Unused: { $filename } +## + +# Eg "Basic: Card 1 (Front Template)" +media-check-notetype-template = { $notetype }: { $card_type } ({ $side }) + ## Progress media-check-checked = Checked { $count }... diff --git a/qt/aqt/clayout.py b/qt/aqt/clayout.py index be9d50160..db7ae7300 100644 --- a/qt/aqt/clayout.py +++ b/qt/aqt/clayout.py @@ -576,6 +576,7 @@ class CardLayout(QDialog): res = f"
{res}" return res + type_filter = r"\[\[type:.+?\]\]" repl: Union[str, Callable] if type == "q": @@ -583,7 +584,10 @@ class CardLayout(QDialog): repl = f"
{repl}
" else: repl = answerRepl - return re.sub(r"\[\[type:.+?\]\]", repl, txt) + out = re.sub(type_filter, repl, txt, count=1) + + warning = f"
{tr.card_templates_type_boxes_warning()}
" + return re.sub(type_filter, warning, out) # Card operations ###################################################################### diff --git a/rslib/src/backend/media.rs b/rslib/src/backend/media.rs index 1c681d8b7..dda0a2f5e 100644 --- a/rslib/src/backend/media.rs +++ b/rslib/src/backend/media.rs @@ -23,7 +23,8 @@ impl MediaService for Backend { let mut checker = MediaChecker::new(ctx, &mgr, progress_fn); let mut output = checker.check()?; - let report = checker.summarize_output(&mut output); + let mut report = checker.summarize_output(&mut output); + ctx.report_media_field_referencing_templates(&mut report)?; Ok(pb::CheckMediaResponse { unused: output.unused, diff --git a/rslib/src/latex.rs b/rslib/src/latex.rs index 435d2fdd0..8f467c262 100644 --- a/rslib/src/latex.rs +++ b/rslib/src/latex.rs @@ -9,7 +9,7 @@ use regex::{Captures, Regex}; use crate::{cloze::expand_clozes_to_reveal_latex, media::files::sha1_of_data, text::strip_html}; lazy_static! { - static ref LATEX: Regex = Regex::new( + pub(crate) static ref LATEX: Regex = Regex::new( r#"(?xsi) \[latex\](.+?)\[/latex\] # 1 - standard latex | diff --git a/rslib/src/notetype/checks.rs b/rslib/src/notetype/checks.rs new file mode 100644 index 000000000..91ae1af5a --- /dev/null +++ b/rslib/src/notetype/checks.rs @@ -0,0 +1,150 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use std::{borrow::Cow, fmt::Write, ops::Deref}; + +use anki_i18n::without_unicode_isolation; +use lazy_static::lazy_static; +use regex::{Captures, Match, Regex}; + +use super::CardTemplate; +use crate::{ + latex::LATEX, + prelude::*, + text::{HTML_MEDIA_TAGS, SOUND_TAG}, +}; + +#[derive(Debug, PartialEq)] +struct Template<'a> { + notetype: &'a str, + card_type: &'a str, + front: bool, +} + +lazy_static! { + static ref FIELD_REPLACEMENT: Regex = Regex::new(r"\{\{.+\}\}").unwrap(); +} + +impl Collection { + pub fn report_media_field_referencing_templates(&mut self, buf: &mut String) -> Result<()> { + let notetypes = self.get_all_notetypes()?; + let templates = media_field_referencing_templates(notetypes.values().map(Deref::deref)); + write_template_report(buf, &templates, &self.tr); + Ok(()) + } +} + +fn media_field_referencing_templates<'a>( + notetypes: impl Iterator, +) -> Vec> { + notetypes + .flat_map(|notetype| { + notetype.templates.iter().flat_map(|card_type| { + card_type.sides().into_iter().filter_map(|(format, front)| { + references_media_field(format) + .then(|| Template::new(¬etype.name, &card_type.name, front)) + }) + }) + }) + .collect() +} + +fn references_media_field(format: &str) -> bool { + for regex in [&*HTML_MEDIA_TAGS, &*SOUND_TAG, &*LATEX] { + if regex + .captures_iter(format) + .any(captures_contain_field_replacement) + { + return true; + } + } + false +} + +fn captures_contain_field_replacement(caps: Captures) -> bool { + caps.iter() + .skip(1) + .any(|opt| opt.map_or(false, match_contains_field_replacement)) +} + +fn match_contains_field_replacement(m: Match) -> bool { + FIELD_REPLACEMENT.is_match(m.as_str()) +} + +fn write_template_report(buf: &mut String, templates: &[Template], tr: &I18n) { + if templates.is_empty() { + return; + } + writeln!( + buf, + "\n{}", + &tr.media_check_template_references_field_header() + ) + .unwrap(); + for template in templates { + writeln!(buf, "{}", template.as_str(tr)).unwrap(); + } +} + +impl<'a> Template<'a> { + fn new(notetype: &'a str, card_type: &'a str, front: bool) -> Self { + Template { + notetype, + card_type, + front, + } + } + + fn as_str(&self, tr: &I18n) -> String { + without_unicode_isolation(&tr.media_check_notetype_template( + self.notetype, + self.card_type, + self.side_name(tr), + )) + } + + fn side_name<'tr>(&self, tr: &'tr I18n) -> Cow<'tr, str> { + if self.front { + tr.card_templates_front_template() + } else { + tr.card_templates_back_template() + } + } +} + +impl CardTemplate { + fn sides(&self) -> [(&str, bool); 2] { + [ + (&self.config.q_format, true), + (&self.config.a_format, false), + ] + } +} + +#[cfg(test)] +mod test { + use std::iter::once; + + use super::*; + + #[test] + fn should_report_media_field_referencing_template() { + let notetype = "foo"; + let card_type = "bar"; + let mut nt = Notetype { + name: notetype.into(), + ..Default::default() + }; + nt.add_field("baz"); + nt.add_template(card_type, "", ""); + + let templates = media_field_referencing_templates(once(&nt)); + + let expected = Template { + notetype, + card_type, + front: false, + }; + assert_eq!(templates, &[expected]); + } +} diff --git a/rslib/src/notetype/mod.rs b/rslib/src/notetype/mod.rs index e450b7b5f..56f70afe5 100644 --- a/rslib/src/notetype/mod.rs +++ b/rslib/src/notetype/mod.rs @@ -2,6 +2,7 @@ // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html mod cardgen; +mod checks; mod emptycards; mod fields; mod notetypechange; diff --git a/rslib/src/text.rs b/rslib/src/text.rs index cea3c627d..11ed8c986 100644 --- a/rslib/src/text.rs +++ b/rslib/src/text.rs @@ -91,7 +91,7 @@ lazy_static! { "# ).unwrap(); - static ref HTML_MEDIA_TAGS: Regex = Regex::new( + pub(crate) static ref HTML_MEDIA_TAGS: Regex = Regex::new( r#"(?xsi) # the start of the image, audio, or object tag <\b(?:img|audio|object)\b[^>]+\b(?:src|data)\b= @@ -135,7 +135,7 @@ lazy_static! { static ref PERSISTENT_HTML_SPACERS: Regex = Regex::new(r#"(?i)|
|\n"#).unwrap(); static ref TYPE_TAG: Regex = Regex::new(r"\[\[type:[^]]+\]\]").unwrap(); - static ref SOUND_TAG: Regex = Regex::new(r"\[sound:([^]]+)\]").unwrap(); + pub(crate) static ref SOUND_TAG: Regex = Regex::new(r"\[sound:([^]]+)\]").unwrap(); /// Files included in CSS with a leading underscore. static ref UNDERSCORED_CSS_IMPORTS: Regex = Regex::new( @@ -333,6 +333,10 @@ pub fn strip_html_preserving_media_filenames(html: &str) -> Cow { .map_cow(strip_html) } +pub fn contains_media_tag(html: &str) -> bool { + HTML_MEDIA_TAGS.is_match(html) +} + #[allow(dead_code)] pub(crate) fn sanitize_html(html: &str) -> String { ammonia::clean(html)