1
Fork 0

Clean up generation code

This commit is contained in:
prescientmoon 2024-11-07 05:28:31 +01:00
parent bc3d9f79ff
commit 5d65037b26
Signed by: prescientmoon
SSH key fingerprint: SHA256:WFp/cO76nbarETAoQcQXuV+0h7XJsEsOCI0UsyPIy6U
5 changed files with 141 additions and 128 deletions

103
src/generate.rs Normal file
View file

@ -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(())
}
// }}}
}

View file

@ -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;

View file

@ -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(())
}

View file

@ -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,

View file

@ -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>