use fuzzy_matcher::FuzzyMatcher;
#[derive(Debug, Default, Clone, Copy, serde::Serialize, serde::Deserialize)]
pub enum TagMatch {
#[default]
Exact,
Prefix,
}
impl TagMatch {
pub fn cycle(self) -> Self {
match self {
TagMatch::Exact => TagMatch::Prefix,
TagMatch::Prefix => TagMatch::Exact,
}
}
}
#[derive(Debug, Default, Clone)]
pub struct Filter {
pub any: bool,
pub tag_match: TagMatch,
pub tags: Vec<(String, bool)>,
pub links: Vec<(String, bool)>,
pub blinks: Vec<(String, bool)>,
pub title: String,
pub full_text: Option<String>,
}
impl Filter {
pub fn new(filter_string: &str, any: bool, tag_match: TagMatch) -> Self {
let mut tags = Vec::new();
let mut links = Vec::new();
let mut blinks = Vec::new();
let mut title = String::new();
let (filters, full_text) = filter_string
.split_once('|')
.map(|(filters, rest)| (filters, Some(rest.to_lowercase())))
.unwrap_or((filter_string, None));
for word in filters.split_whitespace() {
if word.starts_with("!#") {
tags.push((word.trim_start_matches('!').to_string(), false));
continue;
}
if word.starts_with('#') {
tags.push((word.to_string(), true));
continue;
}
if word.starts_with("!>") {
links.push((
super::name_to_id(word.trim_start_matches("!>")).to_string(),
false,
));
continue;
}
if word.starts_with('>') {
links.push((
super::name_to_id(word.trim_start_matches('>')).to_string(),
true,
));
continue;
}
if word.starts_with("!<") {
blinks.push((
super::name_to_id(word.trim_start_matches("!<")).to_string(),
false,
));
continue;
}
if word.starts_with('<') {
blinks.push((
super::name_to_id(word.trim_start_matches('<')).to_string(),
true,
));
continue;
}
title.push_str(word);
}
Self {
any,
tag_match,
tags,
links,
blinks,
title,
full_text,
}
}
pub fn apply(&self, note: &super::Note, index: &super::NoteIndex) -> Option<i64> {
let mut any = false;
let mut all = true;
for (tag, included) in self.tags.iter() {
if note
.tags
.iter()
.flat_map(|tag| {
tag.match_indices('/')
.map(|(index, _match)| &tag[0..index])
.chain(std::iter::once(tag.as_str()))
})
.any(|subtag|
match self.tag_match {
TagMatch::Exact => tag.replace('-', " ") == subtag.replace('-', " "),
TagMatch::Prefix => subtag.replace('-', " ").starts_with(&tag.replace('-', " ")),
}
)
== *included
{
any = true;
} else {
all = false;
}
}
for (link, included) in self.links.iter() {
if note.links.contains(link) == *included {
any = true;
} else {
all = false;
}
}
for (blink, included) in self.blinks.iter() {
let exists_and_contains = if let Some(other_note) = index.inner.get(blink) {
other_note.links.contains(&super::name_to_id(¬e.name))
} else {
false
};
if exists_and_contains == *included {
any = true;
} else {
all = false;
}
}
if let Some(text) = &self.full_text {
if std::fs::read_to_string(¬e.path)
.map(|content| content.to_lowercase().contains(text))
.unwrap_or(false)
{
any = true;
} else {
all = false;
}
}
let fuz_match = if self.title.is_empty() {
None
} else {
let matcher = fuzzy_matcher::skim::SkimMatcherV2::default();
let fuzzy_match = matcher.fuzzy_match(¬e.display_name, &self.title);
if fuzzy_match.is_some() {
any = true;
} else {
all = false;
}
fuzzy_match
};
if self.tags.is_empty() && self.links.is_empty() && self.blinks.is_empty() && self.full_text.is_none() && self.title.is_empty() ||
(!self.any && all || self.any && any)
{
fuz_match.or(Some(0))
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{data, io};
use pretty_assertions::assert_eq;
#[test]
fn test_filter() {
let config = crate::Config {
vault_path: Some(std::env::current_dir().unwrap().join("tests")),
..Default::default()
};
let tracker = io::FileTracker::new(&config).unwrap();
let builder = io::HtmlBuilder::new(&config);
let index = data::NoteIndex::new(tracker, builder, &config).0;
assert_eq!(index.inner.len(), 12);
let linux = index.inner.get("linux").unwrap();
let win = index.inner.get("windows").unwrap();
let osx = index.inner.get("osx").unwrap();
let filter1 = Filter {
any: false,
tag_match: data::TagMatch::Exact,
tags: vec![("#os".to_string(), true), ("#os/win".to_string(), false)],
links: vec![],
blinks: vec![],
title: String::new(),
full_text: None,
};
assert!(filter1.apply(linux, &index).is_some());
assert!(filter1.apply(osx, &index).is_some());
assert!(filter1.apply(win, &index).is_none());
}
#[test]
fn test_filter_prefix() {
let config = crate::Config {
vault_path: Some(std::env::current_dir().unwrap().join("tests")),
..Default::default()
};
let tracker = io::FileTracker::new(&config).unwrap();
let builder = io::HtmlBuilder::new(&config);
let index = data::NoteIndex::new(tracker, builder, &config).0;
assert_eq!(index.inner.len(), 12);
let linux = index.inner.get("linux").unwrap();
let win = index.inner.get("windows").unwrap();
let osx = index.inner.get("osx").unwrap();
let topology = index.inner.get("topology").unwrap();
let atlas = index.inner.get("atlas").unwrap();
let chart = index.inner.get("chart").unwrap();
let manifold = index.inner.get("manifold").unwrap();
let smooth_map = index.inner.get("smooth-map").unwrap();
let filter1 = Filter {
any: false,
tag_match: data::TagMatch::Prefix,
tags: vec![("#diff".to_string(), true)],
links: vec![],
blinks: vec![],
title: String::new(),
full_text: None,
};
assert!(filter1.apply(linux, &index).is_none());
assert!(filter1.apply(osx, &index).is_none());
assert!(filter1.apply(win, &index).is_none());
assert!(filter1.apply(topology, &index).is_none());
assert!(filter1.apply(atlas, &index).is_some());
assert!(filter1.apply(chart, &index).is_some());
assert!(filter1.apply(manifold, &index).is_some());
assert!(filter1.apply(smooth_map, &index).is_some());
let filter2 = Filter {
any: false,
tag_match: data::TagMatch::Exact,
tags: vec![("#diff".to_string(), true)],
links: vec![],
blinks: vec![],
title: String::new(),
full_text: None,
};
assert!(filter2.apply(linux, &index).is_none());
assert!(filter2.apply(osx, &index).is_none());
assert!(filter2.apply(win, &index).is_none());
assert!(filter2.apply(topology, &index).is_none());
assert!(filter2.apply(atlas, &index).is_none());
assert!(filter2.apply(chart, &index).is_none());
assert!(filter2.apply(manifold, &index).is_none());
assert!(filter2.apply(smooth_map, &index).is_none());
}
#[test]
fn test_filter_prefix_negate() {
let config = crate::Config {
vault_path: Some(std::env::current_dir().unwrap().join("tests")),
..Default::default()
};
let tracker = io::FileTracker::new(&config).unwrap();
let builder = io::HtmlBuilder::new(&config);
let index = data::NoteIndex::new(tracker, builder, &config).0;
assert_eq!(index.inner.len(), 12);
let linux = index.inner.get("linux").unwrap();
let win = index.inner.get("windows").unwrap();
let osx = index.inner.get("osx").unwrap();
let topology = index.inner.get("topology").unwrap();
let atlas = index.inner.get("atlas").unwrap();
let chart = index.inner.get("chart").unwrap();
let manifold = index.inner.get("manifold").unwrap();
let smooth_map = index.inner.get("smooth-map").unwrap();
let filter1 = Filter {
any: false,
tag_match: data::TagMatch::Prefix,
tags: vec![("#o".to_string(), true), ("#os/wi".to_string(), false)],
links: vec![],
blinks: vec![],
title: String::new(),
full_text: None,
};
assert!(filter1.apply(linux, &index).is_some());
assert!(filter1.apply(osx, &index).is_some());
assert!(filter1.apply(win, &index).is_none());
assert!(filter1.apply(topology, &index).is_none());
assert!(filter1.apply(atlas, &index).is_none());
assert!(filter1.apply(chart, &index).is_none());
assert!(filter1.apply(manifold, &index).is_none());
assert!(filter1.apply(smooth_map, &index).is_none());
let filter2 = Filter {
any: false,
tag_match: data::TagMatch::Exact,
tags: vec![("#o".to_string(), true), ("#os/wi".to_string(), false)],
links: vec![],
blinks: vec![],
title: String::new(),
full_text: None,
};
assert!(filter2.apply(linux, &index).is_none());
assert!(filter2.apply(osx, &index).is_none());
assert!(filter2.apply(win, &index).is_none());
assert!(filter2.apply(topology, &index).is_none());
assert!(filter2.apply(atlas, &index).is_none());
assert!(filter2.apply(chart, &index).is_none());
assert!(filter2.apply(manifold, &index).is_none());
assert!(filter2.apply(smooth_map, &index).is_none());
}
#[test]
fn test_filter_from_string() {
let config = crate::Config {
vault_path: Some(std::env::current_dir().unwrap().join("tests")),
..Default::default()
};
let tracker = io::FileTracker::new(&config).unwrap();
let builder = io::HtmlBuilder::new(&config);
let index = data::NoteIndex::new(tracker, builder, &config).0;
assert_eq!(index.inner.len(), 12);
let filter2 = Filter::new(
"!#lietheo #diffgeo >Manifold !>atlas",
false,
TagMatch::Exact,
);
assert_eq!(
filter2.tags,
vec![
("#lietheo".to_string(), false),
("#diffgeo".to_string(), true)
]
);
assert_eq!(
filter2.links,
vec![("manifold".to_string(), true), ("atlas".to_string(), false)]
);
assert_eq!(filter2.title, "");
let liegroup = index.inner.get("lie-group").unwrap();
let chart = index.inner.get("chart").unwrap();
let manifold = index.inner.get("manifold").unwrap();
let smoothmap = index.inner.get("smooth-map").unwrap();
let topology = index.inner.get("topology").unwrap();
assert!(filter2.apply(liegroup, &index).is_none());
assert!(filter2.apply(chart, &index).is_some());
assert!(filter2.apply(manifold, &index).is_none());
assert!(filter2.apply(smoothmap, &index).is_none());
assert!(filter2.apply(topology, &index).is_none());
}
#[test]
fn test_filter_from_string_all() {
let filter3 = Filter::new(
"!#topology #os >TopologY !>Smooth-mAp <atlas !<linux |equivalent",
false,
TagMatch::Exact,
);
assert_eq!(
filter3.tags,
vec![("#topology".to_string(), false), ("#os".to_string(), true)]
);
assert_eq!(
filter3.links,
vec![
("topology".to_string(), true),
("smooth-map".to_string(), false)
]
);
assert_eq!(
filter3.blinks,
vec![("atlas".to_string(), true), ("linux".to_string(), false)]
);
assert_eq!(filter3.title, "");
assert_eq!(filter3.full_text, Some(String::from("equivalent")));
}
#[test]
fn test_filter_from_string_case_insensitive() {
let filter4 = Filter::new("<aTlas >Smooth-mAp", true, TagMatch::Exact);
assert_eq!(filter4.tags, vec![]);
assert_eq!(filter4.links, vec![("smooth-map".to_string(), true),]);
assert_eq!(filter4.blinks, vec![("atlas".to_string(), true)]);
assert_eq!(filter4.title, "");
}
#[test]
fn test_filter_from_string_multi_word_tags() {
let config = crate::Config {
vault_path: Some(std::env::current_dir().unwrap().join("tests")),
..Default::default()
};
let tracker = io::FileTracker::new(&config).unwrap();
let builder = io::HtmlBuilder::new(&config);
let index = data::NoteIndex::new(tracker, builder, &config).0;
assert_eq!(index.inner.len(), 12);
let filter2 = Filter::new("#funny-abbreviations", false, TagMatch::Exact);
assert_eq!(
filter2.tags,
vec![("#funny-abbreviations".to_string(), true),]
);
assert_eq!(filter2.links, vec![]);
assert_eq!(filter2.title, "");
let yamlformat = index.inner.get("note25").unwrap();
let chart = index.inner.get("chart").unwrap();
assert!(filter2.apply(yamlformat, &index).is_some());
assert!(filter2.apply(chart, &index).is_none());
}
}