use crate::error::DocumentError;
use crate::frontmatter::{Frontmatter, REQUIRED_FRONTMATTER_KEYS};
use crate::links::{self, Citation, Link};
use crate::yaml::Value;
const FRONTMATTER_DELIM: &str = "---";
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Document {
pub frontmatter: Frontmatter,
pub body: String,
}
impl Document {
pub fn new(frontmatter: Frontmatter, body: impl Into<String>) -> Self {
Document {
frontmatter,
body: body.into(),
}
}
pub fn parse(text: &str) -> Result<Document, DocumentError> {
let lines: Vec<&str> = text.lines().collect();
if lines.is_empty() || lines[0].trim() != FRONTMATTER_DELIM {
return Ok(Document {
frontmatter: Frontmatter::new(),
body: text.to_string(),
});
}
let mut end_idx = None;
for (i, line) in lines.iter().enumerate().skip(1) {
if line.trim() == FRONTMATTER_DELIM {
end_idx = Some(i);
break;
}
}
let end_idx = end_idx.ok_or(DocumentError::UnterminatedFrontmatter)?;
let fm_text = lines[1..end_idx].join("\n");
let value = Value::parse(&fm_text)?;
let frontmatter = match value {
Value::Null => Frontmatter::new(),
Value::Mapping(m) => Frontmatter::from_mapping(m),
_ => return Err(DocumentError::FrontmatterNotMapping),
};
let mut body = lines[end_idx + 1..].join("\n");
if let Some(stripped) = body.strip_prefix('\n') {
body = stripped.to_string();
}
Ok(Document { frontmatter, body })
}
pub fn serialize(&self) -> String {
let fm_text = Value::Mapping(self.frontmatter.as_mapping().clone())
.to_yaml_string()
.trim_end()
.to_string();
let body = if self.body.ends_with('\n') {
self.body.clone()
} else {
format!("{}\n", self.body)
};
format!("{FRONTMATTER_DELIM}\n{fm_text}\n{FRONTMATTER_DELIM}\n\n{body}")
}
pub fn validate(&self) -> Result<(), DocumentError> {
let missing: Vec<String> = REQUIRED_FRONTMATTER_KEYS
.iter()
.filter(|k| {
self.frontmatter
.get(k)
.map(Value::is_empty_value)
.unwrap_or(true)
})
.map(|k| k.to_string())
.collect();
if missing.is_empty() {
Ok(())
} else {
Err(DocumentError::MissingKeys(missing))
}
}
pub fn validate_conformance(&self) -> Result<(), DocumentError> {
let has_type = self
.frontmatter
.get("type")
.map(|v| !v.is_empty_value())
.unwrap_or(false);
if has_type {
Ok(())
} else {
Err(DocumentError::MissingKeys(vec!["type".to_string()]))
}
}
pub fn links(&self) -> Vec<Link> {
links::extract_links(&self.body)
}
pub fn citations(&self) -> Vec<Citation> {
links::extract_citations(&self.body)
}
}