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?
|
||||
*[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-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 }...
|
||||
|
|
|
@ -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
|
||||
######################################################################
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
||||
|
|
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
|
||||
|
||||
mod cardgen;
|
||||
mod checks;
|
||||
mod emptycards;
|
||||
mod fields;
|
||||
mod notetypechange;
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue