More template checks (#2032)

* Show warning if multiple type boxes are used

* Report templates referencing media in Media Check

* Apply suggestions from code review

* Fix media-check.ftl

* Only report media references with fields

Like `<img src={{Front}}>`.
Also report Anki sound tags and latex.

* Loop existing media regexes
This commit is contained in:
RumovZ 2022-09-05 08:52:25 +02:00 committed by GitHub
parent e39fb74e82
commit e7af0febb1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 175 additions and 5 deletions

View file

@ -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.

View file

@ -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 }...

View file

@ -576,6 +576,7 @@ class CardLayout(QDialog):
res = f"<hr id=answer>{res}"
return res
type_filter = r"\[\[type:.+?\]\]"
repl: Union[str, Callable]
if type == "q":
@ -583,7 +584,10 @@ class CardLayout(QDialog):
repl = f"<center>{repl}</center>"
else:
repl = answerRepl
return re.sub(r"\[\[type:.+?\]\]", repl, txt)
out = re.sub(type_filter, repl, txt, count=1)
warning = f"<center><b>{tr.card_templates_type_boxes_warning()}</center><b>"
return re.sub(type_filter, warning, out)
# Card operations
######################################################################

View file

@ -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,

View file

@ -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
|

View file

@ -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<Item = &'a Notetype>,
) -> Vec<Template<'a>> {
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(&notetype.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, "<img src=baz>", "<img src={{baz}}>");
let templates = media_field_referencing_templates(once(&nt));
let expected = Template {
notetype,
card_type,
front: false,
};
assert_eq!(templates, &[expected]);
}
}

View file

@ -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;

View file

@ -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)<br\s*/?>|<div>|\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<str> {
.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)