use crate::config::ARGS;
use anyhow::{anyhow, Result};
use core::marker::PhantomPinned;
use std::fmt;
use std::fs::OpenOptions;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::pin::Pin;
#[derive(Debug, PartialEq)]
pub struct Content<'a> {
s: String,
pub header: &'a str,
pub body: &'a str,
_pin: PhantomPinned,
}
macro_rules! unborrow {
($slice_u8:expr) => {{
use std::slice;
use std::str;
let ptr = $slice_u8.as_ptr();
let len = $slice_u8.len();
&unsafe { str::from_utf8_unchecked(slice::from_raw_parts(ptr, len)) }
}};
}
impl<'a> Content<'a> {
pub fn new(input: String, relax: bool) -> Pin<Box<Self>> {
let input = Self::remove_cr(input);
let mut c = Box::pin(Content {
s: input,
header: "",
body: "",
_pin: PhantomPinned,
});
let c_s = unborrow!(&c.s);
let (header, body) = Self::split(&c_s, relax);
let mut_ref = Pin::as_mut(&mut c);
unsafe {
Pin::get_unchecked_mut(mut_ref).header = header;
}
let mut_ref = Pin::as_mut(&mut c);
unsafe {
Pin::get_unchecked_mut(mut_ref).body = body;
}
c
}
pub fn is_empty(&self) -> bool {
self.s.is_empty()
}
#[inline]
fn remove_cr(input: String) -> String {
if input.find('\r').is_none() {
input
} else {
input.replace("\r\n", "\n")
}
}
fn split(content: &'a str, relax: bool) -> (&'a str, &'a str) {
let content = content.trim_start_matches('\u{feff}');
if content.is_empty() {
return ("", "");
};
let pattern = "---";
let fm_start = if content.starts_with(pattern) {
pattern.len()
} else {
if !relax {
return ("", content);
};
let pattern = "\n\n---";
if let Some(start) = content.find(pattern).map(|x| x + pattern.len()) {
start
} else {
return ("", content);
}
};
let pattern1 = "\n---";
let pattern2 = "\n...";
let pattern_len = 4;
let fm_end = content[fm_start..]
.find(pattern1)
.or_else(|| content[fm_start..].find(pattern2))
.map(|x| x + fm_start);
let fm_end = if let Some(n) = fm_end {
n
} else {
return ("", content);
};
let mut body_start = fm_end + pattern_len;
if (content.len() > body_start) && (content.as_bytes()[body_start] == b'\n') {
body_start += 1;
};
(content[fm_start..fm_end].trim(), &content[body_start..])
}
pub fn write_to_disk(
self: Pin<Box<Self>>,
new_fqfn: PathBuf,
) -> Result<PathBuf, anyhow::Error> {
let outfile = OpenOptions::new()
.write(true)
.create_new(true)
.open(&new_fqfn);
match outfile {
Ok(mut outfile) => {
if ARGS.debug {
eprintln!("Creating file: {:?}", new_fqfn);
};
write!(outfile, "\u{feff}")?;
if !self.header.is_empty() {
write!(outfile, "---")?;
#[cfg(target_family = "windows")]
write!(outfile, "\r")?;
write!(outfile, "\n")?;
for l in self.header.lines() {
write!(outfile, "{}", l)?;
#[cfg(target_family = "windows")]
write!(outfile, "\r")?;
write!(outfile, "\n")?;
}
write!(outfile, "---")?;
#[cfg(target_family = "windows")]
write!(outfile, "\r")?;
write!(outfile, "\n")?;
};
for l in self.body.lines() {
write!(outfile, "{}", l)?;
#[cfg(target_family = "windows")]
write!(outfile, "\r")?;
write!(outfile, "\n")?;
}
}
Err(e) => {
if Path::new(&new_fqfn).exists() {
return Err(anyhow!(format!(
"Can not write new note, file exists:\n\
\t{:?}\n{}",
new_fqfn, e
)));
} else {
return Err(anyhow!(format!(
"Can not write file: {:?}\n{}",
new_fqfn, e
)));
}
}
}
Ok(new_fqfn)
}
}
impl<'a> fmt::Display for Content<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let s = if self.header.is_empty() {
self.body.to_string()
} else {
format!("\u{feff}---\n{}\n---\n{}", &self.header, &self.body)
};
write!(f, "{}", s)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new() {
let content = Content::new("first\r\nsecond\r\nthird".to_string(), false);
assert_eq!(content.body, "first\nsecond\nthird");
let content = Content::new("first\nsecond\nthird".to_string(), false);
assert_eq!(content.body, "first\nsecond\nthird");
let content = Content::new("\u{feff}first\nsecond\nthird".to_string(), false);
assert_eq!(content.body, "first\nsecond\nthird");
let content = Content::new("\u{feff}---\nfirst\n---\nsecond\nthird".to_string(), false);
assert_eq!(content.header, "first");
assert_eq!(content.body, "second\nthird");
let content = Content::new("\u{feff}---\nfirst\n---".to_string(), false);
assert_eq!(content.header, "first");
assert_eq!(content.body, "");
let content = Content::new("\u{feff}not ignored\n\n---\nfirst\n---".to_string(), false);
assert_eq!(content.header, "");
assert_eq!(content.body, "not ignored\n\n---\nfirst\n---");
}
#[test]
fn test_new_relax() {
let content = Content::new("\u{feff}---\nfirst\n---".to_string(), true);
assert_eq!(content.header, "first");
assert_eq!(content.body, "");
let content = Content::new("\u{feff}ignored\n\n---\nfirst\n---".to_string(), true);
assert_eq!(content.header, "first");
assert_eq!(content.body, "");
}
#[test]
fn test_split() {
let input_stream = String::from("---first\n---\nsecond\nthird");
let expected = ("first", "second\nthird");
let result = Content::split(&input_stream, false);
assert_eq!(result, expected);
let input_stream = String::from("---\nfirst\n---\nsecond\nthird");
let expected = ("first", "second\nthird");
let result = Content::split(&input_stream, false);
assert_eq!(result, expected);
let input_stream = String::from("---\n\nfirst\n\n---\nsecond\nthird");
let expected = ("first", "second\nthird");
let result = Content::split(&input_stream, false);
assert_eq!(result, expected);
let input_stream = String::from("---\nfirst\n---\n\nsecond\nthird\n");
let expected = ("first", "\nsecond\nthird\n");
let result = Content::split(&input_stream, false);
assert_eq!(result, expected);
let input_stream = String::from("\nsecond\nthird");
let expected = ("", "\nsecond\nthird");
let result = Content::split(&input_stream, false);
assert_eq!(result, expected);
let input_stream = String::from("");
let expected = ("", "");
let result = Content::split(&input_stream, false);
assert_eq!(result, expected);
let input_stream = String::from("\u{feff}\nsecond\nthird");
let expected = ("", "\nsecond\nthird");
let result = Content::split(&input_stream, false);
assert_eq!(result, expected);
let input_stream = String::from("\u{feff}");
let expected = ("", "");
let result = Content::split(&input_stream, false);
assert_eq!(result, expected);
let input_stream = String::from("[📽 2 videos]");
let expected = ("", "[📽 2 videos]");
let result = Content::split(&input_stream, false);
assert_eq!(result, expected);
}
#[test]
fn test_display_for_content() {
let input = Content {
s: "".to_string(),
header: "first",
body: "\nsecond\nthird\n",
_pin: PhantomPinned,
};
let expected = "\u{feff}---\nfirst\n---\n\nsecond\nthird\n".to_string();
assert_eq!(input.to_string(), expected);
let input = Content {
s: "".to_string(),
header: "",
body: "\nsecond\nthird\n",
_pin: PhantomPinned,
};
let expected = "\nsecond\nthird\n".to_string();
assert_eq!(input.to_string(), expected);
let input = Content {
s: "".to_string(),
header: "",
body: "",
_pin: PhantomPinned,
};
let expected = "".to_string();
assert_eq!(input.to_string(), expected);
}
}