mod asciidoc;
mod markdown;
mod org;
use crate::{Error, site::SiteMetadata};
use std::{
fmt,
path::{Component, Path, PathBuf},
sync::Arc,
time::SystemTime,
};
use tracing::info;
#[derive(Hash, Eq, Clone, PartialEq, Debug, PartialOrd, Ord)]
pub struct DocumentName {
pub id: Option<String>,
pub labels: Vec<String>,
pub post: Option<DocumentPostLabel>,
}
impl fmt::Display for DocumentName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.labels.is_empty() && self.post.is_none() {
write!(f, "[index]")?;
return Ok(());
}
write!(f, "{}", self.labels.join("/"))?;
if let Some(post) = self.post.as_ref() {
write!(f, "[post:{},{}]", post.date, post.label)?;
}
Ok(())
}
}
impl DocumentName {
pub fn path(&self) -> PathBuf {
let mut path = self.folder_path();
path.push("index.html");
path
}
pub fn folder_path(&self) -> PathBuf {
let mut path = PathBuf::new();
if let Some(id) = &self.id {
path.push(id);
} else {
for label in &self.labels {
path.push(label);
}
if let Some(post) = self.post.as_ref() {
path.push(post.date.split('-').collect::<Vec<_>>().join("/"));
path.push(&post.label);
}
}
path
}
pub fn is_matched(&self, other: &Path) -> bool {
other == self.path() || other == self.folder_path()
}
pub fn is_ancestor_of(&self, child: &Self) -> bool {
if self.post.is_some() {
return false;
}
if self.labels.len() > child.labels.len() {
return false;
}
if self.labels[0..self.labels.len()] != child.labels[0..self.labels.len()] {
return false;
}
if self.labels.len() == child.labels.len() {
return self.post.is_none() && child.post.is_some();
}
return true;
}
pub fn is_root(&self) -> bool {
self.post.is_none() && self.labels.is_empty()
}
}
#[derive(Hash, Eq, Clone, PartialEq, Debug, PartialOrd, Ord)]
pub struct DocumentPostLabel {
pub date: String,
pub label: String,
}
#[derive(Eq, Clone, PartialEq, Debug)]
pub enum DocumentType {
AsciiDoc,
Markdown,
Org,
}
impl<'a> TryFrom<&'a str> for DocumentType {
type Error = ();
fn try_from(s: &'a str) -> Result<DocumentType, ()> {
match s {
"adoc" => Ok(DocumentType::AsciiDoc),
"md" => Ok(DocumentType::Markdown),
"org" => Ok(DocumentType::Org),
_ => Err(()),
}
}
}
#[derive(Eq, Clone, PartialEq, Debug)]
pub struct DocumentMetadata {
pub typ: DocumentType,
pub modified: SystemTime,
pub source_path: PathBuf,
pub rel_source_path: PathBuf,
}
impl DocumentMetadata {
pub fn new(
site: &SiteMetadata,
file_path: &Path,
typ: DocumentType,
modified: SystemTime,
) -> Result<DocumentMetadata, Error> {
Ok(DocumentMetadata {
source_path: file_path.to_owned(),
rel_source_path: file_path.strip_prefix(&site.source_path)?.to_owned(),
modified,
typ,
})
}
}
fn derive_name(rel_file_path: &Path, id: Option<String>) -> Result<DocumentName, Error> {
let mut labels = Vec::new();
let mut is_post = false;
let mut components = rel_file_path.components().peekable();
while let Some(component) = components.next() {
if let Component::Normal(component_name) = component {
let component_name = component_name
.to_str()
.ok_or(Error::PathContainNonUnicode)?;
if component_name == "_posts" {
is_post = true;
break;
}
if components.peek().is_none() {
break;
} else {
labels.push(component_name.to_owned());
}
} else {
return Err(Error::InvalidPathComponent);
}
}
let file_stem = rel_file_path
.file_stem()
.ok_or(Error::InvalidPathComponent)?
.to_str()
.ok_or(Error::PathContainNonUnicode)?;
let post = if is_post {
let file_parts = file_stem.split('-').collect::<Vec<_>>();
let date_part = file_parts[0..3].join("-");
let label_part = file_parts[3..].join("-");
Some(DocumentPostLabel {
date: date_part,
label: label_part,
})
} else {
if file_stem != "index" {
labels.push(file_stem.to_owned());
}
None
};
Ok(DocumentName { id, labels, post })
}
#[derive(Eq, Clone, PartialEq, Debug)]
pub struct RenderedData {
pub name: DocumentName,
pub title: String,
pub sitemap_title: Option<String>,
pub content: String,
pub toc: Option<String>,
pub description: String,
pub description_content: Option<String>,
pub license: Option<String>,
pub license_code: Option<String>,
pub specs: Vec<Spec>,
}
#[derive(Eq, Clone, PartialEq, Debug)]
pub struct RenderedDocument {
pub site_metadata: Arc<SiteMetadata>,
pub metadata: Arc<DocumentMetadata>,
pub data: Arc<RenderedData>,
}
impl RenderedDocument {
pub fn new(
site: Arc<SiteMetadata>,
document: Arc<DocumentMetadata>,
) -> Result<RenderedDocument, Error> {
let rel_file_path = &document.rel_source_path;
info!("[{}] Rendering document {:?} ...", site.name, rel_file_path);
Ok(match document.typ {
DocumentType::AsciiDoc => {
let output = self::asciidoc::process_asciidoc(&site.source_path, &rel_file_path)?;
let id = None;
let name = derive_name(rel_file_path, id)?;
RenderedDocument {
site_metadata: site,
metadata: document,
data: Arc::new(RenderedData {
name,
title: output.document.title,
sitemap_title: None,
content: output.document.content,
toc: output.document.toc,
description: output.document.description.clone(),
description_content: None,
license: output.document.license,
license_code: output.document.license_code,
specs: output
.specs
.into_iter()
.map(|spec| Spec {
id: spec.id,
description: spec.description,
discuss: spec.discuss,
source_path: Path::new(&spec.source_path).to_owned(),
anchor: spec.anchor,
})
.collect(),
}),
}
}
DocumentType::Markdown => {
let output = self::markdown::process_markdown(&site.source_path, &rel_file_path)?;
let id = output.id;
let name = derive_name(rel_file_path, id)?;
RenderedDocument {
site_metadata: site,
metadata: document,
data: Arc::new(RenderedData {
name,
title: output.title,
sitemap_title: output.sitemap_title,
content: output.content,
toc: Some(output.toc),
description: output.description,
description_content: Some(output.description_content),
license: None,
license_code: None,
specs: Vec::new(),
}),
}
}
DocumentType::Org => {
let output = self::org::process_org(&site.source_path, &rel_file_path)?;
let id = None;
let name = derive_name(rel_file_path, id)?;
RenderedDocument {
site_metadata: site,
metadata: document,
data: Arc::new(RenderedData {
name,
title: output.title,
sitemap_title: None,
content: output.content,
toc: Some(output.toc),
description: output.description,
description_content: Some(output.description_content),
license: None,
license_code: None,
specs: Vec::new(),
}),
}
}
})
}
}
#[derive(Eq, Clone, PartialEq, Debug)]
pub struct Spec {
pub id: String,
pub description: String,
pub discuss: String,
pub source_path: PathBuf,
pub anchor: String,
}