use std::io::{BufRead, BufReader};
use std::fmt::Write as FmtWrite;
use std::fs::File;
use std::path::{Path, PathBuf};
use std::collections::HashSet;
use backstage::error::Error;
use backstage::indexing::normalize_path;
use backstage::indexing::normalize_link;
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
pub struct Zettel {
#[serde(default = "default_title")]
pub title: String,
#[serde(default = "default_links")]
pub links: Vec<PathBuf>,
#[serde(default = "default_links")]
pub followups: Vec<PathBuf>,
#[serde(default = "default_keywords")]
pub keywords: Vec<String>,
}
fn default_title() -> String { String::from("untitled") }
fn default_links() -> Vec<PathBuf> { vec![] } fn default_keywords() -> Vec<String> { vec![] }
impl Zettel {
pub fn new<T: AsRef<str>>(title: T) -> Zettel {
let title = title.as_ref();
Zettel {
title: title.to_string(),
links: vec![],
followups: vec![],
keywords: vec![],
}
}
pub fn from_yaml<P: AsRef<Path>>(yaml: &str, rootdir: P, zettelfile: P)
-> Result<Zettel, Error> {
let rootdir = rootdir.as_ref();
let zettelfile = zettelfile.as_ref();
let zettelfile = normalize_path(rootdir, zettelfile)?;
let s: Vec<&str> = yaml.split("...").collect();
let s = s[0];
let mut z: Zettel = match serde_yaml::from_str(s) {
Ok(zettel) => Ok(zettel),
Err(yaml_e) => Err(
Error::BadHeader(
zettelfile.clone().to_path_buf(),
yaml_e
)
),
}?;
let mut normalized_followups = vec![];
trace!("Normalizing followups of Zettel {:?}", zettelfile);
for followup in z.followups.drain(..) {
let nf = normalize_link(rootdir,
&zettelfile,
&followup)?; normalized_followups.push(nf);
}
z.followups = normalized_followups;
Ok(z)
}
pub fn from_file<P: AsRef<Path>>(zettelfile: P, rootdir: P)
-> Result<Zettel, Error> {
let zettelfile = zettelfile.as_ref();
let rootdir = rootdir.as_ref();
let file = File::open(zettelfile)?;
let contents = get_first_yaml_document(file)?; match contents {
None => Ok(Zettel::new("untitled")),
Some(s) => Zettel::from_yaml(&s, rootdir, zettelfile), }
}
pub fn add_link<P: AsRef<Path>>(&mut self, link: P) {
let link = link.as_ref().to_path_buf();
if !self.links.contains(&link) {
self.links.push(link);
}
}
pub fn add_followup<P: AsRef<Path>>(&mut self, followup: P) {
let followup = followup.as_ref().to_path_buf();
if !self.followups.contains(&followup) {
self.followups.push(followup);
}
}
pub fn add_keyword<T: AsRef<str>>(&mut self, keyword: T) {
let keyword = keyword.as_ref().to_string();
if !self.keywords.contains(&keyword) {
self.keywords.push(keyword);
}
}
pub fn links_to(&self, searched_links: &HashSet<PathBuf>, all: bool)
-> bool {
if all {
for link in searched_links {
if !self.links.contains(&link) {
return false;
}
}
return true;
} else {
for link in searched_links {
if self.links.contains(&link) {
return true;
}
}
return false;
}
}
}
fn get_first_yaml_document(opened_zettelfile: File)
-> Result<Option<String>, std::io::Error> {
let buf_reader = BufReader::new(opened_zettelfile);
let mut within_yaml = false; let mut s = String::new();
let mut xml = false;
let mut linecount = 0;
for line in buf_reader.lines() {
let line = match line {
Ok(l) => l,
Err(e) => return Err(e), };
linecount += 1;
if linecount == 1 && line.trim().starts_with("<?xml") {
xml = true;
}
if linecount > 1 && xml && line.trim().starts_with("<svg") {
return Err(
std::io::Error::new(std::io::ErrorKind::InvalidData,
"File is SVG"));
}
if line == "---" && !within_yaml { writeln!(&mut s, "{}", line)
.expect("Failed to write the line we just read from a text\
file to a string.");
within_yaml = true; } else if within_yaml && (line == "..." || line == "---") {
break;
} else if within_yaml {
writeln!(&mut s, "{}", line)
.expect("Failed to write the line we just read from a text\
file to a string.");
}
}
match s.len() {
0 => Ok(None),
_ => Ok(Some(s)),
}
}
#[cfg(test)]
mod tests {
extern crate tempfile;
use self::tempfile::tempdir;
use super::*;
use examples::*;
#[test]
fn test_default_title() {
assert_eq!(default_title(), "untitled");
}
#[test]
fn test_default_links() {
assert!(default_links().is_empty());
let mut d = default_links();
d.push(PathBuf::from("dummy.txt"));
}
#[test]
fn test_default_keywords() {
assert!(default_keywords().is_empty());
let mut d = default_keywords();
d.push("dummy.txt".to_string());
}
#[test]
fn test_zettel_new() {
let z = Zettel::new("Example Zettel");
assert_eq!(&z.title, &String::from("Example Zettel"));
assert!(&z.links.is_empty());
assert!(&z.followups.is_empty());
assert!(&z.keywords.is_empty());
}
#[test]
fn test_zettel_from_yaml() {
let tmp_dir = tempdir().expect("Failed to setup temp dir");
let dir = tmp_dir.path();
generate_bare_examples(dir).expect("Failed to generate examples");
let rootdir = dir.join("examples/Zettelkasten");
let zettelfile = rootdir.join("file1.md");
let yaml = "---
title: 'A Zettel'
keywords: [example, yaml]
followups: [file2.md]
...
Here be contents.";
let z = Zettel::from_yaml(yaml, rootdir, zettelfile);
assert!(z.is_ok());
}
#[test]
fn test_zettel_from_file() {
let tmp_dir = tempdir().expect("Failed to setup temp dir");
let dir = tmp_dir.path();
generate_bare_examples(dir).expect("Failed to generate examples");
let rootdir = dir.join("examples/Zettelkasten");
let zettelfile = rootdir.join("file1.md");
let z = Zettel::from_file(zettelfile, rootdir);
assert!(z.is_ok());
}
#[test]
fn test_zettel_add_link() {
let mut z = Zettel::new("Example Zettel");
z.add_link(PathBuf::from("anotherfile.md"));
assert_eq!(&z.links, &vec![PathBuf::from("anotherfile.md")]);
}
#[test]
fn test_test_add_double_link() {
let mut z = Zettel::new("Example Zettel");
z.add_link(PathBuf::from("anotherfile.md"));
z.add_link(PathBuf::from("anotherfile.md"));
assert_eq!(z.links.len(), 1);
}
#[test]
fn test_zettel_add_followup() {
let mut z = Zettel::new("Example Zettel");
z.add_followup(PathBuf::from("anotherfile.md"));
assert_eq!(&z.followups, &vec![PathBuf::from("anotherfile.md")]);
}
#[test]
fn test_zettel_add_double_followup() {
let mut z = Zettel::new("Example Zettel");
z.add_followup(PathBuf::from("anotherfile.md"));
z.add_followup(PathBuf::from("anotherfile.md"));
assert_eq!(z.followups.len(), 1);
}
#[test]
fn test_zettel_add_keyword() {
let mut z = Zettel::new("Example Zettel");
z.add_keyword("foo");
assert_eq!(&z.keywords, &vec![String::from("foo")]);
}
#[test]
fn test_zettel_add_double_keyword() {
let mut z = Zettel::new("Example Zettel");
z.add_keyword("foo");
z.add_keyword("foo");
assert_eq!(z.keywords.len(), 1);
}
#[test]
fn test_get_first_yaml_document() {
use std::io::Write;
let tmp_dir = tempdir().expect("Failed to create tempdir.");
let mut f1 = File::create(tmp_dir.path().join("file1.md"))
.expect("Something went wrong with creating temporary file1 for
testing.");
let mut f2 = File::create(tmp_dir.path().join("file2.md"))
.expect("Something went wrong with creating temporary file2 for
testing.");
let mut f3 = File::create(tmp_dir.path().join("file3.md"))
.expect("Something went wrong with creating temporary file3 for
testing.");
let mut f4 = File::create(tmp_dir.path().join("file4.md"))
.expect("Something went wrong with creating temporary file4 for
testing.");
writeln!(f1, "{}", "---
foo: bar
...
Lorem ipsum --- dolor
--- sit amet,
consectetur ... adipiscing
... elit, sed
---
...
").expect("Failed to write to file");
writeln!(f2, "{}", "---
foo: bar
---
Lorem ipsum --- dolor
--- sit amet,
consectetur ... adipiscing
... elit, sed
---
...
").expect("Failed to write to file");
writeln!(f3, "{}", "Lorem ipsum
---
foo: bar
---
Lorem ipsum --- dolor
--- sit amet,
consectetur ... adipiscing
... elit, sed
---
...
").expect("Failed to write to file");
writeln!(f4, "{}", "Lorem ipsum dolor sit --- amet, consectetur adipiscing
elit, sed do eiusmod tempor ... incididunt ut labore et dolore magna aliqua. Ut
enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut .")
.expect("Failed to write to file");
let f1 = File::open(tmp_dir.path().join("file1.md"))
.expect("Failed to open file.");
let f2 = File::open(tmp_dir.path().join("file2.md"))
.expect("Failed to open file.");
let f3 = File::open(tmp_dir.path().join("file3.md"))
.expect("Failed to open file.");
let f4 = File::open(tmp_dir.path().join("file4.md"))
.expect("Failed to open file.");
let s1 = get_first_yaml_document(f1)
.expect("Failed to read from file f1");
let s2 = get_first_yaml_document(f2)
.expect("Failed to read from file f2");
let s3 = get_first_yaml_document(f3)
.expect("Failed to read from file f3");
let s4 = get_first_yaml_document(f4)
.expect("Failed to read from file f4");
let s1 = s1.unwrap();
let s2 = s2.unwrap();
let s3 = s3.unwrap();
assert_eq!(s1, "---\nfoo: bar\n");
assert_eq!(s1, s2);
assert_eq!(s1, s3);
assert!(s4.is_none());
}
#[test]
fn test_get_first_yaml_document_empty_file() {
let tmp_dir = tempfile::tempdir()
.expect("Failed to create tempdir");
let path = tmp_dir.path().join("dummy.txt");
std::fs::File::create(&path)
.expect("Failed to create file.");
let file = std::fs::File::open(&path)
.expect("Failed to open file.");
let outcome = get_first_yaml_document(file);
assert!(outcome.is_ok());
let outcome = outcome.unwrap();
assert!(outcome.is_none());
}
#[test]
fn test_get_first_yaml_document_image_file() {
let tmp_dir = tempdir().expect("Failed to create tempdir");
let image_file = tmp_dir.path().join("foo.png");
let width: u32 = 10;
let height: u32 = 10;
let mut non_text = image::ImageBuffer::new(width, height);
for (_, _, pixel) in non_text.enumerate_pixels_mut() {
*pixel = image::Rgb([255, 255, 255]);
}
non_text.save(&image_file).unwrap();
let file = std::fs::File::open(&image_file)
.expect("Failed to open file.");
let outcome = get_first_yaml_document(file);
assert!(outcome.is_err());
}
#[test]
fn test_zettel_from_yaml_invalid_type() {
let tmp_dir = tempdir().expect("Failed to setup temp dir");
let dir = tmp_dir.path();
generate_bare_examples(dir).expect("Failed to generate examples");
let rootdir = dir.join("examples/Zettelkasten");
let zettelfile = rootdir.join("file1.md");
let yaml = "---
title: Test
keywords: 1
followups: [file2.md]
...
Here be contents.";
let z = Zettel::from_yaml(yaml, rootdir, zettelfile);
assert!(z.is_err());
let e = z.unwrap_err();
match e {
Error::BadHeader(_, inner) => {
let message = inner.to_string();
assert!(message.contains("invalid type"));
},
_ => panic!("Expected a BadHeader error, got: {:#?}", e),
}
}
#[test]
fn test_zettel_from_yaml_invalid_followups() {
let tmp_dir = tempdir().expect("Failed to setup temp dir");
let dir = tmp_dir.path();
generate_bare_examples(dir).expect("Failed to generate examples");
let rootdir = dir.join("examples/Zettelkasten");
let zettelfile = rootdir.join("file1.md");
let yaml = "---
title: Test
keywords: []
followups: [file2.md,
file3.md]
...
Here be contents.";
let z = Zettel::from_yaml(yaml, rootdir, zettelfile);
assert!(z.is_err());
let e = z.unwrap_err();
match e {
Error::BadHeader(_, inner) => {
let message = inner.to_string();
println!("{:?}", message);
assert!(message.contains("simple key expected"));
},
_ => panic!("Expected a BadHeader error, got: {:#?}", e),
}
}
#[test]
fn test_zettel_from_yaml_duplicate_field() {
let tmp_dir = tempdir().expect("Failed to setup temp dir");
let dir = tmp_dir.path();
generate_bare_examples(dir).expect("Failed to generate examples");
let rootdir = dir.join("examples/Zettelkasten");
let zettelfile = rootdir.join("file1.md");
let yaml = "---
title: 'A Zettel'
title: 'A Zettel'
keywords: [example, yaml]
followups: [file2.md]
...
Here be contents.";
let z = Zettel::from_yaml(yaml, rootdir, zettelfile);
let e = z.unwrap_err();
match e {
Error::BadHeader(_, inner) => {
let message = inner.to_string();
assert!(message.contains("duplicate field `title`"));
},
_ => panic!("Expected a BadHeader error, got: {:#?}", e),
}
}
#[test]
fn test_zettel_from_file_non_existing_file() {
let tmp_dir = tempdir().expect("Failed to setup temp dir");
let dir = tmp_dir.path();
generate_bare_examples(dir).expect("Failed to generate examples");
let rootdir = dir.join("examples/Zettelkasten");
let zettelfile = rootdir.join("foo.md"); let z = Zettel::from_file(zettelfile, rootdir);
assert!(z.is_err());
let e = z.unwrap_err();
match e {
Error::Io(inner) => {
match inner.kind() {
std::io::ErrorKind::NotFound => {
assert!(inner.to_string().contains("No such file or \
directory"));
},
_ => panic!("Expected a NotFound error, got: {:#?}", inner)
}
},
_ => panic!("Expected a Io error, got: {:#?}", e),
}
}
#[test]
fn test_links_to() {
let mut z = Zettel::new("Example Zettel");
z.add_link(PathBuf::from("file1.md"));
let mut searched_links = HashSet::new();
searched_links.insert(PathBuf::from("file1.md"));
searched_links.insert(PathBuf::from("file2.md"));
assert!(z.links_to(&searched_links, false));
assert!(!z.links_to(&searched_links, true));
}
}