use crate::render::render;
use crate::site::{default_true, Site};
use crate::sources::{Parser, Source, Sources};
use crate::taxonomies::{Classification, Taxonomies};
use crate::types::{Ancestors, Any, DateTime, HashMap};
use beef::lean::Cow;
use ramhorns::{
encoding::Encoder, traits::ContentSequence, Content, Error, Ramhorns, Section, Template,
};
use serde::{Deserialize, Serialize};
use std::cmp::{min, Ordering, Reverse};
use std::fs::create_dir_all;
use std::io;
use std::num::NonZeroUsize;
use std::ops::Range;
use std::ops::{Deref, DerefMut};
use std::path::{is_separator, Path, PathBuf};
#[derive(Content, Default, Deserialize, Serialize)]
pub struct Page<'p> {
#[serde(borrow, default, skip_serializing_if = "str::is_empty")]
pub title: Cow<'p, str>,
#[serde(borrow, default, skip_serializing_if = "str::is_empty")]
pub slug: Cow<'p, str>,
#[serde(borrow, default, skip_serializing_if = "is_ancestors_empty")]
pub path: Ancestors<'p>,
#[serde(default, skip_serializing_if = "is_slice_empty")]
pub alternative_paths: Box<[&'p str]>,
#[serde(default, skip_serializing_if = "equal_zero")]
#[ramhorns(skip)]
pub weight: i64,
#[serde(borrow, default, skip_serializing_if = "str::is_empty")]
#[ramhorns(skip)]
pub template: Cow<'p, str>,
#[serde(borrow, default = "def_page", skip_serializing_if = "eq_def_page")]
#[ramhorns(skip)]
pub page_template: Cow<'p, str>,
#[serde(borrow, default = "def_section", skip_serializing_if = "eq_def_sect")]
#[ramhorns(skip)]
pub section_template: Cow<'p, str>,
#[serde(borrow, default = "def_gallery", skip_serializing_if = "eq_def_gall")]
#[ramhorns(skip)]
pub gallery_template: Cow<'p, str>,
#[serde(skip_serializing_if = "Option::is_none")]
#[ramhorns(skip)]
pub paginate_by: Option<NonZeroUsize>,
#[serde(borrow, default, skip_serializing_if = "str::is_empty")]
pub image: Cow<'p, str>,
#[serde(borrow, default, skip_serializing_if = "str::is_empty")]
pub summary: Cow<'p, str>,
#[serde(borrow, default, skip_serializing_if = "str::is_empty")]
#[ramhorns(callback = render_content)]
pub content: Cow<'p, str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub date: Option<DateTime>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
#[ramhorns(skip)]
pub sort_by_weight: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub is_section: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub hidden: bool,
#[serde(skip, default = "default_range")]
#[ramhorns(skip)]
pages: Range<usize>,
#[serde(skip, default = "default_range")]
#[ramhorns(skip)]
subsections: Range<usize>,
#[serde(skip)]
#[ramhorns(skip)]
parent: usize,
#[serde(skip)]
#[ramhorns(skip)]
previous: usize,
#[serde(skip)]
#[ramhorns(skip)]
next: usize,
#[serde(skip, default = "default_true")]
#[ramhorns(skip)]
nonstandard_path: bool,
#[serde(skip, default = "default_priority")]
pub priority: f32,
#[serde(default, skip_serializing_if = "is_slice_empty")]
#[ramhorns(skip)]
pub pictures: Box<[Picture<'p>]>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub taxonomies: Taxonomies<'p>,
#[serde(default, skip_serializing_if = "is_slice_empty")]
#[ramhorns(skip)]
pub plugins: Box<[&'p str]>,
#[serde(skip)]
#[ramhorns(skip)]
id: usize,
#[serde(flatten)]
#[ramhorns(flatten)]
pub extra: HashMap<&'p str, Any<'p>>,
}
#[derive(Serialize)]
#[serde(transparent)]
pub struct Pages<'p>(Box<[Page<'p>]>);
#[derive(Clone, Content, Deserialize, Serialize)]
pub struct Picture<'p> {
#[serde(borrow, default)]
pub alt: Cow<'p, str>,
#[serde(borrow, default)]
pub caption: Cow<'p, str>,
#[serde(borrow)]
pub file: Cow<'p, str>,
#[serde(borrow)]
pub pid: Cow<'p, str>,
pub taken: Option<DateTime>,
}
#[derive(Clone, Copy)]
pub struct Context<'p, 'r>(
pub &'r Pages<'p>,
pub &'r Site<'p>,
pub &'r Classification<'p, 'r>,
pub &'r Ramhorns,
pub &'r Path,
);
#[derive(Clone, Content)]
pub(crate) struct PageRef<'p, 'r> {
pages: PageList<'p, 'r>,
subsections: PageList<'p, 'r>,
pictures: Pictures<'p, 'r>,
permalink: Permalink<'p, 'r>,
active: bool,
#[ramhorns(flatten)]
page: &'r Page<'p>,
}
#[derive(Clone, Content)]
struct PageContext<'p, 'r> {
pages: PageList<'p, 'r>,
subsections: PageList<'p, 'r>,
previous: Option<PageRef<'p, 'r>>,
next: Option<PageRef<'p, 'r>>,
parent: PageRef<'p, 'r>,
pictures: Pictures<'p, 'r>,
index: PageRef<'p, 'r>,
pagination: Option<Pagination>,
permalink: Permalink<'p, 'r>,
site: &'r Site<'p>,
classification: &'r Classification<'p, 'r>,
active: Active,
#[ramhorns(flatten)]
page: &'r Page<'p>,
}
#[derive(Clone)]
pub(crate) struct PageList<'p, 'r> {
all: &'r [Page<'p>],
range: Range<usize>,
active: usize,
site_url: &'r str,
}
#[derive(Clone, Content, Copy)]
pub(crate) struct Pagination {
previous: Option<usize>,
next: Option<usize>,
current: usize,
length: usize,
}
#[derive(Content)]
struct PictureRef<'p, 'r> {
#[ramhorns(flatten)]
picture: Picture<'r>,
permalink: PicturePermalink<'p, 'r>,
}
#[derive(Content)]
struct PictureView<'p, 'r> {
current: PictureRef<'p, 'r>,
previous: PictureRef<'p, 'r>,
next: PictureRef<'p, 'r>,
parent: PageRef<'p, 'r>,
index: PageRef<'p, 'r>,
site: &'r Site<'p>,
classification: &'r Classification<'p, 'r>,
}
#[derive(Clone)]
struct Pictures<'p, 'r>(&'r [Picture<'p>], &'r Page<'p>, &'r str);
#[derive(Clone)]
struct Active;
impl Content for Active {}
#[derive(Clone)]
pub struct Permalink<'p, 'r>(pub(crate) &'r Page<'p>, pub(crate) &'r str);
struct PicturePermalink<'p, 'r>(&'r Page<'p>, &'r str, &'r str);
pub(crate) trait Paginate: Content + Sized {
fn paginate(&self, pages: Range<usize>, length: usize, current: usize) -> Self;
fn render_paginated(
&self,
range: Range<usize>,
by: usize,
path: &mut PathBuf,
tpl: &Template,
rendered: &mut HashMap<PathBuf, u32>,
buffer: &mut Vec<u8>,
) -> Result<(), io::Error> {
let (mut first, last) = (range.start, range.end);
let count = last - first;
let by = min(by, count);
let len = count / by + ((count % by != 0) as usize);
render(
tpl,
&path,
&self.paginate(first..(first + by), len, 1),
rendered,
buffer,
)?;
for i in 0..len {
path.pop();
path.push((i + 1).to_string());
path.set_extension("html");
let end = min(first + by, last);
render(
tpl,
&path,
&self.paginate(first..end, len, i + 1),
rendered,
buffer,
)?;
first = end;
}
Ok(())
}
}
impl<'p> Page<'p> {
#[inline]
pub fn new<P: Parser>(
source: &'p Source<P>,
data: &'p Sources<P>,
) -> Result<Self, (P::Error, Box<str>)> {
let path = std::str::from_utf8(&data.data[source.path.clone()]).unwrap();
let mut page = source
.format
.parse(&data.data[source.source.clone()])
.map_err(|e| (e, path.into()))?;
let is_section = source.is_section;
page.is_section = is_section;
page.pages = source.pages.clone();
page.subsections = source.subsections.clone();
page.parent = source.parent;
page.date = page.date.or_else(|| source.date.map(|d| d.into()));
if is_section || page.slug.is_empty() || page.slug.contains(is_separator) {
let slug = path.rsplit(is_separator).next().unwrap_or_default();
page.slug = Cow::const_str(slug);
}
let page_path = page.path.as_ref();
if is_section || page_path.is_empty() || Path::new(page_path).is_absolute() {
let path = &path[0..path.rfind(is_separator).unwrap_or_default()];
page.path = Cow::const_str(path).into();
page.nonstandard_path = false;
} else if page_path == "." {
page.path = Cow::const_str("").into();
}
Ok(page)
}
#[inline]
pub(crate) fn by_ref<'r>(&'r self, all: &'r [Self], i: usize, url: &'r str) -> PageRef<'p, 'r> {
PageRef {
pages: PageList::new(all, self.pages.clone(), i, url),
subsections: PageList::new(all, self.subsections.clone(), i, url),
pictures: Pictures(&self.pictures, self, url),
page: self,
permalink: Permalink(self, url),
active: self.id == i,
}
}
#[inline]
fn in_context<'r>(
&'r self,
all: &'r [Self],
site: &'r Site<'p>,
classification: &'r Classification<'p, 'r>,
) -> PageContext<'p, 'r> {
PageContext {
pages: PageList::new(all, self.pages.clone(), self.id, &site.url),
subsections: PageList::new(all, self.subsections.clone(), self.id, &site.url),
previous: Some(self.previous)
.filter(|&i| i != 0)
.map(|i| all[i].by_ref(all, self.id, &site.url)),
next: Some(self.next)
.filter(|&i| i != 0)
.map(|i| all[i].by_ref(all, self.id, &site.url)),
parent: all[self.parent].by_ref(all, self.id, &site.url),
pictures: Pictures(&self.pictures, self, &site.url),
permalink: Permalink(self, &site.url),
index: all[0].by_ref(all, self.id, &site.url),
pagination: None,
classification,
site,
active: Active,
page: self,
}
}
pub fn create_directory<P: AsRef<Path>>(&self, output_dir: P) -> Result<(), io::Error> {
let output_dir = output_dir.as_ref();
for path in self.alternative_paths.iter() {
let path = output_dir.join(path);
create_dir_all(path)?;
}
if self.is_section || !self.pictures.is_empty() {
let mut path = output_dir.join(self.path.as_ref());
path.push(self.slug.as_ref());
create_dir_all(path)
} else if self.nonstandard_path {
let path = output_dir.join(self.path.as_ref());
create_dir_all(path)
} else {
Ok(())
}
}
#[inline]
pub fn render(
&self,
Context(all, site, classification, templates, output_dir): Context<'p, '_>,
rendered: &mut HashMap<PathBuf, u32>,
buffer: &mut Vec<u8>,
) -> Result<(), Error> {
let mut output = output_dir.join(self.path.as_ref());
output.push(self.slug.as_ref());
if self.is_section {
output.push("index");
}
output.set_extension("html");
let template = if self.template.is_empty() {
if self.is_section {
&all[self.parent].section_template
} else {
&all[self.parent].page_template
}
} else {
&self.template
};
let template = templates
.get(template)
.ok_or_else(|| Error::NotFound(template.as_ref().into()))?;
let page = self.in_context(all, site, classification);
let by = self.paginate_by.map(NonZeroUsize::get).unwrap_or(0);
if by > 0 && self.pages.len() > by {
let (start, end) = (self.pages.start, self.pages.end);
page.render_paginated(start..end, by, &mut output, template, rendered, buffer)?
} else if !self.pictures.is_empty() {
render(template, &output, &page, rendered, buffer)?;
if self.is_section {
output.pop();
} else {
output.set_extension("");
};
let template = templates
.get(&self.gallery_template)
.ok_or_else(|| Error::NotFound(self.gallery_template.as_ref().into()))?;
let pictures = &self.pictures;
let last = pictures.len() - 1;
for i in 0..=last {
let page = PictureView {
current: pictures[i].by_ref(self, &site.url),
previous: pictures[if i == 0 { last } else { i - 1 }].by_ref(self, &site.url),
next: pictures[if i == last { 0 } else { i + 1 }].by_ref(self, &site.url),
parent: self.by_ref(all, self.id, &site.url),
index: all[0].by_ref(all, self.id, &site.url),
site,
classification,
};
output.push(pictures[i].pid.as_ref());
output.set_extension("html");
render(template, &output, &page, rendered, buffer)?;
output.pop();
}
} else {
render(template, output, &page, rendered, buffer)?;
}
for path in self.alternative_paths.iter() {
let mut output = output_dir.join(path);
output.push("index.html");
render(template, output, &page, rendered, buffer)?;
}
Ok(())
}
}
impl<'p> Pages<'p> {
#[inline]
pub fn from_sources(mut pages: Vec<Page<'p>>) -> Self {
for i in 0..pages.len() {
let page = &pages[i];
let subpages = page.pages.clone();
let subsects = page.subsections.clone();
if page.sort_by_weight {
pages[subpages.clone()].sort_unstable_by_key(|p| p.weight);
pages[subsects.clone()].sort_unstable_by_key(|p| p.weight);
} else {
pages[subpages.clone()].sort_unstable_by_key(|p| Reverse(p.date));
pages[subsects.clone()].sort_unstable_by_key(|p| Reverse(p.date));
}
for i in subpages.clone().skip(1) {
pages[i].previous = i - 1;
}
for i in subpages.clone().take_while(|i| *i != subpages.end - 1) {
pages[i].next = i + 1;
}
for i in subsects.clone().skip(1) {
pages[i].previous = i - 1;
}
for i in subsects.clone().take_while(|i| *i != subsects.end - 1) {
pages[i].next = i + 1;
}
pages[i].id = i;
}
Pages(pages.into())
}
#[inline]
#[allow(clippy::needless_range_loop)]
pub fn from_external(mut pages: Vec<Page<'p>>) -> Self {
#[inline]
fn is_subpage(path: &str, section_path: &str, section_slug: &str) -> bool {
path.strip_suffix(is_separator)
.unwrap_or(path)
.strip_suffix(section_slug)
.and_then(|p| p.strip_prefix(section_path))
.filter(|s| s.chars().all(is_separator))
.is_some()
}
for i in 0..pages.len() {
if !pages[i].is_section {
continue;
}
let (mut found, mut subpage_found) = (false, false);
let (mut start, mut end) = (0, 0);
let (mut pstart, mut pend) = (0, 0);
for j in i + 1..pages.len() {
if !is_subpage(&pages[j].path.0, &pages[i].path.0, &pages[i].slug) {
if !found {
continue;
} else {
if subpage_found {
pend = j;
} else {
end = j;
}
break;
}
}
if !found {
found = true;
if !pages[j].is_section {
subpage_found = true;
pstart = j;
} else {
start = j;
}
} else if !subpage_found && !pages[j].is_section {
subpage_found = true;
pstart = j;
end = j;
}
pages[j].parent = i;
}
if end == 0 && start != 0 {
end = pages.len()
} else if pend == 0 && pstart != 0 {
pend = pages.len()
}
pages[i].subsections = start..end;
pages[i].pages = pstart..pend;
if pages[i].sort_by_weight {
pages[start..end].sort_unstable_by_key(|p| p.weight);
pages[pstart..pend].sort_unstable_by_key(|p| p.weight);
}
if end != 0 {
for j in start + 1..end {
pages[j].previous = j - 1;
}
for j in start..end - 1 {
pages[j].next = j + 1;
}
}
if pend != 0 {
for j in pstart + 1..pend {
pages[j].previous = j - 1;
}
for j in pstart..pend - 1 {
pages[j].next = j + 1;
}
}
pages[i].id = i;
}
Pages(pages.into())
}
}
impl<'p> Ord for Page<'p> {
fn cmp(&self, other: &Self) -> Ordering {
let mut self_paths = self.path.0.split(is_separator);
let mut other_paths = other.path.0.split(is_separator);
loop {
match (self_paths.next(), other_paths.next()) {
(Some(s1), Some(s2)) => match s1.cmp(s2) {
Ordering::Less => return Ordering::Less,
Ordering::Greater => return Ordering::Greater,
_ => continue,
},
(None, Some(_)) => return Ordering::Less,
(Some(_), None) => return Ordering::Greater,
(None, None) => break,
}
}
match (self.is_section, other.is_section) {
(true, true) => self.slug.cmp(&other.slug),
(true, false) => Ordering::Less,
(false, true) => Ordering::Greater,
(false, false) => Reverse(self.date).cmp(&Reverse(other.date)),
}
}
}
impl<'p> PartialOrd for Page<'p> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl<'p> PartialEq for Page<'p> {
fn eq(&self, other: &Self) -> bool {
self.path.0 == other.path.0 && self.slug == other.slug && self.id == other.id
}
}
impl<'p> Eq for Page<'p> {}
impl<'p, 'r> Paginate for PageContext<'p, 'r> {
#[inline]
fn paginate(&self, pages: Range<usize>, length: usize, current: usize) -> Self {
let old = &self.pages;
Self {
pages: PageList::new(old.all, pages, old.active, old.site_url),
pagination: Some(Pagination::new(length, current)),
..self.clone()
}
}
}
impl Pagination {
#[inline]
pub(crate) fn new(length: usize, current: usize) -> Self {
Self {
length,
current,
previous: Some(current - 1).filter(|&i| i > 0),
next: Some(current + 1).filter(|&i| i <= length),
}
}
}
impl<'p> Picture<'p> {
fn by_ref<'r>(&'r self, page: &'r Page<'p>, site_url: &'r str) -> PictureRef<'p, 'r> {
PictureRef {
picture: Picture {
alt: self.alt.as_ref().into(),
caption: self.caption.as_ref().into(),
pid: self.pid.as_ref().into(),
file: self.file.as_ref().into(),
taken: self.taken,
},
permalink: PicturePermalink(page, site_url, &self.pid),
}
}
}
impl<'p, 'r> PageList<'p, 'r> {
pub(crate) fn new(all: &'r [Page<'p>], range: Range<usize>, id: usize, url: &'r str) -> Self {
Self {
all,
range,
active: id,
site_url: url,
}
}
}
#[inline]
fn render_content<E: Encoder>(source: &str, encoder: &mut E) -> Result<(), E::Error> {
let parser = pulldown_cmark::Parser::new_ext(source, pulldown_cmark::Options::all());
let processed = cmark_syntax::SyntaxPreprocessor::new(parser);
encoder.write_html(processed)
}
impl<'p, 'r> Content for PageList<'p, 'r> {
#[inline]
fn is_truthy(&self) -> bool {
!self.range.is_empty()
}
#[inline]
fn render_section<C, E>(&self, section: Section<C>, encoder: &mut E) -> Result<(), E::Error>
where
C: ContentSequence,
E: Encoder,
{
let range = self.range.clone();
for page in self.all[range].iter().filter(|p| !p.hidden) {
page.by_ref(self.all, self.active, self.site_url)
.render_section(section, encoder)?;
}
Ok(())
}
}
impl<'p, 'r> Content for Permalink<'p, 'r> {
#[inline]
fn is_truthy(&self) -> bool {
true
}
#[inline]
fn render_escaped<E: Encoder>(&self, encoder: &mut E) -> Result<(), E::Error> {
encoder.write_escaped(self.1)?;
self.0.path.render_escaped(encoder)?;
if !self.0.slug.is_empty() {
encoder.write_unescaped("/")?;
self.0.slug.render_escaped(encoder)?;
}
if self.0.is_section {
encoder.write_unescaped("/")
} else {
encoder.write_unescaped(".html")
}
}
#[inline]
fn render_unescaped<E: Encoder>(&self, encoder: &mut E) -> Result<(), E::Error> {
encoder.write_unescaped(self.1)?;
self.0.path.render_unescaped(encoder)?;
if !self.0.slug.is_empty() {
encoder.write_unescaped("/")?;
self.0.slug.render_unescaped(encoder)?;
}
if self.0.is_section {
encoder.write_unescaped("/")
} else {
encoder.write_unescaped(".html")
}
}
}
impl<'p, 'r> Content for Pictures<'p, 'r> {
#[inline]
fn is_truthy(&self) -> bool {
!self.0.is_empty()
}
#[inline]
fn render_section<C, E>(&self, section: Section<C>, encoder: &mut E) -> Result<(), E::Error>
where
C: ContentSequence,
E: Encoder,
{
for picture in self.0 {
picture
.by_ref(self.1, self.2)
.render_section(section, encoder)?;
}
Ok(())
}
}
impl<'p, 'r> Content for PicturePermalink<'p, 'r> {
#[inline]
fn is_truthy(&self) -> bool {
true
}
#[inline]
fn render_escaped<E: Encoder>(&self, encoder: &mut E) -> Result<(), E::Error> {
encoder.write_escaped(self.1)?;
self.0.path.render_escaped(encoder)?;
if !self.0.slug.is_empty() {
encoder.write_unescaped("/")?;
self.0.slug.render_escaped(encoder)?;
}
encoder.write_unescaped("/")?;
encoder.write_escaped(self.2)?;
encoder.write_unescaped(".html")
}
#[inline]
fn render_unescaped<E: Encoder>(&self, encoder: &mut E) -> Result<(), E::Error> {
encoder.write_unescaped(self.1)?;
self.0.path.render_unescaped(encoder)?;
if !self.0.slug.is_empty() {
encoder.write_unescaped("/")?;
self.0.slug.render_unescaped(encoder)?;
}
encoder.write_unescaped("/")?;
encoder.write_unescaped(self.2)?;
encoder.write_unescaped(".html")
}
}
impl<'p> Deref for Pages<'p> {
type Target = [Page<'p>];
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'p> DerefMut for Pages<'p> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
#[inline]
const fn default_priority() -> f32 {
0.5
}
#[inline]
const fn default_range() -> Range<usize> {
0..0
}
#[inline]
const fn def_gallery() -> Cow<'static, str> {
Cow::const_str("gallery.html")
}
#[inline]
const fn def_page() -> Cow<'static, str> {
Cow::const_str("page.html")
}
#[inline]
const fn def_section() -> Cow<'static, str> {
Cow::const_str("section.html")
}
#[inline]
fn eq_def_gall(c: &str) -> bool {
c == "gallery.html"
}
#[inline]
fn eq_def_page(c: &str) -> bool {
c == "page.html"
}
#[inline]
fn eq_def_sect(c: &str) -> bool {
c == "section.html"
}
#[inline]
const fn equal_zero(i: &i64) -> bool {
*i == 0
}
#[inline]
fn is_slice_empty<T>(s: &[T]) -> bool {
s.is_empty()
}
#[inline]
fn is_ancestors_empty(s: &Ancestors) -> bool {
s.0.is_empty()
}