Clean up generation code
This commit is contained in:
parent
bc3d9f79ff
commit
5d65037b26
src
103
src/generate.rs
Normal file
103
src/generate.rs
Normal 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(())
|
||||
}
|
||||
// }}}
|
||||
}
|
|
@ -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;
|
||||
|
|
128
src/main.rs
128
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(())
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue