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)