mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
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:
parent
e39fb74e82
commit
e7af0febb1
8 changed files with 175 additions and 5 deletions
|
@ -56,3 +56,4 @@ card-templates-this-will-create-card-proceed =
|
||||||
[one] This will create { $count } card. Proceed?
|
[one] This will create { $count } card. Proceed?
|
||||||
*[other] This will create { $count } cards. Proceed?
|
*[other] This will create { $count } cards. Proceed?
|
||||||
}
|
}
|
||||||
|
card-templates-type-boxes-warning = Only one typing box per card template is supported.
|
||||||
|
|
|
@ -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-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-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-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
|
## Shown once for each file
|
||||||
|
|
||||||
|
@ -31,6 +35,11 @@ media-check-subfolder-file = Folder: { $filename }
|
||||||
media-check-missing-file = Missing: { $filename }
|
media-check-missing-file = Missing: { $filename }
|
||||||
media-check-unused-file = Unused: { $filename }
|
media-check-unused-file = Unused: { $filename }
|
||||||
|
|
||||||
|
##
|
||||||
|
|
||||||
|
# Eg "Basic: Card 1 (Front Template)"
|
||||||
|
media-check-notetype-template = { $notetype }: { $card_type } ({ $side })
|
||||||
|
|
||||||
## Progress
|
## Progress
|
||||||
|
|
||||||
media-check-checked = Checked { $count }...
|
media-check-checked = Checked { $count }...
|
||||||
|
|
|
@ -576,6 +576,7 @@ class CardLayout(QDialog):
|
||||||
res = f"<hr id=answer>{res}"
|
res = f"<hr id=answer>{res}"
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
type_filter = r"\[\[type:.+?\]\]"
|
||||||
repl: Union[str, Callable]
|
repl: Union[str, Callable]
|
||||||
|
|
||||||
if type == "q":
|
if type == "q":
|
||||||
|
@ -583,7 +584,10 @@ class CardLayout(QDialog):
|
||||||
repl = f"<center>{repl}</center>"
|
repl = f"<center>{repl}</center>"
|
||||||
else:
|
else:
|
||||||
repl = answerRepl
|
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
|
# Card operations
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
|
@ -23,7 +23,8 @@ impl MediaService for Backend {
|
||||||
let mut checker = MediaChecker::new(ctx, &mgr, progress_fn);
|
let mut checker = MediaChecker::new(ctx, &mgr, progress_fn);
|
||||||
let mut output = checker.check()?;
|
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 {
|
Ok(pb::CheckMediaResponse {
|
||||||
unused: output.unused,
|
unused: output.unused,
|
||||||
|
|
|
@ -9,7 +9,7 @@ use regex::{Captures, Regex};
|
||||||
use crate::{cloze::expand_clozes_to_reveal_latex, media::files::sha1_of_data, text::strip_html};
|
use crate::{cloze::expand_clozes_to_reveal_latex, media::files::sha1_of_data, text::strip_html};
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref LATEX: Regex = Regex::new(
|
pub(crate) static ref LATEX: Regex = Regex::new(
|
||||||
r#"(?xsi)
|
r#"(?xsi)
|
||||||
\[latex\](.+?)\[/latex\] # 1 - standard latex
|
\[latex\](.+?)\[/latex\] # 1 - standard latex
|
||||||
|
|
|
|
||||||
|
|
150
rslib/src/notetype/checks.rs
Normal file
150
rslib/src/notetype/checks.rs
Normal 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(¬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, "<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]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,7 @@
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
mod cardgen;
|
mod cardgen;
|
||||||
|
mod checks;
|
||||||
mod emptycards;
|
mod emptycards;
|
||||||
mod fields;
|
mod fields;
|
||||||
mod notetypechange;
|
mod notetypechange;
|
||||||
|
|
|
@ -91,7 +91,7 @@ lazy_static! {
|
||||||
"#
|
"#
|
||||||
).unwrap();
|
).unwrap();
|
||||||
|
|
||||||
static ref HTML_MEDIA_TAGS: Regex = Regex::new(
|
pub(crate) static ref HTML_MEDIA_TAGS: Regex = Regex::new(
|
||||||
r#"(?xsi)
|
r#"(?xsi)
|
||||||
# the start of the image, audio, or object tag
|
# the start of the image, audio, or object tag
|
||||||
<\b(?:img|audio|object)\b[^>]+\b(?:src|data)\b=
|
<\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 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 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.
|
/// Files included in CSS with a leading underscore.
|
||||||
static ref UNDERSCORED_CSS_IMPORTS: Regex = Regex::new(
|
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)
|
.map_cow(strip_html)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn contains_media_tag(html: &str) -> bool {
|
||||||
|
HTML_MEDIA_TAGS.is_match(html)
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub(crate) fn sanitize_html(html: &str) -> String {
|
pub(crate) fn sanitize_html(html: &str) -> String {
|
||||||
ammonia::clean(html)
|
ammonia::clean(html)
|
||||||
|
|
Loading…
Reference in a new issue