use self_cell::self_cell;
use std::fmt;
use std::fmt::Debug;
use std::fs::File;
use std::fs::OpenOptions;
use std::fs::create_dir_all;
use std::io::Write;
use std::path::Path;
use substring::Substring;
use crate::config::TMPL_VAR_DOC;
use crate::error::InputStreamError;
use crate::text_reader::read_as_string_with_crlf_suppression;
const BEFORE_HEADER_MAX_IGNORED_CHARS: usize = 1024;
pub trait Content: AsRef<str> + Debug + Eq + PartialEq + Default {
fn open(path: &Path) -> Result<Self, std::io::Error>
where
Self: Sized,
{
Ok(Self::from_string(
read_as_string_with_crlf_suppression(File::open(path)?)?,
TMPL_VAR_DOC.to_string(),
))
}
fn from_string(input: String, name: String) -> Self;
fn header(&self) -> &str;
fn body(&self) -> &str;
fn name(&self) -> &str;
fn from_html(input: String, name: String) -> Result<Self, InputStreamError> {
use crate::html::HtmlString;
let input = input.prepend_html_start_tag()?;
Ok(Self::from_string(input, name))
}
fn save_as(&self, new_file_path: &Path) -> Result<(), std::io::Error> {
create_dir_all(new_file_path.parent().unwrap_or_else(|| Path::new("")))?;
let mut outfile = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(new_file_path)?;
log::trace!("Creating file: {:?}", new_file_path);
write!(outfile, "\u{feff}")?;
for l in self.as_str().lines() {
write!(outfile, "{}", l)?;
#[cfg(target_family = "windows")]
write!(outfile, "\r")?;
writeln!(outfile)?;
}
Ok(())
}
fn as_str(&self) -> &str {
self.as_ref().trim_start_matches('\u{feff}')
}
fn is_empty(&self) -> bool {
use crate::html::HtmlStr;
self.header().is_empty() && (self.body().is_empty_html())
}
fn split(content: &str) -> (&str, &str) {
use crate::html::HtmlStr;
let content = content.trim_start_matches('\u{feff}');
if content.is_empty() {
return ("", "");
};
if content.has_html_start_tag() {
return ("", content);
}
const HEADER_START_TAG: &str = "---";
let fm_start = if content.starts_with(HEADER_START_TAG) {
HEADER_START_TAG.len()
} else {
const HEADER_START_TAG: &str = "\n\n---";
if let Some(start) = content
.substring(0, BEFORE_HEADER_MAX_IGNORED_CHARS)
.find(HEADER_START_TAG)
.map(|x| x + HEADER_START_TAG.len())
{
start
} else {
return ("", content);
}
};
if !content[fm_start..]
.chars()
.next()
.unwrap_or('x')
.is_whitespace()
{
return ("", content);
};
const HEADER_END_TAG1: &str = "\n---";
const HEADER_END_TAG2: &str = "\n...";
debug_assert_eq!(HEADER_END_TAG1.len(), HEADER_END_TAG2.len());
const TAG_LEN: usize = HEADER_END_TAG1.len();
let fm_end = content[fm_start..]
.find(HEADER_END_TAG1)
.or_else(|| content[fm_start..].find(HEADER_END_TAG2))
.map(|x| x + fm_start);
let fm_end = if let Some(n) = fm_end {
n
} else {
return ("", content);
};
let mut body_start = fm_end + TAG_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..])
}
}
#[derive(Debug, Eq, PartialEq)]
pub struct ContentRef<'a> {
pub header: &'a str,
pub body: &'a str,
pub name: String,
}
self_cell!(
pub struct ContentString {
owner: String,
#[covariant]
dependent: ContentRef,
}
impl {Debug, Eq, PartialEq}
);
impl Content for ContentString {
fn from_string(input: String, name: String) -> Self {
ContentString::new(input, |owner: &String| {
let (header, body) = ContentString::split(owner);
ContentRef { header, body, name }
})
}
fn header(&self) -> &str {
self.borrow_dependent().header
}
fn body(&self) -> &str {
self.borrow_dependent().body
}
fn name(&self) -> &str {
&self.borrow_dependent().name
}
}
impl Default for ContentString {
fn default() -> Self {
Self::from_string(String::new(), String::new())
}
}
impl AsRef<str> for ContentString {
fn as_ref(&self) -> &str {
self.borrow_owner()
}
}
impl fmt::Display for ContentRef<'_> {
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 ContentString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
std::fmt::Display::fmt(&self.borrow_dependent(), f)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_from_string() {
let content =
ContentString::from_string("first\nsecond\nthird".to_string(), "doc".to_string());
assert_eq!(content.borrow_dependent().body, "first\nsecond\nthird");
assert_eq!(content.borrow_dependent().name, "doc");
let content = ContentString::from_string(
"\u{feff}first\nsecond\nthird".to_string(),
"doc".to_string(),
);
assert_eq!(content.borrow_dependent().body, "first\nsecond\nthird");
assert_eq!(content.borrow_dependent().name, "doc");
let content = ContentString::from_string(
"\u{feff}---\nfirst\n---\nsecond\nthird".to_string(),
"doc".to_string(),
);
assert_eq!(content.borrow_dependent().header, "first");
assert_eq!(content.borrow_dependent().body, "second\nthird");
assert_eq!(content.borrow_dependent().name, "doc");
let content =
ContentString::from_string("\u{feff}---\nfirst\n---".to_string(), "doc".to_string());
assert_eq!(content.borrow_dependent().header, "first");
assert_eq!(content.borrow_dependent().body, "");
let content = ContentString::from_string(
"\u{feff}ignored\n\n---\nfirst\n---".to_string(),
"doc".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 = ContentString::from_string(s, "doc".to_string());
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 = ContentString::from_string(s, "doc".to_string());
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 = ContentString::split(&input_stream);
assert_eq!(result, expected);
let input_stream = String::from("---\nfirst\n---\nsecond\nthird");
let expected = ("first", "second\nthird");
let result = ContentString::split(&input_stream);
assert_eq!(result, expected);
let input_stream = String::from("---\tfirst\n---\nsecond\nthird");
let expected = ("first", "second\nthird");
let result = ContentString::split(&input_stream);
assert_eq!(result, expected);
let input_stream = String::from("--- first\n---\nsecond\nthird");
let expected = ("first", "second\nthird");
let result = ContentString::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 = ContentString::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 = ContentString::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 = ContentString::split(&input_stream);
assert_eq!(result, expected);
let input_stream = String::from("\nsecond\nthird");
let expected = ("", "\nsecond\nthird");
let result = ContentString::split(&input_stream);
assert_eq!(result, expected);
let input_stream = String::from("");
let expected = ("", "");
let result = ContentString::split(&input_stream);
assert_eq!(result, expected);
let input_stream = String::from("\u{feff}\nsecond\nthird");
let expected = ("", "\nsecond\nthird");
let result = ContentString::split(&input_stream);
assert_eq!(result, expected);
let input_stream = String::from("\u{feff}");
let expected = ("", "");
let result = ContentString::split(&input_stream);
assert_eq!(result, expected);
let input_stream = String::from("[📽 2 videos]");
let expected = ("", "[📽 2 videos]");
let result = ContentString::split(&input_stream);
assert_eq!(result, expected);
let input_stream = "my prelude\n\n---\nmy header\n--- \nmy body\n";
let expected = ("my header", "my body\n");
let result = ContentString::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 = ContentString::from_string(expected.clone(), "does not matter".to_string());
assert_eq!(input.to_string(), expected);
let expected = "\nsecond\nthird\n".to_string();
let input = ContentString::from_string(expected.clone(), "does not matter".to_string());
assert_eq!(input.to_string(), expected);
let expected = "".to_string();
let input = ContentString::from_string(expected.clone(), "does not matter".to_string());
assert_eq!(input.to_string(), expected);
let expected = "\u{feff}---\nfirst\n---\n\nsecond\nthird\n".to_string();
let input = ContentString::from_string(
"\u{feff}ignored\n\n---\nfirst\n---\n\nsecond\nthird\n".to_string(),
"does not matter".to_string(),
);
assert_eq!(input.to_string(), expected);
}
}