Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ fancy-regex = "0.5.0"
serde = { version = "1.0", features = ["derive"] }
ramhorns = "0.10.2"
thiserror = "1.0.32"
once_cell = "1.18.0"

[dev-dependencies]
anyhow = "1.0.62"
Expand Down
4 changes: 4 additions & 0 deletions src/builders/field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ impl Field {
self.size = Some(value);
self
}

pub fn name(&self) -> &str {
&self.name
}
}

impl Into<Fld> for Field {
Expand Down
37 changes: 25 additions & 12 deletions src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::{Error, Field};
use fancy_regex::Regex;
use ramhorns::Template as RamTemplate;
use std::collections::HashMap;
use std::rc::Rc;

const DEFAULT_LATEX_PRE: &str = r#"
\documentclass[12pt]{article}
Expand All @@ -17,6 +18,7 @@ const DEFAULT_LATEX_PRE: &str = r#"

"#;
const DEFAULT_LATEX_POST: &str = r"\end{document}";
const SENTINEL: &str = "SeNtInEl";

/// `FrontBack` or `Cloze` to determine the type of a Model.
///
Expand All @@ -39,6 +41,7 @@ pub struct Model {
latex_pre: String,
latex_post: String,
sort_field_index: i64,
sentinel_regexes: Rc<Vec<Regex>>, // Rc<_> so this can be clone
}

impl Model {
Expand Down Expand Up @@ -68,6 +71,7 @@ impl Model {
latex_pre: DEFAULT_LATEX_PRE.to_string(),
latex_post: DEFAULT_LATEX_POST.to_string(),
sort_field_index: 0,
sentinel_regexes: compile_sentinel_regexes(&fields),
}
}

Expand Down Expand Up @@ -99,6 +103,7 @@ impl Model {
latex_pre: latex_pre.unwrap_or(DEFAULT_LATEX_PRE).to_string(),
latex_post: latex_post.unwrap_or(DEFAULT_LATEX_POST).to_string(),
sort_field_index: sort_field_index.unwrap_or(0),
sentinel_regexes: compile_sentinel_regexes(&fields),
}
}

Expand Down Expand Up @@ -152,11 +157,10 @@ impl Model {
}

pub(super) fn req(&self) -> Result<Vec<(usize, String, Vec<usize>)>, Error> {
let sentinel = "SeNtInEl".to_string();
let field_names: Vec<String> = self.fields.iter().map(|field| field.name.clone()).collect();
let field_values = field_names
.iter()
.map(|field| (field.as_str(), format!("{}{}", &field, &sentinel)));
.map(|field| (field.as_str(), format!("{}{}", &field, &SENTINEL)));
let mut req = Vec::new();
for (template_ord, template) in self.templates.iter().enumerate() {
let rendered = RamTemplate::new(template.qfmt.clone())
Expand All @@ -165,7 +169,7 @@ impl Model {
let required_fields = field_values
.clone()
.enumerate()
.filter(|(_, (_, field))| !contains_other_fields(&rendered, field, &sentinel))
.filter(|(field_ord, _)| !self.contains_other_fields(&rendered, *field_ord))
.map(|(field_ord, _)| field_ord)
.collect::<Vec<_>>();
if !required_fields.is_empty() {
Expand Down Expand Up @@ -239,17 +243,26 @@ impl Model {
.map_err(json_error)?,
)
}

fn contains_other_fields(&self, rendered: &str, field_ord: usize) -> bool {
self.sentinel_regexes[field_ord].is_match(rendered).unwrap()
}
}

fn contains_other_fields(rendered: &str, current_field: &str, sentinel: &str) -> bool {
Regex::new(&format!(
"(?!{field}\\b)\\b(\\w)*{sentinel}+",
field = current_field,
sentinel = sentinel
))
.unwrap()
.is_match(rendered)
.unwrap()
fn compile_sentinel_regexes(fields: &[Field]) -> Rc<Vec<Regex>> {
Rc::new(
fields
.iter()
.map(|f| {
Regex::new(&format!(
"(?!{field}{sentinel}\\b)\\b(\\w)*{sentinel}+",
field = f.name(),
sentinel = SENTINEL
))
.unwrap()
})
.collect(),
)
}

#[cfg(test)]
Expand Down
29 changes: 19 additions & 10 deletions src/note.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,20 @@ use std::collections::HashSet;
use std::ops::RangeFrom;
use std::str::FromStr;

use once_cell::sync::Lazy;

static CLOZE_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"{{[^}]*?cloze:(?:[^}]?:)*(.+?)}}").expect("static regex"));

static CLOZE2_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new("<%cloze:(.+?)%>").expect("static regex"));

static UPDATES_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?s){{c(\d+)::.+?}}").expect("static regex"));

static INVALID_HTML_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"<(?!/?[a-z0-9]+(?: .*|/?)>)(?:.|\n)*?>").expect("static regex"));

/// Note (Flashcard) to be added to a `Deck`
#[derive(Clone)]
pub struct Note {
Expand Down Expand Up @@ -194,11 +208,8 @@ impl Note {
fn cloze_cards(model: &Model, self_fields: &Vec<String>) -> Vec<Card> {
let mut card_ords: HashSet<i64> = HashSet::new();
let mut cloze_replacements: HashSet<String> = HashSet::new();
cloze_replacements.extend(re_findall(
r"{{[^}]*?cloze:(?:[^}]?:)*(.+?)}}",
&model.templates()[0].qfmt,
));
cloze_replacements.extend(re_findall("<%cloze:(.+?)%>", &model.templates()[0].qfmt));
cloze_replacements.extend(re_findall(&CLOZE_REGEX, &model.templates()[0].qfmt));
cloze_replacements.extend(re_findall(&CLOZE2_REGEX, &model.templates()[0].qfmt));
for field_name in cloze_replacements {
let fields = model.fields();
let mut field_index_iter = fields
Expand All @@ -211,7 +222,7 @@ fn cloze_cards(model: &Model, self_fields: &Vec<String>) -> Vec<Card> {
} else {
"".to_string()
};
let updates_str = re_findall(r"(?s){{c(\d+)::.+?}}", &field_value);
let updates_str = re_findall(&UPDATES_REGEX, &field_value);
let updates = updates_str
.iter()
.map(|m| i64::from_str(m).expect("parsed from regex") - 1)
Expand Down Expand Up @@ -243,8 +254,7 @@ fn front_back_cards(model: &Model, self_fields: &Vec<String>) -> Result<Vec<Card
Ok(rv)
}

fn re_findall(regex_str: &'static str, to_match: &str) -> Vec<String> {
let regex = Regex::new(regex_str).expect("static regex");
fn re_findall(regex: &Regex, to_match: &str) -> Vec<String> {
regex
.captures_iter(to_match)
.filter_map(|m| m.ok())
Expand All @@ -268,8 +278,7 @@ fn validate_tags(tags: &Vec<String>) -> Result<(), Error> {
}

fn find_invalid_html_tags_in_field(field: &str) -> Vec<String> {
let regex = Regex::new(r"<(?!/?[a-z0-9]+(?: .*|/?)>)(?:.|\n)*?>").unwrap();
regex
INVALID_HTML_REGEX
.find_iter(field)
.map(|m| m.unwrap().as_str().to_string())
.collect()
Expand Down