use crate::error::FileError;
use self_cell::self_cell;
use std::fmt;
use std::fs::create_dir_all;
use std::fs::OpenOptions;
use std::io::Write;
use std::path::Path;
const BEFORE_HEADER_MAX_IGNORED_CHARS: usize = 1024;
#[derive(Debug, Eq, PartialEq)]
pub struct ContentRef<'a> {
pub header: &'a str,
pub body: &'a str,
}
self_cell!(
pub struct Content {
owner: String,
#[covariant]
dependent: ContentRef,
}
impl {Debug, Eq, PartialEq}
);
impl<'a> Content {
pub fn from(input: String) -> Self {
Content::new(input, |owner: &String| {
let (header, body) = Content::split(owner);
ContentRef { header, body }
})
}
pub fn from_input_with_cr(input: String) -> Self {
let input = Self::remove_cr(input);
Content::from(input)
}
pub fn is_empty(&self) -> bool {
self.borrow_owner().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) -> (&'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 {
let pattern = "\n\n---";
if let Some(start) = content
.chars()
.take(BEFORE_HEADER_MAX_IGNORED_CHARS)
.collect::<String>()
.find(pattern)
.map(|x| x + pattern.len())
{
start
} else {
return ("", content);
}
};
if !content[fm_start..]
.chars()
.next()
.unwrap_or('x')
.is_whitespace()
{
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;
while let Some(c) = content[body_start..].chars().next() {
if c == ' ' || c == '\t' {
body_start += 1;
} else {
if c == '\n' {
body_start += 1;
}
break;
};
}
(content[fm_start..fm_end].trim(), &content[body_start..])
}
pub fn write_to_disk(&self, new_file_path: &Path) -> Result<(), FileError> {
#[allow(clippy::or_fun_call)]
create_dir_all(new_file_path.parent().unwrap_or(Path::new("")))?;
let outfile = OpenOptions::new()
.write(true)
.create_new(true)
.open(&new_file_path);
match outfile {
Ok(mut outfile) => {
log::trace!("Creating file: {:?}", new_file_path);
write!(outfile, "\u{feff}")?;
if !self.borrow_dependent().header.is_empty() {
write!(outfile, "---")?;
#[cfg(target_family = "windows")]
write!(outfile, "\r")?;
writeln!(outfile)?;
for l in self.borrow_dependent().header.lines() {
write!(outfile, "{}", l)?;
#[cfg(target_family = "windows")]
write!(outfile, "\r")?;
writeln!(outfile)?;
}
write!(outfile, "---")?;
#[cfg(target_family = "windows")]
write!(outfile, "\r")?;
writeln!(outfile)?;
};
for l in self.borrow_dependent().body.lines() {
write!(outfile, "{}", l)?;
#[cfg(target_family = "windows")]
write!(outfile, "\r")?;
writeln!(outfile)?;
}
}
Err(e) => {
return Err(FileError::Write {
path: new_file_path.to_path_buf(),
source_str: e.to_string(),
});
}
}
Ok(())
}
}
impl<'a> fmt::Display for ContentRef<'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)
}
}
impl fmt::Display for Content {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.borrow_dependent().fmt(f)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_from_input_with_cr() {
let content = Content::from_input_with_cr("first\r\nsecond\r\nthird".to_string());
assert_eq!(content.borrow_dependent().body, "first\nsecond\nthird");
let content = Content::from_input_with_cr("first\nsecond\nthird".to_string());
assert_eq!(content.borrow_dependent().body, "first\nsecond\nthird");
let content = Content::from_input_with_cr("\u{feff}first\nsecond\nthird".to_string());
assert_eq!(content.borrow_dependent().body, "first\nsecond\nthird");
}
#[test]
fn test_new() {
let content = Content::from("first\nsecond\nthird".to_string());
assert_eq!(content.borrow_dependent().body, "first\nsecond\nthird");
let content = Content::from("\u{feff}first\nsecond\nthird".to_string());
assert_eq!(content.borrow_dependent().body, "first\nsecond\nthird");
let content = Content::from("\u{feff}---\nfirst\n---\nsecond\nthird".to_string());
assert_eq!(content.borrow_dependent().header, "first");
assert_eq!(content.borrow_dependent().body, "second\nthird");
let content = Content::from("\u{feff}---\nfirst\n---".to_string());
assert_eq!(content.borrow_dependent().header, "first");
assert_eq!(content.borrow_dependent().body, "");
let content = Content::from("\u{feff}ignored\n\n---\nfirst\n---".to_string());
assert_eq!(content.borrow_dependent().header, "first");
assert_eq!(content.borrow_dependent().body, "");
let mut s = "\u{feff}".to_string();
s.push_str(&String::from_utf8(vec![b'X'; BEFORE_HEADER_MAX_IGNORED_CHARS]).unwrap());
s.push_str("\n\n---\nfirst\n---\nsecond");
let s_ = s.clone();
let content = Content::from(s);
assert_eq!(content.borrow_dependent().header, "");
assert_eq!(content.borrow_dependent().body, &s_[3..]);
let mut s = "\u{feff}".to_string();
s.push_str(
&String::from_utf8(vec![
b'X';
BEFORE_HEADER_MAX_IGNORED_CHARS - "\n\n---".len()
])
.unwrap(),
);
s.push_str("\n\n---\nfirst\n---\nsecond");
let content = Content::from(s);
assert_eq!(content.borrow_dependent().header, "first");
assert_eq!(content.borrow_dependent().body, "second");
}
#[test]
fn test_split() {
let input_stream = String::from("---first\n---\nsecond\nthird");
let expected = ("", "---first\n---\nsecond\nthird");
let result = Content::split(&input_stream);
assert_eq!(result, expected);
let input_stream = String::from("---\nfirst\n---\nsecond\nthird");
let expected = ("first", "second\nthird");
let result = Content::split(&input_stream);
assert_eq!(result, expected);
let input_stream = String::from("---\tfirst\n---\nsecond\nthird");
let expected = ("first", "second\nthird");
let result = Content::split(&input_stream);
assert_eq!(result, expected);
let input_stream = String::from("--- first\n---\nsecond\nthird");
let expected = ("first", "second\nthird");
let result = Content::split(&input_stream);
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);
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);
assert_eq!(result, expected);
let input_stream = String::from("---\nfirst\n--- \t \n\nsecond\nthird\n");
let expected = ("first", "\nsecond\nthird\n");
let result = Content::split(&input_stream);
assert_eq!(result, expected);
let input_stream = String::from("\nsecond\nthird");
let expected = ("", "\nsecond\nthird");
let result = Content::split(&input_stream);
assert_eq!(result, expected);
let input_stream = String::from("");
let expected = ("", "");
let result = Content::split(&input_stream);
assert_eq!(result, expected);
let input_stream = String::from("\u{feff}\nsecond\nthird");
let expected = ("", "\nsecond\nthird");
let result = Content::split(&input_stream);
assert_eq!(result, expected);
let input_stream = String::from("\u{feff}");
let expected = ("", "");
let result = Content::split(&input_stream);
assert_eq!(result, expected);
let input_stream = String::from("[📽 2 videos]");
let expected = ("", "[📽 2 videos]");
let result = Content::split(&input_stream);
assert_eq!(result, expected);
}
#[test]
fn test_display_for_content() {
let expected = "\u{feff}---\nfirst\n---\n\nsecond\nthird\n".to_string();
let input = Content::from(expected.clone());
assert_eq!(input.to_string(), expected);
let expected = "\nsecond\nthird\n".to_string();
let input = Content::from(expected.clone());
assert_eq!(input.to_string(), expected);
let expected = "".to_string();
let input = Content::from(expected.clone());
assert_eq!(input.to_string(), expected);
let expected = "\u{feff}---\nfirst\n---\n\nsecond\nthird\n".to_string();
let input =
Content::from("\u{feff}ignored\n\n---\nfirst\n---\n\nsecond\nthird\n".to_string());
assert_eq!(input.to_string(), expected);
}
}