use crate::config::CFG;
use crate::content::Content;
use crate::context::ContextWrapper;
use crate::error::NoteError;
use crate::error::FRONT_MATTER_ERROR_MAX_LINES;
use crate::filename;
use crate::filename::MarkupLanguage;
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::fs::File;
use std::fs::OpenOptions;
use std::io;
use std::io::prelude::*;
use std::io::Write;
use std::matches;
use std::path::{Path, PathBuf};
use std::str;
use std::time::SystemTime;
use tera::Tera;
pub const TMPL_VAR_PATH: &str = "path";
pub const TMPL_VAR_DIR_PATH: &str = "dir_path";
pub const TMPL_VAR_CLIPBOARD_HEADER: &str = "clipboard_header";
pub const TMPL_VAR_CLIPBOARD: &str = "clipboard";
pub const TMPL_VAR_STDIN_HEADER: &str = "stdin_header";
pub const TMPL_VAR_STDIN: &str = "stdin";
pub const TMPL_VAR_EXTENSION_DEFAULT: &str = "extension_default";
pub const TMPL_VAR_USERNAME: &str = "username";
pub const TMPL_VAR_LANG: &str = "lang";
const TMPL_VAR_PATH_FILE_TEXT: &str = "path_file_text";
const TMPL_VAR_PATH_FILE_DATE: &str = "path_file_date";
pub const TMPL_VAR_FM_: &str = "fm_";
pub 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";
const TMPL_VAR_FM_SORT_TAG: &str = "fm_sort_tag";
pub const TMPL_VAR_FM_NO_FILENAME_SYNC: &str = "fm_no_filename_sync";
pub const TMPL_VAR_FM_FILENAME_SYNC: &str = "fm_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)]
pub struct FrontMatter {
pub map: tera::Map<String, tera::Value>,
}
impl TryFrom<&Content> for FrontMatter {
type Error = NoteError;
fn try_from(content: &Content) -> Result<FrontMatter, NoteError> {
let header = content.borrow_dependent().header;
Self::try_from(header)
}
}
impl TryFrom<&str> for FrontMatter {
type Error = NoteError;
fn try_from(header: &str) -> Result<FrontMatter, NoteError> {
if header.is_empty() {
return Err(NoteError::MissingFrontMatter {
compulsory_field: CFG.tmpl.compulsory_header_field.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(TMPL_VAR_FM_SORT_TAG.trim_start_matches(TMPL_VAR_FM_))
{
if !sort_tag.is_empty() {
if !sort_tag
.trim_start_matches(
&CFG.filename.sort_tag_chars.chars().collect::<Vec<char>>()[..],
)
.is_empty()
{
return Err(NoteError::SortTagVarInvalidChar {
sort_tag: sort_tag.to_owned(),
sort_tag_chars: CFG.filename.sort_tag_chars.escape_default().to_string(),
});
}
};
};
if let Some(tera::Value::String(file_ext)) = &fm
.map
.get(TMPL_VAR_FM_FILE_EXT.trim_start_matches(TMPL_VAR_FM_))
{
let extension_is_unknown =
matches!(MarkupLanguage::from(&**file_ext), MarkupLanguage::None);
if extension_is_unknown {
return Err(NoteError::FileExtNotRegistered {
extension: file_ext.to_owned(),
md_ext: CFG.filename.extensions_md.to_owned(),
rst_ext: CFG.filename.extensions_rst.to_owned(),
html_ext: CFG.filename.extensions_html.to_owned(),
txt_ext: CFG.filename.extensions_txt.to_owned(),
no_viewer_ext: CFG.filename.extensions_no_viewer.to_owned(),
});
}
};
Ok(fm)
}
}
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 fm = FrontMatter::try_from(&content)?;
if !&CFG.tmpl.compulsory_header_field.is_empty() {
if let Some(tera::Value::String(header_field)) =
fm.map.get(&CFG.tmpl.compulsory_header_field)
{
if header_field.is_empty() {
return Err(NoteError::CompulsoryFrontMatterFieldIsEmpty {
field_name: CFG.tmpl.compulsory_header_field.to_owned(),
});
};
} else {
return Err(NoteError::MissingFrontMatterField {
field_name: CFG.tmpl.compulsory_header_field.to_owned(),
});
}
}
let mut context = ContextWrapper::new();
context.insert_environment(path)?;
(*context).insert(TMPL_VAR_FM_ALL_YAML, &content.borrow_dependent().header);
context.insert_front_matter(&fm);
Ok(Self {
context,
content,
})
}
pub fn from_text_file(path: &Path, template: &str) -> Result<Self, NoteError> {
let mut context = ContextWrapper::new();
{
let mut file = File::open(path)?;
let mut raw_text = String::new();
file.read_to_string(&mut raw_text)?;
let content = Content::from_input_with_cr(raw_text);
let header = &content.borrow_dependent().header;
if !header.is_empty() {
return Err(NoteError::CannotPrependHeader {
existing_header: header
.lines()
.take(5)
.map(|s| s.to_string())
.collect::<String>(),
});
};
(*context).insert(TMPL_VAR_PATH_FILE_TEXT, &content.borrow_dependent().body);
let metadata = file.metadata()?;
if let Ok(time) = metadata.created() {
(*context).insert(
TMPL_VAR_PATH_FILE_DATE,
&time
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
);
}
}
Self::from_content_template_context(path, template, context)
}
pub fn from_content_template(path: &Path, template: &str) -> Result<Self, NoteError> {
let context = ContextWrapper::new();
Self::from_content_template_context(path, template, context)
}
fn from_content_template_context(
path: &Path,
template: &str,
mut context: ContextWrapper,
) -> Result<Self, NoteError> {
context.insert_environment(path)?;
log::trace!(
"Available substitution variables for content template:\n{:#?}",
*context
);
log::trace!("Applying content template:\n{}", template);
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::debug!(
"Rendered content template:\n---\n{}\n---\n{}",
content.borrow_dependent().header,
content.borrow_dependent().body.trim()
);
let fm = FrontMatter::try_from(&content)?;
context.insert_front_matter(&fm);
Ok(Self {
context,
content,
})
}
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))
}
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(note_path_ext, template, "")?.as_bytes())?;
} else {
let mut handle = OpenOptions::new()
.write(true)
.create(true)
.open(&html_path)?;
handle.write_all(self.render_content(note_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 fm_file_ext = match self.context.get(TMPL_VAR_FM_FILE_EXT) {
Some(tera::Value::String(fm_file_ext)) => fm_file_ext.as_str(),
_ => "",
};
let html_output = match MarkupLanguage::from(fm_file_ext).or(MarkupLanguage::from(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(note_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 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, FrontMatter::try_from(input).unwrap());
let input = "";
assert!(FrontMatter::try_from(input).is_err());
let input = "# document start
title: The book
subtitle: you always wanted
author: It's me
sort_tag: 123x4";
assert!(FrontMatter::try_from(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!(FrontMatter::try_from(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);
input1.insert_front_matter(&input2);
let result = input1;
assert_eq!(result, expected);
}
}