use crate::config::CFG;
use crate::config::CLIPBOARD;
use crate::config::STDIN;
use crate::content::Content;
use crate::error::NoteError;
use crate::error::FRONT_MATTER_ERROR_MAX_LINES;
use crate::filename;
use crate::filename::MarkupLanguage;
use crate::filter::ContextWrapper;
use crate::filter::TERA;
use crate::note_error_tera_template;
use parse_hyperlinks::renderer::text_links2html;
#[cfg(feature = "viewer")]
use parse_hyperlinks::renderer::text_rawlinks2html;
#[cfg(feature = "renderer")]
use pulldown_cmark::{html, Options, Parser};
#[cfg(feature = "renderer")]
use rst_parser::parse;
#[cfg(feature = "renderer")]
use rst_renderer::render_html;
use std::default::Default;
use std::env;
use std::fs::OpenOptions;
use std::io;
use std::io::Write;
use std::matches;
use std::path::{Path, PathBuf};
use std::str;
use tera::Tera;
pub const TMPL_VAR_PATH: &str = "path";
const TMPL_VAR_DIR_PATH: &str = "dir_path";
const TMPL_VAR_CLIPBOARD_HEADER: &str = "clipboard_header";
const TMPL_VAR_CLIPBOARD: &str = "clipboard";
const TMPL_VAR_STDIN_HEADER: &str = "stdin_header";
const TMPL_VAR_STDIN: &str = "stdin";
const TMPL_VAR_EXTENSION_DEFAULT: &str = "extension_default";
const TMPL_VAR_USERNAME: &str = "username";
pub const TMPL_VAR_FM_: &str = "fm_";
const TMPL_VAR_FM_ALL: &str = "fm_all";
const TMPL_VAR_FM_ALL_YAML: &str = "fm_all_yaml";
const TMPL_VAR_FM_FILE_EXT: &str = "fm_file_ext";
pub const TMPL_VAR_FM_NO_FILENAME_SYNC: &str = "fm_no_filename_sync";
const TMPL_VAR_NOTE_BODY: &str = "note_body";
pub const TMPL_VAR_NOTE_JS: &str = "note_js";
#[cfg(feature = "viewer")]
pub const TMPL_VAR_NOTE_ERROR: &str = "note_error";
#[cfg(feature = "viewer")]
pub const TMPL_VAR_NOTE_ERRONEOUS_CONTENT: &str = "note_erroneous_content";
#[derive(Debug, PartialEq)]
pub struct Note {
pub context: ContextWrapper,
pub content: Content,
}
#[derive(Debug, PartialEq)]
struct FrontMatter {
map: tera::Map<String, tera::Value>,
}
use std::fs;
impl Note {
pub fn from_existing_note(path: &Path) -> Result<Self, NoteError> {
let content =
Content::from_input_with_cr(fs::read_to_string(path).map_err(|e| NoteError::Read {
path: path.to_path_buf(),
source: e,
})?);
let mut context = Self::capture_environment(&path)?;
(*context).insert(TMPL_VAR_FM_ALL_YAML, &content.borrow_dependent().header);
let fm = Note::deserialize_header(content.borrow_dependent().header)?;
if !&CFG.tmpl_compulsory_field_content.is_empty()
&& fm.map.get(&CFG.tmpl_compulsory_field_content).is_none()
{
return Err(NoteError::MissingFrontMatterField {
field_name: CFG.tmpl_compulsory_field_content.to_owned(),
});
}
Self::register_front_matter(&mut context, &fm);
Ok(Self {
context,
content,
})
}
pub fn from_content_template(path: &Path, template: &str) -> Result<Self, NoteError> {
let mut context = Self::capture_environment(&path)?;
let content = Content::from({
let mut tera = Tera::default();
tera.extend(&TERA)?;
tera.render_str(template, &context)
.map_err(|e| note_error_tera_template!(e))?
});
log::trace!(
"Available substitution variables for content template:\n{:#?}",
*context
);
log::trace!("Applying content template:\n{}", template);
log::debug!(
"Rendered content template:\n---\n{}\n---\n{}",
content.borrow_dependent().header,
content.borrow_dependent().body.trim()
);
let fm = Note::deserialize_header(content.borrow_dependent().header)?;
Self::register_front_matter(&mut context, &fm);
Ok(Self {
context,
content,
})
}
fn capture_environment(path: &Path) -> Result<ContextWrapper, NoteError> {
let mut context = ContextWrapper::new();
let file = path.to_str().unwrap_or_default();
(*context).insert(TMPL_VAR_PATH, &file);
let dir_path = if path.is_dir() {
path
} else {
path.parent().unwrap_or_else(|| Path::new("./"))
};
(*context).insert(TMPL_VAR_DIR_PATH, &dir_path.to_str().unwrap_or_default());
(*context).insert(
TMPL_VAR_CLIPBOARD_HEADER,
CLIPBOARD.borrow_dependent().header,
);
(*context).insert(TMPL_VAR_CLIPBOARD, CLIPBOARD.borrow_dependent().body);
(*context).insert(TMPL_VAR_STDIN_HEADER, STDIN.borrow_dependent().header);
(*context).insert(TMPL_VAR_STDIN, STDIN.borrow_dependent().body);
let stdin_fm = Self::deserialize_header(STDIN.borrow_dependent().header);
match stdin_fm {
Ok(ref stdin_fm) => log::trace!(
"YAML front matter in the input stream stdin found:\n{:#?}",
&stdin_fm
),
Err(ref e) => {
if !STDIN.borrow_dependent().header.is_empty() {
return Err(NoteError::InvalidStdinYaml {
source_str: e.to_string(),
});
}
}
};
let clipboard_fm = Self::deserialize_header(CLIPBOARD.borrow_dependent().header);
match clipboard_fm {
Ok(ref clipboard_fm) => log::trace!(
"YAML front matter in the clipboard found:\n{:#?}",
&clipboard_fm
),
Err(ref e) => {
if !CLIPBOARD.borrow_dependent().header.is_empty() {
return Err(NoteError::InvalidClipboardYaml {
source_str: e.to_string(),
});
}
}
};
if let Ok(fm) = clipboard_fm {
Self::register_front_matter(&mut context, &fm);
}
if let Ok(fm) = stdin_fm {
Self::register_front_matter(&mut context, &fm);
}
(*context).insert(TMPL_VAR_EXTENSION_DEFAULT, CFG.extension_default.as_str());
let author = env::var("LOGNAME").unwrap_or_else(|_| {
env::var("USERNAME").unwrap_or_else(|_| env::var("USER").unwrap_or_default())
});
(*context).insert(TMPL_VAR_USERNAME, &author);
context.dir_path = dir_path.to_path_buf();
Ok(context)
}
fn register_front_matter(context: &mut ContextWrapper, fm: &FrontMatter) {
let mut tera_map = tera::Map::new();
for (name, value) in &fm.map {
let val = match value {
tera::Value::String(_) => value.to_owned(),
tera::Value::Number(_) => value.to_owned(),
tera::Value::Bool(_) => value.to_owned(),
_ => tera::Value::String(value.to_string()),
};
tera_map.insert(name.to_string(), val.to_owned());
let mut var_name = TMPL_VAR_FM_.to_string();
var_name.push_str(name);
(*context).insert(&var_name, &val);
}
(*context).insert(TMPL_VAR_FM_ALL, &tera_map);
}
pub fn render_filename(&self, template: &str) -> Result<PathBuf, NoteError> {
log::trace!(
"Available substitution variables for the filename template:\n{:#?}",
*self.context
);
log::trace!("Applying the filename template:\n{}", template);
let mut file_path = self.context.dir_path.to_owned();
let mut tera = Tera::default();
tera.extend(&TERA)?;
match tera.render_str(template, &self.context) {
Ok(filename) => {
log::debug!("Rendered filename template:\n{:?}", filename.trim());
file_path.push(filename.trim());
}
Err(e) => {
return Err(note_error_tera_template!(e));
}
}
Ok(filename::shorten_filename(file_path))
}
fn deserialize_header(header: &str) -> Result<FrontMatter, NoteError> {
if header.is_empty() {
return Err(NoteError::MissingFrontMatter {
compulsory_field: CFG.tmpl_compulsory_field_content.to_owned(),
});
};
let map: tera::Map<String, tera::Value> =
serde_yaml::from_str(&header).map_err(|e| NoteError::InvalidFrontMatterYaml {
front_matter: header
.lines()
.enumerate()
.map(|(n, s)| format!("{:03}: {}\n", n + 1, s))
.take(FRONT_MATTER_ERROR_MAX_LINES)
.collect::<String>(),
source_error: e,
})?;
let fm = FrontMatter { map };
if let Some(tera::Value::String(sort_tag)) = &fm.map.get("sort_tag") {
if !sort_tag.is_empty() {
if sort_tag
.chars()
.filter(|&c| !c.is_numeric() && c != '_' && c != '-')
.count()
> 0
{
return Err(NoteError::SortTagVarInvalidChar {
sort_tag: sort_tag.to_owned(),
});
}
};
};
if let Some(tera::Value::String(extension)) = &fm.map.get("file_ext") {
let extension_is_unknown =
matches!(MarkupLanguage::new(extension), MarkupLanguage::None);
if extension_is_unknown {
return Err(NoteError::FileExtNotRegistered {
extension: extension.to_owned(),
md_ext: CFG.note_file_extensions_md.to_owned(),
rst_ext: CFG.note_file_extensions_rst.to_owned(),
html_ext: CFG.note_file_extensions_html.to_owned(),
txt_ext: CFG.note_file_extensions_txt.to_owned(),
no_viewer_ext: CFG.note_file_extensions_no_viewer.to_owned(),
});
}
};
Ok(fm)
}
pub fn render_and_write_content(
&mut self,
note_path: &Path,
template: &str,
export_dir: &Path,
) -> Result<(), NoteError> {
let mut html_path = PathBuf::new();
if export_dir
.as_os_str()
.to_str()
.unwrap_or_default()
.is_empty()
{
html_path = note_path
.parent()
.unwrap_or_else(|| Path::new(""))
.to_path_buf();
let mut html_filename = note_path
.file_name()
.unwrap_or_default()
.to_str()
.unwrap_or_default()
.to_string();
html_filename.push_str(".html");
html_path.push(PathBuf::from(html_filename.as_str()));
} else if export_dir.as_os_str().to_str().unwrap_or_default() != "-" {
html_path = export_dir.to_owned();
let mut html_filename = note_path
.file_name()
.unwrap_or_default()
.to_str()
.unwrap_or_default()
.to_string();
html_filename.push_str(".html");
html_path.push(PathBuf::from(html_filename.as_str()));
} else {
}
if html_path
.as_os_str()
.to_str()
.unwrap_or_default()
.is_empty()
{
log::info!("Rendering HTML to STDOUT (`{:?}`)", export_dir);
} else {
log::info!("Rendering HTML into: {:?}", html_path);
};
let note_path_ext = note_path
.extension()
.unwrap_or_default()
.to_str()
.unwrap_or_default();
if html_path
.as_os_str()
.to_str()
.unwrap_or_default()
.is_empty()
{
let stdout = io::stdout();
let mut handle = stdout.lock();
handle.write_all(
self.render_content(¬e_path_ext, &template, "")?
.as_bytes(),
)?;
} else {
let mut handle = OpenOptions::new()
.write(true)
.create(true)
.open(&html_path)?;
handle.write_all(
self.render_content(¬e_path_ext, &template, "")?
.as_bytes(),
)?;
};
Ok(())
}
#[inline]
pub fn render_content(
&mut self,
file_ext: &str,
tmpl: &str,
java_script_insert: &str,
) -> Result<String, NoteError> {
let input = self.content.borrow_dependent().body;
let ext = match self.context.get(TMPL_VAR_FM_FILE_EXT) {
Some(tera::Value::String(file_ext)) => Some(file_ext.as_str()),
_ => None,
};
let html_output = match MarkupLanguage::from(ext, &file_ext) {
#[cfg(feature = "renderer")]
MarkupLanguage::Markdown => Self::render_md_content(input),
#[cfg(feature = "renderer")]
MarkupLanguage::RestructuredText => Self::render_rst_content(input)?,
MarkupLanguage::Html => input.to_string(),
_ => Self::render_txt_content(input),
};
self.context.insert(TMPL_VAR_NOTE_BODY, &html_output);
self.context.insert(TMPL_VAR_NOTE_JS, java_script_insert);
let mut tera = Tera::default();
tera.extend(&TERA)?;
let html = tera
.render_str(tmpl, &self.context)
.map_err(|e| note_error_tera_template!(e))?;
Ok(html)
}
#[inline]
#[cfg(feature = "renderer")]
fn render_md_content(markdown_input: &str) -> String {
let options = Options::all();
let parser = Parser::new_ext(markdown_input, options);
let mut html_output: String = String::with_capacity(markdown_input.len() * 3 / 2);
html::push_html(&mut html_output, parser);
html_output
}
#[inline]
#[cfg(feature = "renderer")]
fn render_rst_content(rest_input: &str) -> Result<String, NoteError> {
let mut rest_input = rest_input.trim_start();
while rest_input.ends_with("\n\n") {
rest_input = &rest_input[..rest_input.len() - 1];
}
let document = parse(rest_input.trim_start())
.map_err(|e| NoteError::RstParse { msg: e.to_string() })?;
let mut html_output: Vec<u8> = Vec::with_capacity(rest_input.len() * 3 / 2);
let _ = render_html(&document, &mut html_output, false);
Ok(str::from_utf8(&html_output)?.to_string())
}
#[inline]
fn render_txt_content(other_input: &str) -> String {
text_links2html(other_input)
}
#[inline]
#[cfg(feature = "viewer")]
pub fn render_erroneous_content(
doc_path: &Path,
template: &str,
java_script_insert: &str,
err: NoteError,
) -> Result<String, NoteError> {
let mut context = tera::Context::new();
let err = err.to_string();
context.insert(TMPL_VAR_NOTE_ERROR, &err);
context.insert(TMPL_VAR_PATH, &doc_path.to_str().unwrap_or_default());
context.insert(TMPL_VAR_NOTE_JS, &java_script_insert);
let note_erroneous_content = fs::read_to_string(&doc_path).unwrap_or_default();
let note_erroneous_content = note_erroneous_content.trim_start_matches('\u{feff}');
let note_erroneous_content = text_rawlinks2html(¬e_erroneous_content);
context.insert(TMPL_VAR_NOTE_ERRONEOUS_CONTENT, ¬e_erroneous_content);
let mut tera = Tera::default();
tera.extend(&TERA)?;
let html = tera
.render_str(&template, &context)
.map_err(|e| note_error_tera_template!(e))?;
Ok(html)
}
}
#[cfg(test)]
mod tests {
use super::ContextWrapper;
use super::FrontMatter;
use super::Note;
use serde_json::json;
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 { map: expected };
assert_eq!(
expected_front_matter,
Note::deserialize_header(&input).unwrap()
);
let input = "";
assert!(Note::deserialize_header(&input).is_err());
let input = "# document start
title: The book
subtitle: you always wanted
author: It's me
sort_tag: 123x4";
assert!(Note::deserialize_header(&input).is_err());
let input = "# document start
title: The book
subtitle: you always wanted
author: It's me
sort_tag: 123x4
file_ext: xyz";
assert!(Note::deserialize_header(&input).is_err());
}
#[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 = tmp.clone();
let mut input1 = ContextWrapper::new();
let input2 = FrontMatter { map: tmp };
let mut expected = ContextWrapper::new();
(*expected).insert("fm_file_ext".to_string(), &json!("md")); (*expected).insert("fm_height".to_string(), &json!(1.23)); (*expected).insert("fm_count".to_string(), &json!(2)); (*expected).insert("fm_neg".to_string(), &json!(-1)); (*expected).insert("fm_flag".to_string(), &json!(true)); (*expected).insert("fm_numbers".to_string(), &json!("[1,3,5]")); tmp2.remove("numbers");
tmp2.insert("numbers".to_string(), json!("[1,3,5]")); (*expected).insert("fm_all".to_string(), &tmp2);
Note::register_front_matter(&mut input1, &input2);
let result = input1;
assert_eq!(result, expected);
}
}