diff --git a/src/generate.rs b/src/generate.rs new file mode 100644 index 0000000..4079b8f --- /dev/null +++ b/src/generate.rs @@ -0,0 +1,103 @@ +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::str::FromStr; + +use crate::html::render_html; +use crate::metadata::PageMetadata; +use crate::template; +use anyhow::{bail, Context}; + +pub fn copy_recursively(from: &Path, to: &Path) -> anyhow::Result<()> { + Command::new("cp").arg("-r").arg(from).arg(to).output()?; + + Ok(()) +} + +#[derive(Debug, Default)] +pub struct Pages<'a> { + pages: Vec<PageMetadata<'a>>, + assets: Vec<PathBuf>, +} + +impl<'a> Pages<'a> { + // {{{ Collect simple pages + pub fn add_page(&mut self, path: &Path) -> anyhow::Result<()> { + let content_path = PathBuf::from_str("content")?.join(path); + let source = std::fs::read_to_string(&content_path).unwrap(); + + // We leak all the file contents, which is fine, as we expect them to + // live on for the rest of the duration of the program. + // + // I'm doing this because a lot of places want to reference this, + // which makes memory management a bit nasty (I need to be able to + // return the metadata out of this function, but that's not really allowed + // if it's referencing a "local variable" like the initial string) + let source = Box::leak(Box::new(source)); + + let events = jotdown::Parser::new(source); + let metadata = PageMetadata::new(content_path, source, events)?; + + self.pages.push(metadata); + + Ok(()) + } + // }}} + // {{{ Collect directory of pages + pub fn add_dir(&mut self, path: &Path) -> anyhow::Result<()> { + let content_path = PathBuf::from_str("content")?.join(path); + + for file in std::fs::read_dir(&content_path)? { + let file_path = file?.path(); + let filename = file_path.file_name().unwrap(); + let path = path.join(filename); + if file_path.is_dir() { + self.add_dir(&path)?; + } else if file_path.extension().map_or(false, |ext| ext == "dj") { + self.add_page(&path)?; + } else { + self.assets.push(path); + } + } + + Ok(()) + } + // }}} + // {{{ Generate + pub fn generate(&self, out_root: PathBuf) -> anyhow::Result<()> { + for page in &self.pages { + let out_dir = out_root.join(page.route.to_path()); + std::fs::create_dir_all(&out_dir) + .with_context(|| format!("Failed to generate {out_dir:?} directory"))?; + + let mut out = String::new(); + let mut page_renderer = template!("templates/page.html", &mut out)?; + + page_renderer.feed(&mut out, |label, out| { + match label { + "content" => { + let events = jotdown::Parser::new(page.source); + render_html(page, &self.pages, events, out)?; + } + _ => bail!("Unknown label {label} in page template"), + } + + Ok(true) + })?; + + std::fs::write(out_dir.join("index.html"), out) + .with_context(|| format!("Failed to write {out_dir:?} post"))?; + } + + let content_root = PathBuf::from_str("content")?; + for path in &self.assets { + std::fs::create_dir_all(out_root.join(path).parent().unwrap()) + .with_context(|| format!("Failed to create parent dir for asset at {path:?}"))?; + + std::fs::copy(content_root.join(path), out_root.join(path)) + .with_context(|| format!("Failed to copy asset at {path:?}"))?; + } + + Ok(()) + } + // }}} +} diff --git a/src/html.rs b/src/html.rs index b2ae3b9..140885d 100644 --- a/src/html.rs +++ b/src/html.rs @@ -31,7 +31,7 @@ use crate::template::TemplateRenderer; /// Render djot content as HTML. pub fn render_html<'s>( metadata: &'s PageMetadata, - pages: Option<&'s [PageMetadata]>, + pages: &'s [PageMetadata], mut events: impl Iterator<Item = Event<'s>>, out: &mut impl std::fmt::Write, ) -> anyhow::Result<()> { @@ -48,7 +48,7 @@ pub struct Writer<'s> { states: Vec<State<'s>>, footnotes: Footnotes<'s>, metadata: Option<&'s PageMetadata<'s>>, - pages: Option<&'s [PageMetadata<'s>]>, + pages: &'s [PageMetadata<'s>], } #[derive(Debug, Clone)] @@ -66,7 +66,7 @@ enum State<'s> { } impl<'s> Writer<'s> { - pub fn new(metadata: Option<&'s PageMetadata>, pages: Option<&'s [PageMetadata]>) -> Self { + pub fn new(metadata: Option<&'s PageMetadata>, pages: &'s [PageMetadata]) -> Self { Self { list_tightness: Vec::new(), states: Vec::new(), @@ -278,7 +278,7 @@ impl<'s> Writer<'s> { // {{{ Post list Container::Div { class: "posts" } => { write!(out, r#"<ol class="article-list">"#)?; - for post in self.pages.ok_or_else(|| anyhow!("No post list given"))? { + for post in self.pages { // Skip drafts if post.config.created_at.is_none() { continue; diff --git a/src/main.rs b/src/main.rs index eef1f95..4903629 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,125 +1,13 @@ -use std::fmt::Write; -use std::fs::{self}; -use std::path::{Path, PathBuf}; -use std::process::Command; +use std::path::PathBuf; use std::str::FromStr; -use anyhow::{anyhow, bail, Context}; -use html::render_html; -use metadata::PageMetadata; +use anyhow::Context; +mod generate; mod html; mod metadata; mod template; -fn copy_recursively(from: &Path, to: &Path) -> anyhow::Result<()> { - Command::new("cp").arg("-r").arg(from).arg(to).output()?; - - Ok(()) -} - -// {{{ Generate single page -fn generate_page<'s>( - path: &Path, - pages: Option<&[PageMetadata]>, -) -> anyhow::Result<PageMetadata<'s>> { - let content_path = PathBuf::from_str("content")?.join(path); - - let djot_input = std::fs::read_to_string(&content_path).unwrap(); - - // We leak all the file contents, which is fine, as we expect them to - // live on for the rest of the duration of the program. - // - // I'm doing this because a lot of places want to reference this, - // which makes memory management a bit nasty (I need to be able to - // return the metadata out of this function, but that's not really allowed - // if it's referencing a "local variable" like the initial string) - let djot_input = Box::leak(Box::new(djot_input)); - let mut out = String::new(); - - let mut page_renderer = template!("templates/page.html", &mut out)?; - - let events = jotdown::Parser::new(djot_input); - let metadata = PageMetadata::new(content_path, events)?; - - while let Some(label) = page_renderer.current() { - if label == "content" { - let events = jotdown::Parser::new(djot_input); - render_html(&metadata, pages, events, &mut out)?; - } else if label == "navigation" { - out.write_str(r#"<a href="/"><code>~</code></a>"#)?; - out.write_str(" / ")?; - out.write_str(r#"<a href="/echoes"><code>echoes</code></a>"#)?; - } else { - bail!("Unknown label {label} in page template") - } - - page_renderer.next(&mut out)?; - } - - page_renderer.finish(&mut out)?; - - let mut out_path = PathBuf::from_str("dist")?.join(path); - out_path.set_file_name(format!( - "{}.html", - path.file_stem() - .ok_or_else(|| anyhow!("Empty filestem encountered"))? - .to_str() - .unwrap() - )); - std::fs::write(out_path, out).with_context(|| "Failed to write `arcaea.html` post")?; - - Ok(metadata) -} -// }}} -// {{{ Generate an entire directory -fn generate_dir(path: &Path) -> anyhow::Result<()> { - let content_path = PathBuf::from_str("content")?.join(path); - let out_path = PathBuf::from_str("dist")?.join(path); - fs::create_dir_all(&out_path) - .with_context(|| format!("Could not generate directory {path:?}"))?; - let mut files = fs::read_dir(&content_path)?.collect::<Result<Vec<_>, _>>()?; - - // Iterates over the files, removing the `index.dj` file if it does exist. - let has_index = files - .iter() - .enumerate() - .find_map(|(i, f)| { - if f.path().file_name().and_then(|f| f.to_str()) == Some("index.dj") { - Some(i) - } else { - None - } - }) - .map(|index| files.swap_remove(index)) - .is_some(); - - let mut pages = Vec::new(); - - for file in files { - let file_path = file.path(); - let filename = file_path.file_name().unwrap(); - let path = path.join(filename); - if file_path.is_dir() { - generate_dir(&path)?; - } else if file_path.extension().map_or(false, |ext| ext == "dj") { - pages.push(generate_page(&path, None)?); - } else { - fs::copy(content_path.join(filename), out_path.join(filename))?; - } - } - - if has_index { - pages.sort_by_key(|post| (post.config.created_at, post.last_modified)); - - let path = path.join("index.dj"); - generate_page(&path, Some(&pages))?; - } - - Ok(()) -} -// }}} - fn main() -> anyhow::Result<()> { let public_path = PathBuf::from_str("public")?; let dist_path = PathBuf::from_str("dist")?; @@ -130,12 +18,16 @@ fn main() -> anyhow::Result<()> { std::fs::create_dir(&dist_path).with_context(|| "Cannot create `dist` directory")?; + let mut page = generate::Pages::default(); + page.add_dir(&PathBuf::from_str("")?) + .with_context(|| "Failed to collect directories")?; + page.generate(PathBuf::from_str("dist")?) + .with_context(|| "Failed to generate markup")?; + for p in std::fs::read_dir(public_path)? { - copy_recursively(&p?.path(), &dist_path) + generate::copy_recursively(&p?.path(), &dist_path) .with_context(|| "Cannot copy `public` -> `dist`")?; } - generate_dir(&PathBuf::new())?; - Ok(()) } diff --git a/src/metadata.rs b/src/metadata.rs index b7acb15..576a283 100644 --- a/src/metadata.rs +++ b/src/metadata.rs @@ -2,6 +2,7 @@ use std::ffi::OsStr; use std::fmt::Write; use std::path::{Component, Path, PathBuf}; use std::process::Command; +use std::str::FromStr; use anyhow::{anyhow, bail, Context}; use chrono::{DateTime, FixedOffset, Utc}; @@ -41,6 +42,7 @@ pub enum PageRoute { } impl PageRoute { + // {{{ Convert a path to a route fn from_path(path: &Path) -> anyhow::Result<Self> { let Some(Component::Normal(first)) = path.components().nth(1) else { bail!("Path is too short"); @@ -68,6 +70,17 @@ impl PageRoute { Ok(result) } + // }}} + // {{{ Convert a route to a path + #[inline] + pub fn to_path(&self) -> PathBuf { + match self { + Self::Home => PathBuf::from_str(".").unwrap(), + Self::Posts => PathBuf::from_str("echoes").unwrap(), + Self::Post(id) => PathBuf::from_str(&format!("echoes/{id}")).unwrap(), + } + } + // }}} } // }}} // {{{ Metadata @@ -88,13 +101,18 @@ pub struct PageMetadata<'s> { pub description: Vec<jotdown::Event<'s>>, #[allow(dead_code)] pub toc: Vec<Heading<'s>>, + pub source: &'s str, pub word_count: usize, pub last_modified: DateTime<FixedOffset>, } impl<'a> PageMetadata<'a> { - pub fn new(path: PathBuf, mut events: impl Iterator<Item = Event<'a>>) -> anyhow::Result<Self> { + pub fn new( + path: PathBuf, + source: &'a str, + mut events: impl Iterator<Item = Event<'a>>, + ) -> anyhow::Result<Self> { let last_modified_output = Command::new("git") .arg("log") .arg("-1") @@ -128,6 +146,7 @@ impl<'a> PageMetadata<'a> { route: PageRoute::from_path(&path)?, title: title.clone(), last_modified, + source, config: w.config, description: w.description, toc: w.toc, diff --git a/src/templates/page.html b/src/templates/page.html index 3733735..433c545 100644 --- a/src/templates/page.html +++ b/src/templates/page.html @@ -28,11 +28,10 @@ <body> <header> <nav> - {{navigation}} + <a href="/"><code>~</code></a> / + <a href="/echoes"><code>echoes</code></a> </nav> - </header - <main> - {{content}} - </main> + </header> + <main>{{content}}</main> </body> </html>