use gitignore;
use std::path::{Path, PathBuf};
use std::fs;
use std::io;
use backstage::error::Error;
use backstage::index::Index;
use backstage::zettel::Zettel;
mod grep;
mod ripgrep;
mod native;
const PATTERN: &str = r"[^!]\[.?*\]\(.*\)|^\[.*?\]\(.*\)";
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub enum IndexingMethod {
Grep,
RipGrep,
Native,
}
pub fn create_index<P: AsRef<Path>>(indexingmethod: &IndexingMethod,
rootdir: P,
ignorefile: P)
-> Result<Index, Error> {
debug!("Creating new index.");
let rootdir = rootdir.as_ref();
let ignorefile = ignorefile.as_ref();
let mut index = Index::empty();
let mut files = get_list_of_files(rootdir, ignorefile)?; trace!("Files: {:#?}", files);
let mut zettels_to_parse = vec![];
debug!("Start with the yaml-metadata of each file.");
for file in files.drain(..) {
let added_key = add_textfiles_only(&mut index, rootdir, &file)?;
if let Some(key) = added_key {
zettels_to_parse.push(key);
}
}
let _ = parse_and_apply_markdown_links(indexingmethod,
&mut index,
rootdir,
zettels_to_parse,
)?;
index.update_timestamp();
Ok(index)
}
pub fn update_index<P: AsRef<Path>>(index: &mut Index,
indexingmethod: &IndexingMethod,
rootdir: P,
ignorefile: P) -> Result<(), Error> {
debug!("Updating index.");
let rootdir = rootdir.as_ref();
let ignorefile = ignorefile.as_ref();
let (mut zettels_to_update, mut zettels_to_remove)
= get_changed_zettels(index, rootdir, ignorefile)?;
trace!("Update: {:?}", zettels_to_update);
trace!("Remove: {:?}", zettels_to_remove);
debug!("Removing files that no longer exist.");
for k in zettels_to_remove.drain(..) {
index.files.remove(&k);
}
let mut zettels_to_parse = vec![];
debug!("Start with the yaml-metadata of each updated file.");
for k in zettels_to_update.drain(..) {
if index.files.contains_key(&k) {
trace!("Removing old entry for {:?}.", &k);
index.files.remove(&k);
}
let added_key = add_textfiles_only(index, rootdir, &k)?;
if let Some(key) = added_key {
zettels_to_parse.push(key);
}
}
let _ = parse_and_apply_markdown_links(indexingmethod,
index,
rootdir,
zettels_to_parse,
)?;
index.update_timestamp();
Ok(())
}
fn add_textfiles_only<P: AsRef<Path>>(index: &mut Index,
rootdir: P,
zettelfile: P) -> Result<Option<PathBuf>, Error> {
let zettelfile = zettelfile.as_ref();
let rootdir = rootdir.as_ref();
trace!("Adding new or updated entry for {:?}.", &zettelfile);
let z = Zettel::from_file(rootdir.join(&zettelfile), rootdir.to_path_buf());
let k = zettelfile; match z {
Ok(zettel) => { trace!("Key: {:?}", k);
trace!("{:?}", zettel);
index.add_zettel(&k, zettel);
Ok(Some(k.to_path_buf()))
},
Err(e) => match e { Error::Io(io_e) => match io_e.kind() {
io::ErrorKind::InvalidData => {
info!("File {:?} seems to be non-text. Skipping.", k);
Ok(None)
},
_ => return Err(Error::from(io_e)), },
_ => return Err(e), },
}
}
fn parse_and_apply_markdown_links<P: AsRef<Path>>(
indexingmethod: &IndexingMethod,
index: &mut Index,
rootdir: P,
files: Vec<PathBuf>,
) -> Result<(), Error> {
let rootdir = rootdir.as_ref();
debug!("Begin to parse markdown links.");
let links_tuples = match indexingmethod {
IndexingMethod::Grep => grep::parse_files(&rootdir.to_path_buf(),
PATTERN,
files),
IndexingMethod::RipGrep => ripgrep::parse_files(&rootdir.to_path_buf(),
PATTERN,
files),
IndexingMethod::Native => native::parse_files(&rootdir.to_path_buf(),
PATTERN,
files),
}?; debug!("Got {} links tuples.", links_tuples.len());
for (k, link) in links_tuples {
trace!("Working on link from {:?} to {:?}", k, link);
let z = index.get_mut_zettel(k);
match z {
Some(z) => z.add_link(link),
None => return Err(
Error::from(io::Error::new(io::ErrorKind::Other,
"A zettel should be there, but it isn't."))
),
}
}
Ok(())
}
fn get_list_of_files_no_ignore<P: AsRef<Path>>(dir: P) -> Result<Vec<PathBuf>, Error> {
let dir = dir.as_ref();
let mut files = vec![]; for entry in fs::read_dir(dir)? { let entry = entry?; let path = entry.path();
if path.is_dir() { let subdir_files = get_list_of_files_no_ignore(&path)?;
let norm_path = normalize_path(dir.to_path_buf(), path)?;
for file in subdir_files {
files.push(norm_path.join(file));
}
} else {
let path = normalize_path(dir.to_path_buf(), path)?; files.push(path);
}
}
Ok(files)
}
fn get_list_of_files<P: AsRef<Path>>(dir: P, ignorefile: P)
-> Result<Vec<PathBuf>, Error> {
let dir = dir.as_ref();
let ignorefile = ignorefile.as_ref();
let gitignore_path = dir.join(ignorefile);
if !gitignore_path.exists() {
info!("The ignore file {:?} doesn't exist. Proceeding without it.",
&gitignore_path);
return get_list_of_files_no_ignore(&dir.to_path_buf()); }
let gitignore_file = gitignore::File::new(&gitignore_path)?; let mut entries = gitignore_file.included_files()?; let mut files = vec![];
for entry in entries.drain(..) {
if entry.is_file() {
let entry = normalize_path(&dir.to_path_buf(), &entry)?;
files.push(entry);
}
}
Ok(files)
}
fn get_changed_zettels<P: AsRef<Path>>(index: &mut Index, rootdir: P, ignorefile: P)
-> Result<(Vec<PathBuf>, Vec<PathBuf>), Error> {
let rootdir = rootdir.as_ref();
let ignorefile = ignorefile.as_ref();
let mut zettels_to_update = vec![];
let mut zettels_to_remove = vec![];
let mut files = get_list_of_files(&rootdir, &ignorefile)?;
let mut rest = vec![];
for file in files.drain(..) {
if !&index.files.contains_key(&file) {
zettels_to_update.push(file);
} else {
rest.push(file);
}
}
let files = rest;
for (k, _) in &index.files {
if !files.contains(&k) { zettels_to_remove.push(k.clone()); } else {
let modified = fs::metadata(&rootdir.join(&k))?.modified()?;
if modified > index.timestamp {
zettels_to_update.push(k.clone()); }
}
}
Ok((zettels_to_update, zettels_to_remove))
}
pub fn normalize_link<P: AsRef<Path>>(rootdir: P, zettelfile: P, link: P)
-> Result<PathBuf, Error> {
let rootdir = rootdir.as_ref();
let zettelfile = zettelfile.as_ref();
let link = link.as_ref();
trace!("Normalizing link {:?} from {:?}.", link, zettelfile);
trace!("Rootdir: {:?}", rootdir);
let file_dir = match zettelfile.parent() { None => return Err(
Error::Io(
io::Error::new(
io::ErrorKind::InvalidInput,
format!("{:#?} is not a valid path to a Zettel.", zettelfile)
))),
Some(parent) => parent,
};
trace!("File dir: {:?}", file_dir);
let link = file_dir.join(link);
let link = rootdir.join(link);
trace!("Joined link: {:?}", link);
let canon_link = link.canonicalize();
let link = match canon_link {
Ok(canon_link) => {
trace!("Absolute link: {:?}", canon_link);
Ok(canon_link)
},
Err(e) => {
match e.kind() {
io::ErrorKind::NotFound => {
trace!("{:?} doesn't exist. Emmiting Error::BadLink.", link);
Err(Error::BadLink(zettelfile.clone().to_path_buf(), link, e))
},
_ => Err(Error::Io(e)), }
}
}?;
let rootdir = rootdir.canonicalize()?;
trace!("Absolute rootdir: {:?}", rootdir);
let rellink = link.strip_prefix(rootdir)?; trace!("Normalized link: {:?}", rellink);
Ok(rellink.to_path_buf())
}
pub fn normalize_path<P: AsRef<Path>>(rootdir: P, zettelfile: P)
-> Result<PathBuf, Error> {
let zettelfile = zettelfile.as_ref().canonicalize()?; let rootdir = rootdir.as_ref().canonicalize()?; let relfile = zettelfile.strip_prefix(rootdir)?; Ok(relfile.to_path_buf())
}
#[cfg(test)]
mod tests {
use super::PATTERN;
use regex::Regex;
mod valid;
mod invalid;
#[test]
fn test_regex() {
let r = Regex::new(PATTERN)
.expect("Failed to build regex.");
let text = "Duis [ornare](enim) magna";
assert!(r.is_match(text));
let text = "Duis [ornare](enim) [magna](foo)";
assert!(r.is_match(text));
let text = "[ornare](enim) magna";
assert!(r.is_match(text));
let text = "Integer consectetur neque velit, at.";
assert!(!r.is_match(text));
let text = "Integer [consectetur (neque)][velit], at.";
assert!(!r.is_match(text));
let text = "[consectetur (neque)][velit], at.";
assert!(!r.is_match(text));
let text = "Maecenas [rutrum][pretium] velit vitae.";
assert!(!r.is_match(text));
let text = "[rutrum][pretium] velit vitae.";
assert!(!r.is_match(text));
}
}