extern crate regex;
extern crate glob;
use std::error::Error;
use std::io::prelude::*;
use std::fs::File;
use glob::glob;
use std::path::PathBuf;
use regex::Regex;
use std::fmt;
#[allow(dead_code)]
pub struct Notes {
dir: String,
notes: Vec<Note>
}
impl Notes {
pub fn from_dir(dir: String) -> Notes {
let dir_buf = {
let buf = PathBuf::from(dir.clone());
buf.canonicalize().unwrap()
};
let files: Vec<PathBuf> = glob(format!("{}/**/*", dir_buf.to_str().unwrap()).as_str())
.expect("Failed to read glob pattern")
.filter_map(Result::ok)
.filter(|p| !p.symlink_metadata().unwrap()
.file_type().is_symlink())
.collect();
let symlinks: Vec<PathBuf> = glob(format!("{}/**/*", dir_buf.to_str().unwrap()).as_str())
.expect("Failed to read glob pattern")
.filter_map(Result::ok)
.filter(|p| p.symlink_metadata().unwrap()
.file_type().is_symlink())
.collect();
let mut notes: Vec<Note> = files.iter()
.map(|path: &PathBuf| Note::from_path(path, &dir_buf))
.filter_map(Result::ok)
.collect();
for link in symlinks.iter() {
for note in notes.iter_mut() {
let read_link = link.canonicalize().unwrap();
if note.path.to_str() == read_link.to_str() {
let bare_file = link.strip_prefix(dir_buf.to_str().unwrap()).unwrap();
let bare_directory = bare_file.parent().unwrap();
let tag = String::from(bare_directory.to_str().unwrap());
note.tags.push(tag);
}
}
}
Notes {
dir,
notes
}
}
}
#[allow(dead_code)]
#[derive(Debug)]
pub struct Note {
title: String,
path: PathBuf,
tags: Vec<String>
}
#[derive(PartialEq, Debug)]
pub enum NoteErrors {
NoteTitleMissingError,
}
impl fmt::Display for NoteErrors {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
NoteErrors::NoteTitleMissingError => write!(f, "The note title could not be parsed.")
}
}
}
impl Error for NoteErrors {
fn description(&self) -> &str {
match *self {
NoteErrors::NoteTitleMissingError => "The provided string did not contain something like a note title."
}
}
fn cause(&self) -> Option<&Error> {
None
}
}
impl Note {
pub fn title_from_string(text: String) -> Result<String, NoteErrors> {
let re = Regex::new("^([A-Za-z0-9 -_:]+)\n-+\n").unwrap();
let caps = match re.captures(text.as_str()) {
Some(x) => x,
None => return Err(NoteErrors::NoteTitleMissingError),
};
let title = caps.get(1).map_or("", |m| m.as_str());
return Ok(String::from(title));
}
pub fn from_path(path: &PathBuf, note_dir: &PathBuf) -> Result<Note, Box<Error>> {
let mut file = File::open(path.as_path())?; let mut s = String::new();
file.read_to_string(&mut s)?;
let mut end_length = 512;
if s.len() < end_length {
end_length = s.len();
}
s = String::from(&s[..end_length]);
let title = Self::title_from_string(s)?;
let dir_pathbuf = {
let mut new_path = path.clone();
new_path.pop();
new_path.canonicalize().unwrap()
};
let dir = note_dir.canonicalize().unwrap();
let tag = String::from(dir_pathbuf.strip_prefix(dir.to_str().unwrap()).unwrap()
.to_str().unwrap());
let tags: Vec<String> = vec![tag];
let mut path = path.clone();
path = path.canonicalize().unwrap();
Ok(
Note {
title,
path,
tags
}
)
}
}
#[cfg(test)]
mod tests {
use Note;
use Notes;
use std::error::Error;
use std::path::PathBuf;
fn parse_title_or_return_placeholder(text: &str, placeholder: &str) -> String {
let s = String::from(text);
match Note::title_from_string(s) {
Ok(string) => string,
Err(_) => String::from(placeholder),
}
}
#[test]
fn test_title_from_string() {
let n = parse_title_or_return_placeholder("Test\n----\n", "err");
assert_eq!(n, "Test");
}
#[test]
fn test_title_from_string_invalid() {
let n = parse_title_or_return_placeholder("\n", "err");
assert_eq!(n, "err");
}
fn note_from_path(path: &str, dir: &str) -> Result<Note, Box<Error>> {
let p = PathBuf::from(path);
let d = PathBuf::from(dir);
return Note::from_path(&p, &d);
}
#[test]
fn test_note_from_path() {
let path = "./tests/notes/test-note.md";
let n = note_from_path(path, "./").unwrap();
assert_eq!(n.title, "Test note");
assert_eq!(n.tags[0], "tests/notes");
assert_eq!(n.path, PathBuf::from(path).canonicalize().unwrap());
}
#[test]
fn test_very_long_note_from_path() {
let n = note_from_path("./tests/notes/test-note-very-long.md", "./");
assert_eq!(n.is_err(), true);
}
#[test]
fn test_note_from_path_pdf_file() {
let n = note_from_path("./tests/notes/otherfile.pdf", "./");
assert_eq!(n.is_err(), true);
}
#[test]
fn test_note_from_path_tex_file() {
let n = note_from_path("./tests/notes/otherfile.tex", "./");
assert_eq!(n.is_err(), true);
}
#[test]
fn test_note_from_nonexistent_path() {
let n = note_from_path("./tests/notes/test-note-nonexistent.md", "./");
assert_eq!(n.is_err(), true);
}
#[test]
fn test_notes_form_dir() {
let notes = Notes::from_dir(String::from("./tests/"));
assert_eq!(notes.notes.len(), 1);
assert_eq!(notes.notes[0].tags.len(), 2);
}
}