use crate::config::TMPL_VAR_DOC;
use crate::content::Content;
use crate::context::Context;
use crate::context::HasSettings;
use crate::context::ReadyForContentTemplate;
use crate::context::ReadyForFilenameTemplate;
use crate::error::NoteError;
use crate::filename::NotePath;
use crate::filename::NotePathBuf;
use crate::filter::TERA;
use crate::front_matter::FrontMatter;
use crate::note_error_tera_template;
use crate::template::TemplateKind;
use std::default::Default;
use std::fs;
use std::path::{Path, PathBuf};
use std::str;
use tera::Tera;
pub(crate) const ONE_OFF_TEMPLATE_NAME: &str = "__tera_one_off";
pub struct Note<T: Content> {
pub context: Context<ReadyForFilenameTemplate>,
pub content: T,
pub rendered_filename: PathBuf,
}
impl<T: Content> Note<T> {
pub fn from_existing_content(
context: Context<HasSettings>,
content: T,
template_kind: TemplateKind,
) -> Result<Note<T>, NoteError> {
debug_assert!(match template_kind {
TemplateKind::SyncFilename => true,
TemplateKind::None => true,
_ => panic!(
"Contract violation: `template_kind=={:?}` is not acceptable here.",
template_kind
),
});
let fm = FrontMatter::try_from(content.header())?;
let context = context.insert_front_matter(&fm);
context.assert_precoditions()?;
Ok(Note {
context,
content,
rendered_filename: PathBuf::new(),
})
}
pub fn from_content_template(
context: Context<ReadyForContentTemplate>,
template_kind: TemplateKind,
) -> Result<Note<T>, NoteError> {
log::trace!(
"Available substitution variables for the content template:\n{:#?}",
*context
);
debug_assert!(match template_kind {
TemplateKind::SyncFilename => panic!("`TemplateKind::SyncFilename` not allowed here"),
TemplateKind::None => panic!("`TemplateKind::None` not allowed here"),
_ => true,
});
let new_content: T = T::from_string(
{
let mut tera = Tera::default();
tera.extend(&TERA)?;
tera.render_str(&template_kind.get_content_template(), &context)
.map_err(|e| {
note_error_tera_template!(
e,
template_kind.get_content_template_name().to_string()
)
})?
},
TMPL_VAR_DOC.to_string(),
);
log::debug!(
"Rendered content template:\n---\n{}\n---\n\n{}",
new_content.header(),
new_content.body()
);
let fm = FrontMatter::try_from(new_content.header())?;
let new_context = Context::from_context_path(&context).insert_front_matter(&fm);
Ok(Note {
context: new_context,
content: new_content,
rendered_filename: PathBuf::new(),
})
}
pub fn render_filename(&mut self, template_kind: TemplateKind) -> Result<(), NoteError> {
log::trace!(
"Available substitution variables for the filename template:\n{:#?}",
*self.context
);
let mut file_path = self.context.get_dir_path().to_owned();
let mut tera = Tera::default();
tera.extend(&TERA)?;
match tera.render_str(&template_kind.get_filename_template(), &self.context) {
Ok(filename) => {
file_path.push(filename.trim());
}
Err(e) => {
return Err(note_error_tera_template!(
e,
template_kind.get_filename_template_name().to_string()
));
}
}
file_path.shorten_filename();
self.rendered_filename = file_path;
Ok(())
}
pub fn set_next_unused_rendered_filename(&mut self) -> Result<(), NoteError> {
debug_assert_ne!(self.rendered_filename, PathBuf::new());
self.rendered_filename.set_next_unused()?;
Ok(())
}
pub fn set_next_unused_rendered_filename_or(
&mut self,
alt_path: &Path,
) -> Result<(), NoteError> {
debug_assert_ne!(self.rendered_filename, PathBuf::new());
if self.rendered_filename.exclude_copy_counter_eq(alt_path) {
self.rendered_filename = alt_path.to_path_buf();
} else {
self.rendered_filename.set_next_unused()?;
}
Ok(())
}
pub fn save(&self) -> Result<(), NoteError> {
debug_assert_ne!(self.rendered_filename, PathBuf::new());
log::trace!(
"Writing the note's content to file: {:?}",
self.rendered_filename
);
self.content.save_as(&self.rendered_filename)?;
Ok(())
}
pub fn rename_file_from(&self, from_path: &Path) -> Result<(), NoteError> {
debug_assert_ne!(self.rendered_filename, PathBuf::new());
if !from_path.exclude_copy_counter_eq(&self.rendered_filename) {
fs::rename(from_path, &self.rendered_filename)?;
log::trace!(
"File renamed to {}",
self.rendered_filename.to_str().unwrap_or_default()
);
}
Ok(())
}
pub fn save_and_delete_from(&mut self, from_path: &Path) -> Result<(), NoteError> {
debug_assert_ne!(self.rendered_filename, PathBuf::new());
self.save()?;
if from_path != self.rendered_filename {
log::trace!("Deleting file: {:?}", from_path);
fs::remove_file(from_path)?;
}
Ok(())
}
#[inline]
pub fn render_content_to_html(
&self,
tmpl: &str,
viewer_doc_js: &str,
) -> Result<String, NoteError> {
let html_context = self.context.clone();
let html_context = html_context.insert_raw_content_and_css(&self.content, viewer_doc_js);
log::trace!(
"Available substitution variables for the HTML template:\
\n{:#?}",
html_context
);
let mut tera = Tera::default();
tera.extend(&TERA)?;
tera.autoescape_on(vec![ONE_OFF_TEMPLATE_NAME]);
let html = tera.render_str(tmpl, &html_context).map_err(|e| {
note_error_tera_template!(e, "[html_tmpl] viewer/exporter_tmpl ".to_string())
})?;
Ok(html)
}
}
#[cfg(test)]
mod tests {
use super::Context;
use super::FrontMatter;
use crate::config::TMPL_VAR_FM_ALL;
use serde_json::json;
use std::path::Path;
use tera::Value;
#[test]
fn test_deserialize() {
let input = "# document start
title: The book
subtitle: you always wanted
author: It's me
date: 2020-04-21
lang: en
revision: '1.0'
sort_tag: 20200420-21_22
file_ext: md
height: 1.23
count: 2
neg: -1
flag: true
numbers:
- 1
- 3
- 5
";
let mut expected = tera::Map::new();
expected.insert("title".to_string(), Value::String("The book".to_string()));
expected.insert(
"subtitle".to_string(),
Value::String("you always wanted".to_string()),
);
expected.insert("author".to_string(), Value::String("It\'s me".to_string()));
expected.insert("date".to_string(), Value::String("2020-04-21".to_string()));
expected.insert("lang".to_string(), Value::String("en".to_string()));
expected.insert("revision".to_string(), Value::String("1.0".to_string()));
expected.insert(
"sort_tag".to_string(),
Value::String("20200420-21_22".to_string()),
);
expected.insert("file_ext".to_string(), Value::String("md".to_string()));
expected.insert("height".to_string(), json!(1.23)); expected.insert("count".to_string(), json!(2)); expected.insert("neg".to_string(), json!(-1)); expected.insert("flag".to_string(), json!(true)); expected.insert("numbers".to_string(), json!([1, 3, 5]));
let expected_front_matter = FrontMatter(expected);
assert_eq!(expected_front_matter, FrontMatter::try_from(input).unwrap());
}
#[test]
fn test_register_front_matter() {
let mut tmp = tera::Map::new();
tmp.insert("file_ext".to_string(), Value::String("md".to_string())); tmp.insert("height".to_string(), json!(1.23)); tmp.insert("count".to_string(), json!(2)); tmp.insert("neg".to_string(), json!(-1)); tmp.insert("flag".to_string(), json!(true)); tmp.insert("numbers".to_string(), json!([1, 3, 5])); let mut tmp2 = tera::Map::new();
tmp2.insert("fm_file_ext".to_string(), Value::String("md".to_string())); tmp2.insert("fm_height".to_string(), json!(1.23)); tmp2.insert("fm_count".to_string(), json!(2)); tmp2.insert("fm_neg".to_string(), json!(-1)); tmp2.insert("fm_flag".to_string(), json!(true)); tmp2.insert("fm_numbers".to_string(), json!([1, 3, 5]));
let input1 = Context::from(Path::new("a/b/test.md")).unwrap();
let input2 = FrontMatter(tmp);
let mut expected = Context::from(Path::new("a/b/test.md")).unwrap();
tmp2.remove("fm_numbers");
tmp2.insert("fm_numbers".to_string(), json!([1, 3, 5])); let tmp2 = tera::Value::from(tmp2);
expected.insert(TMPL_VAR_FM_ALL, &tmp2); let expected = expected.insert_front_matter(&FrontMatter::try_from("").unwrap());
let result = input1.insert_front_matter(&input2);
assert_eq!(result, expected);
}
#[test]
fn test_from_existing_content1() {
use crate::content::Content;
use crate::content::ContentString;
use crate::context::Context;
use crate::note::Note;
use crate::template::TemplateKind;
use std::env::temp_dir;
use std::fs;
let raw = r#"---
title: "My day"
subtitle: "Note"
---
Body text
"#;
let notefile = temp_dir().join("20221031-hello.md");
fs::write(¬efile, raw.as_bytes()).unwrap();
let expected = temp_dir().join("20221031-My day--Note.md");
let _ = fs::remove_file(&expected);
let context = Context::from(¬efile).unwrap();
let content = <ContentString as Content>::open(¬efile).unwrap();
let mut n = Note::<ContentString>::from_existing_content(
context,
content,
TemplateKind::SyncFilename,
)
.unwrap();
let path = n.context.get_path().to_owned();
n.render_filename(TemplateKind::SyncFilename).unwrap();
n.set_next_unused_rendered_filename_or(&path).unwrap();
assert_eq!(n.rendered_filename, expected);
n.rename_file_from(&path).unwrap();
assert!(n.rendered_filename.is_file());
}
#[test]
fn test_from_existing_content2() {
use crate::config::LIB_CFG;
use crate::config::TMPL_HTML_VAR_VIEWER_DOC_JS;
use crate::content::Content;
use crate::content::ContentString;
use crate::context::Context;
use crate::note::Note;
use crate::template::TemplateKind;
use std::env::temp_dir;
use std::fs;
let raw = r#"---
title: "My day"
subtitle: "Note"
---
Body text
"#;
let notefile = temp_dir().join("20221030-My day--Note.md");
fs::write(¬efile, raw.as_bytes()).unwrap();
let mut context = Context::from(¬efile).unwrap();
context.insert(TMPL_HTML_VAR_VIEWER_DOC_JS, &"".into());
let content = <ContentString as Content>::open(¬efile).unwrap();
let n: Note<ContentString> =
Note::<ContentString>::from_existing_content(context, content, TemplateKind::None)
.unwrap();
let html = n
.render_content_to_html(&LIB_CFG.read_recursive().tmpl_html.viewer, "")
.unwrap();
assert!(html.starts_with("<!DOCTYPE html>\n<html"))
}
#[test]
fn test_from_content_template1() {
use crate::content::Content;
use crate::content::ContentString;
use crate::context::Context;
use crate::note::Note;
use crate::settings::Settings;
use crate::settings::SETTINGS;
use crate::template::TemplateKind;
use parking_lot::RwLockWriteGuard;
use std::env::temp_dir;
use std::fs;
let mut settings = SETTINGS.write();
*settings = Settings::default();
let _settings = RwLockWriteGuard::<'_, _>::downgrade(settings);
let notedir = temp_dir().join("123-my dir/");
fs::create_dir_all(¬edir).unwrap();
let context = Context::from(¬edir).unwrap();
let html_clipboard =
ContentString::from_string("".to_string(), "html_clipboard".to_string());
let txt_clipboard = ContentString::from_string("".to_string(), "txt_clipboard".to_string());
let stdin = ContentString::from_string("".to_string(), "stdin".to_string());
let v = vec![&html_clipboard, &txt_clipboard, &stdin];
let context = context
.insert_front_matter_and_raw_text_from_existing_content(&v)
.unwrap()
.set_state_ready_for_content_template();
let mut n =
Note::<ContentString>::from_content_template(context, TemplateKind::FromDir).unwrap();
assert!(n.content.header().starts_with("title: my dir"));
assert_eq!(n.content.borrow_dependent().body, "\n\n");
assert_eq!(
n.context
.get(TMPL_VAR_FM_ALL)
.unwrap()
.get("fm_title")
.unwrap()
.as_str(),
Some("my dir")
);
assert_eq!(
n.context
.get(TMPL_VAR_FM_ALL)
.unwrap()
.get("fm_subtitle")
.unwrap()
.as_str(),
Some("Note")
);
n.render_filename(TemplateKind::FromDir).unwrap();
n.set_next_unused_rendered_filename().unwrap();
n.save().unwrap();
assert!(n.rendered_filename.is_file());
let raw_note = fs::read_to_string(n.rendered_filename).unwrap();
#[cfg(not(target_family = "windows"))]
assert!(raw_note.starts_with("\u{feff}---\ntitle: my dir"));
#[cfg(target_family = "windows")]
assert!(raw_note.starts_with("\u{feff}---\r\ntitle: my dir"));
}
#[test]
fn test_from_content_template2() {
use crate::content::Content;
use crate::content::ContentString;
use crate::context::Context;
use crate::note::Note;
use crate::settings::Settings;
use crate::settings::SETTINGS;
use crate::template::TemplateKind;
use parking_lot::RwLockWriteGuard;
use std::env::temp_dir;
use std::fs;
let mut settings = SETTINGS.write();
*settings = Settings::default();
let _settings = RwLockWriteGuard::<'_, _>::downgrade(settings);
let notedir = temp_dir();
let context = Context::from(¬edir).unwrap();
let html_clipboard =
ContentString::from_string("html_clp\n".to_string(), "html_clipboard".to_string());
let txt_clipboard =
ContentString::from_string("txt_clp\n".to_string(), "txt_clipboard".to_string());
let stdin = ContentString::from_string("std\n".to_string(), "stdin".to_string());
let v = vec![&html_clipboard, &txt_clipboard, &stdin];
let context = context
.insert_front_matter_and_raw_text_from_existing_content(&v)
.unwrap();
assert!(
html_clipboard.header().is_empty()
&& txt_clipboard.header().is_empty()
&& stdin.header().is_empty()
);
assert!(
!html_clipboard.body().is_empty()
&& !txt_clipboard.body().is_empty()
&& !stdin.body().is_empty()
);
let context = context.set_state_ready_for_content_template();
let mut n =
Note::<ContentString>::from_content_template(context, TemplateKind::FromDir).unwrap();
let expected_body = "\nstd\ntxt_clp\n\n";
assert_eq!(n.content.body(), expected_body);
assert_eq!(
n.context
.get(TMPL_VAR_FM_ALL)
.unwrap()
.get("fm_title")
.unwrap()
.as_str(),
Some("std")
);
assert_eq!(
n.context
.get(TMPL_VAR_FM_ALL)
.unwrap()
.get("fm_subtitle")
.unwrap()
.as_str(),
Some("Note")
);
n.render_filename(TemplateKind::FromDir).unwrap();
n.set_next_unused_rendered_filename().unwrap();
n.save().unwrap();
assert!(n
.rendered_filename
.as_os_str()
.to_str()
.unwrap()
.contains("std--Note"));
assert!(n.rendered_filename.is_file());
let raw_note = fs::read_to_string(&n.rendered_filename).unwrap();
println!("{}", raw_note);
#[cfg(not(target_family = "windows"))]
assert!(raw_note.starts_with("\u{feff}---\ntitle: std"));
#[cfg(target_family = "windows")]
assert!(raw_note.starts_with("\u{feff}---\r\ntitle:"));
}
#[test]
fn test_from_content_template3() {
use crate::content::Content;
use crate::content::ContentString;
use crate::context::Context;
use crate::note::Note;
use crate::settings::Settings;
use crate::settings::SETTINGS;
use crate::template::TemplateKind;
use parking_lot::RwLockWriteGuard;
use std::env::temp_dir;
use std::fs;
let mut settings = SETTINGS.write();
*settings = Settings::default();
let _settings = RwLockWriteGuard::<'_, _>::downgrade(settings);
let notedir = temp_dir().join("123-my dir/");
let context = Context::from(¬edir).unwrap();
let html_clipboard = ContentString::from_string(
"my HTML clipboard\n".to_string(),
"html_clipboard".to_string(),
);
let txt_clipboard = ContentString::from_string(
"my TXT clipboard\n".to_string(),
"txt_clipboard".to_string(),
);
let stdin = ContentString::from_string(
"---\nsubtitle: \"this overwrites\"\n---\nstdin body".to_string(),
"stdin".to_string(),
);
let v = vec![&html_clipboard, &txt_clipboard, &stdin];
let context = context
.insert_front_matter_and_raw_text_from_existing_content(&v)
.unwrap();
assert!(
!html_clipboard.header().is_empty()
|| !txt_clipboard.header().is_empty()
|| !stdin.header().is_empty()
);
let context = context.set_state_ready_for_content_template();
let mut n =
Note::<ContentString>::from_content_template(context, TemplateKind::FromDir).unwrap();
let expected_body = "\nstdin body\nmy TXT clipboard\n\n";
assert_eq!(n.content.body(), expected_body);
assert_eq!(
n.context
.get(TMPL_VAR_FM_ALL)
.unwrap()
.get("fm_title")
.unwrap()
.as_str(),
Some("stdin bod")
);
assert_eq!(
n.context
.get(TMPL_VAR_FM_ALL)
.unwrap()
.get("fm_subtitle")
.unwrap()
.as_str(),
Some("this over")
);
n.render_filename(TemplateKind::FromDir).unwrap();
n.set_next_unused_rendered_filename().unwrap();
n.save().unwrap();
assert!(n
.rendered_filename
.as_os_str()
.to_str()
.unwrap()
.contains("stdin bod--this over"));
assert!(n.rendered_filename.is_file());
let raw_note = fs::read_to_string(n.rendered_filename).unwrap();
#[cfg(not(target_family = "windows"))]
assert!(raw_note.starts_with("\u{feff}---\ntitle: stdin bod"));
#[cfg(target_family = "windows")]
assert!(raw_note.starts_with("\u{feff}---\r\ntitle: stdin bod"));
}
#[test]
fn test_from_content_template4() {
use crate::content::Content;
use crate::content::ContentString;
use crate::context::Context;
use crate::note::Note;
use crate::settings::Settings;
use crate::settings::SETTINGS;
use crate::template::TemplateKind;
use parking_lot::RwLockWriteGuard;
use std::env::temp_dir;
use std::fs;
let mut settings = SETTINGS.write();
*settings = Settings::default();
let _settings = RwLockWriteGuard::<'_, _>::downgrade(settings);
let raw = "This simulates a non tp-note file";
let non_notefile = temp_dir().join("20221030-some.pdf");
fs::write(&non_notefile, raw.as_bytes()).unwrap();
let expected = temp_dir().join("20221030-some.pdf--Note.md");
let _ = fs::remove_file(&expected);
let context = Context::from(&non_notefile).unwrap();
let html_clipboard = ContentString::from_string(
"my HTML clipboard\n".to_string(),
"html_clipboard".to_string(),
);
let txt_clipboard = ContentString::from_string(
"my TXT clipboard\n".to_string(),
"txt_clipboard".to_string(),
);
let stdin =
ContentString::from_string_with_cr("my stdin\n".to_string(), "stdin".to_string());
let v = vec![&html_clipboard, &txt_clipboard, &stdin];
let context = context
.insert_front_matter_and_raw_text_from_existing_content(&v)
.unwrap()
.set_state_ready_for_content_template();
let mut n =
Note::<ContentString>::from_content_template(context, TemplateKind::AnnotateFile)
.unwrap();
let expected_body =
"\n[20221030-some.pdf](<20221030-some.pdf>)\n____\n\nmy stdin\nmy TXT clipboard\n\n";
assert_eq!(n.content.body(), expected_body);
assert_eq!(
n.context
.get(TMPL_VAR_FM_ALL)
.unwrap()
.get("fm_title")
.unwrap()
.as_str(),
Some("some.pdf")
);
assert_eq!(
n.context
.get(TMPL_VAR_FM_ALL)
.unwrap()
.get("fm_subtitle")
.unwrap()
.as_str(),
Some("Note")
);
n.render_filename(TemplateKind::AnnotateFile).unwrap();
n.set_next_unused_rendered_filename().unwrap();
n.save().unwrap();
assert_eq!(n.rendered_filename, expected);
fs::remove_file(n.rendered_filename).unwrap();
}
#[test]
fn test_from_existing_content5() {
use crate::content::Content;
use crate::content::ContentString;
use crate::context::Context;
use crate::note::Note;
use crate::template::TemplateKind;
use std::env::temp_dir;
use std::fs;
let raw = "Body text without header";
let notefile = temp_dir().join("20221030-hello -- world.md");
let _ = fs::write(¬efile, raw.as_bytes());
let expected = temp_dir().join("20221030-hello--world.md");
let _ = fs::remove_file(&expected);
let context = Context::from(¬efile).unwrap();
let content = <ContentString as Content>::open(¬efile).unwrap();
let context = context
.insert_front_matter_and_raw_text_from_existing_content(&vec![&content])
.unwrap()
.set_state_ready_for_content_template();
let mut n = Note::<ContentString>::from_content_template(
context.clone(),
TemplateKind::FromTextFile,
)
.unwrap();
assert!(!n.content.header().is_empty());
assert_eq!(
n.context
.get(TMPL_VAR_FM_ALL)
.unwrap()
.get("fm_title")
.unwrap()
.as_str(),
Some("hello ")
);
assert_eq!(
n.context
.get(TMPL_VAR_FM_ALL)
.unwrap()
.get("fm_subtitle")
.unwrap()
.as_str(),
Some(" world")
);
assert_eq!(n.content.body().trim(), raw);
n.render_filename(TemplateKind::FromTextFile).unwrap();
n.set_next_unused_rendered_filename().unwrap();
n.save_and_delete_from(context.get_path()).unwrap();
assert_eq!(&n.rendered_filename, &expected);
assert!(n.rendered_filename.is_file());
let raw_note = fs::read_to_string(n.rendered_filename).unwrap();
#[cfg(not(target_family = "windows"))]
assert!(raw_note.starts_with("\u{feff}---\ntitle: 'hello '"));
#[cfg(target_family = "windows")]
assert!(raw_note.starts_with("\u{feff}---\r\ntitle: 'hello '"));
}
}