Post list page
This commit is contained in:
parent
4ca0d221d5
commit
3206bd5948
content/echoes
public
src
|
@ -5,7 +5,7 @@ created_at = "2024-11-02T05:13:44+01:00"
|
|||
|
||||
{ role=description }
|
||||
:::
|
||||
This article goes over pretty much everything about the mobile rhythm game "Arcaea". Along the way, you'll learn about everything the game does right, and the areas it could improve on. By the end, I hope you'll come to understand my love for this game.
|
||||
A discussion on pretty much every aspect of the mobile rhythm game "Arcaea".
|
||||
:::
|
||||
|
||||
# Why I love arcaea
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
# Echoes
|
||||
# Articles
|
||||
|
||||
> "Remnants of the One who once dwelled within the Silver of the Sky. They now wander endlessly through the mists of the heavens, at times drawn to lost vessels who seek to hear them. Thence, thou with this knowledge are yet to attain the full knowledge of the Plan, but worry not — the World shall linger much longer still."
|
||||
|
||||
This page contains a list of all my long-form blog posts.
|
||||
|
||||
- [Why I love Arcaea](./arcaea)
|
||||
- [Games I love](./games)
|
||||
- [The rhythm of the moon](./the-realm-s-secrets)
|
||||
::: posts
|
||||
:::
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
{ role=description }
|
||||
:::
|
||||
An overview of the inner workings and technical decisions behind this website, including my reasons for choosing [djot](https://djot.net/) over markdown, rendering LaTeX quickly, templating without needless allocations, and more.
|
||||
:::
|
||||
|
||||
# The realm's secrets
|
||||
|
||||
## Djot (why not markdown?)
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
[home](/)
|
||||
|
||||
# Why I love Yu-Gi-Oh!
|
||||
|
||||
Lorem ipsum odor amet, consectetuer adipiscing elit. Per lacus at sociosqu curae varius nunc; magnis elit. Dictum dis tristique semper velit montes eleifend suscipit taciti. Himenaeos nunc morbi litora mi at molestie porttitor non sit. Convallis cursus ante tincidunt suspendisse class lobortis. Sodales fusce congue aliquet; eros lectus enim ullamcorper. Aptent fames laoreet odio pretium fermentum pharetra nisl fames sem.
|
||||
|
|
|
@ -10,6 +10,16 @@ body {
|
|||
margin: auto;
|
||||
}
|
||||
|
||||
.article-list {
|
||||
max-width: 36em;
|
||||
margin: auto;
|
||||
list-style-type: none;
|
||||
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
}
|
||||
|
||||
/* {{{ General element tweaks*/
|
||||
blockquote {
|
||||
padding-left: 1.25rem;
|
||||
|
@ -19,7 +29,7 @@ blockquote {
|
|||
|
||||
ul,
|
||||
ol {
|
||||
padding-left: 1rem;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
h1,
|
||||
|
@ -28,7 +38,7 @@ h3,
|
|||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
a {
|
||||
.heading-anchor {
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
/* Note: I need to check whether this only aligns things better with the font I use */
|
||||
|
@ -198,10 +208,6 @@ math[display="block"] {
|
|||
|
||||
/* {{{ Light theme */
|
||||
@media (prefers-color-scheme: light) {
|
||||
body {
|
||||
color: #4c4f69;
|
||||
}
|
||||
|
||||
code {
|
||||
background: #eff1f5;
|
||||
}
|
||||
|
|
225
src/html.rs
225
src/html.rs
|
@ -28,12 +28,13 @@ use crate::template::TemplateRenderer;
|
|||
/// Render djot content as HTML.
|
||||
pub fn render_html<'s>(
|
||||
metadata: &'s PageMetadata,
|
||||
pages: Option<&'s [PageMetadata]>,
|
||||
mut events: impl Iterator<Item = Event<'s>>,
|
||||
mut out: impl std::fmt::Write,
|
||||
out: &mut impl std::fmt::Write,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut w = Writer::new(Some(metadata));
|
||||
events.try_for_each(|e| w.render_event(&e, &mut out))?;
|
||||
w.render_epilogue(&mut out)?;
|
||||
let mut w = Writer::new(Some(metadata), pages);
|
||||
events.try_for_each(|e| w.render_event(&e, out))?;
|
||||
w.render_epilogue(out)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -44,6 +45,7 @@ pub struct Writer<'s> {
|
|||
states: Vec<State<'s>>,
|
||||
footnotes: Footnotes<'s>,
|
||||
metadata: Option<&'s PageMetadata<'s>>,
|
||||
pages: Option<&'s [PageMetadata<'s>]>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
@ -59,12 +61,13 @@ enum State<'s> {
|
|||
}
|
||||
|
||||
impl<'s> Writer<'s> {
|
||||
pub fn new(metadata: Option<&'s PageMetadata>) -> Self {
|
||||
pub fn new(metadata: Option<&'s PageMetadata>, pages: Option<&'s [PageMetadata]>) -> Self {
|
||||
Self {
|
||||
list_tightness: Vec::new(),
|
||||
states: Vec::new(),
|
||||
footnotes: Footnotes::default(),
|
||||
metadata,
|
||||
pages,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -72,7 +75,7 @@ impl<'s> Writer<'s> {
|
|||
pub fn render_event(
|
||||
&mut self,
|
||||
e: &Event<'s>,
|
||||
mut out: impl std::fmt::Write,
|
||||
out: &mut impl std::fmt::Write,
|
||||
) -> anyhow::Result<()> {
|
||||
// {{{ Handle "footnote" states
|
||||
if matches!(self.states.last(), Some(State::Footnote(_))) {
|
||||
|
@ -126,16 +129,16 @@ impl<'s> Writer<'s> {
|
|||
match &c {
|
||||
// {{{ Section
|
||||
Container::Section { id } => match self.metadata {
|
||||
Some(meta)
|
||||
if meta.title.id == *id && matches!(meta.route, PageRoute::Post(_)) =>
|
||||
{
|
||||
let mut renderer = template!("templates/post.html", &mut out)?;
|
||||
Some(meta) if meta.title.id == *id => {
|
||||
if matches!(meta.route, PageRoute::Post(_)) {
|
||||
let mut renderer = template!("templates/post.html", out)?;
|
||||
|
||||
assert_eq!(renderer.current(), Some("attrs"));
|
||||
write!(out, "{}", Attr("aria-labeledby", id))?;
|
||||
renderer.next(&mut out)?;
|
||||
assert_eq!(renderer.current(), Some("attrs"));
|
||||
write!(out, "{}", Attr("aria-labeledby", id))?;
|
||||
renderer.next(out)?;
|
||||
|
||||
self.states.push(State::Article(renderer));
|
||||
self.states.push(State::Article(renderer));
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
write!(out, "<section {}>", Attr("aria-labeledby", id))?;
|
||||
|
@ -147,11 +150,11 @@ impl<'s> Writer<'s> {
|
|||
class: class @ ("aside" | "long-aside" | "char-aside"),
|
||||
} => {
|
||||
let mut renderer = if *class == "aside" {
|
||||
template!("templates/aside.html", &mut out)?
|
||||
template!("templates/aside.html", out)?
|
||||
} else if *class == "char-aside" {
|
||||
template!("templates/character-aside.html", &mut out)?
|
||||
template!("templates/character-aside.html", out)?
|
||||
} else {
|
||||
template!("templates/long-aside.html", &mut out)?
|
||||
template!("templates/long-aside.html", out)?
|
||||
};
|
||||
|
||||
while let Some(label) = renderer.current() {
|
||||
|
@ -162,18 +165,18 @@ impl<'s> Writer<'s> {
|
|||
anyhow!("Cannot find `character` attribute on `aside` element")
|
||||
})?;
|
||||
|
||||
write_attribute(&mut out, &character)?;
|
||||
write_attribute(out, &character)?;
|
||||
}
|
||||
"title" => {
|
||||
let title = attrs.get_value("title").ok_or_else(|| {
|
||||
anyhow!("Cannot find `title` attribute on `aside` element")
|
||||
})?;
|
||||
|
||||
write_attribute(&mut out, &title)?;
|
||||
write_attribute(out, &title)?;
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
renderer.next(&mut out)?;
|
||||
renderer.next(out)?;
|
||||
}
|
||||
|
||||
self.states.push(State::Aside(renderer));
|
||||
|
@ -219,7 +222,7 @@ impl<'s> Writer<'s> {
|
|||
""
|
||||
};
|
||||
|
||||
write!(out, r#"<a href="{prefix}{}"">"#, Escaped(dst))?;
|
||||
write!(out, r#"<a href="{prefix}{}">"#, Escaped(dst))?;
|
||||
}
|
||||
}
|
||||
// }}}
|
||||
|
@ -252,7 +255,7 @@ impl<'s> Writer<'s> {
|
|||
Container::Heading { level, id, .. } => {
|
||||
write!(
|
||||
out,
|
||||
r##"<h{level} id="{}"><a href="#{}">◇</a> "##,
|
||||
r##"<h{level} id="{}"><a class="heading-anchor" href="#{}">◇</a> "##,
|
||||
Escaped(id),
|
||||
Escaped(id)
|
||||
)?;
|
||||
|
@ -267,6 +270,50 @@ impl<'s> Writer<'s> {
|
|||
out.write_str("<p>")?;
|
||||
}
|
||||
// }}}
|
||||
// {{{ 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"))? {
|
||||
// Skip drafts
|
||||
if post.config.created_at.is_none() {
|
||||
continue;
|
||||
}
|
||||
|
||||
template!("templates/post-summary.html", out)?.feed(
|
||||
out,
|
||||
|label, out| {
|
||||
match label {
|
||||
"id" => {
|
||||
if let PageRoute::Post(id) = &post.route {
|
||||
write!(out, "{id}")?;
|
||||
}
|
||||
}
|
||||
"title" => {
|
||||
for event in &post.title.events {
|
||||
self.render_event(event, out)?;
|
||||
}
|
||||
}
|
||||
"description" => {
|
||||
for event in &post.description {
|
||||
self.render_event(event, out)?;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if !fill_metadata_label(out, label, post)? {
|
||||
bail!("Unknown label {label} in `post-summary` template");
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
},
|
||||
)?;
|
||||
}
|
||||
write!(out, "</ol>")?;
|
||||
// We don't care about the contents of this block
|
||||
self.states.push(State::Ignore);
|
||||
}
|
||||
// }}}
|
||||
// {{{ Div
|
||||
Container::Div { class } => {
|
||||
if attrs
|
||||
|
@ -365,15 +412,14 @@ impl<'s> Writer<'s> {
|
|||
// }}}
|
||||
// {{{ Section
|
||||
Container::Section { id, .. } => match self.metadata {
|
||||
Some(meta)
|
||||
if meta.title.id == *id
|
||||
&& matches!(self.states.last(), Some(State::Article(_))) =>
|
||||
{
|
||||
let Some(State::Article(renderer)) = self.states.pop() else {
|
||||
unreachable!()
|
||||
};
|
||||
Some(meta) if meta.title.id == *id => {
|
||||
if matches!(self.states.last(), Some(State::Article(_))) {
|
||||
let Some(State::Article(renderer)) = self.states.pop() else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
renderer.finish(&mut out)?;
|
||||
renderer.finish(out)?;
|
||||
}
|
||||
}
|
||||
_ => out.write_str("</section>")?,
|
||||
},
|
||||
|
@ -387,7 +433,7 @@ impl<'s> Writer<'s> {
|
|||
panic!("Finished `aside` element without being in the `Aside` state.")
|
||||
};
|
||||
|
||||
renderer.finish(&mut out)?;
|
||||
renderer.finish(out)?;
|
||||
}
|
||||
// }}}
|
||||
Container::Heading { level, .. } => {
|
||||
|
@ -399,49 +445,8 @@ impl<'s> Writer<'s> {
|
|||
// SAFETY: we can never enter into the `article` state without having
|
||||
// some metadata on hand.
|
||||
let meta = self.metadata.unwrap();
|
||||
while let Some(label) = renderer.next(&mut out)? {
|
||||
if label == "posted_on" {
|
||||
if let Some(d) = meta.config.created_at {
|
||||
write!(out, "Posted on ")?;
|
||||
write_datetime(&mut out, &d)?;
|
||||
} else {
|
||||
write!(out, "Being conjured by ")?;
|
||||
}
|
||||
} else if label == "updated_on" {
|
||||
write_datetime(&mut out, &meta.last_modified)?;
|
||||
} else if label == "word_count" {
|
||||
let wc = meta.word_count;
|
||||
if wc < 400 {
|
||||
write!(out, "{}", wc)?;
|
||||
} else if wc < 1000 {
|
||||
write!(out, "{}", wc / 10 * 10)?;
|
||||
} else if wc < 2000 {
|
||||
write!(out, "{}", wc / 100 * 100)?;
|
||||
} else {
|
||||
write!(out, "{} thousand", wc / 1000)?;
|
||||
}
|
||||
} else if label == "reading_duration" {
|
||||
let minutes = meta.word_count / 200;
|
||||
if minutes == 0 {
|
||||
let seconds = meta.word_count * 60 / 200;
|
||||
write!(out, "very short {seconds} second")?;
|
||||
} else if minutes < 10 {
|
||||
write!(out, "short {minutes} minute")?;
|
||||
} else if minutes < 20 {
|
||||
write!(out, "somewhat short {minutes} minute")?;
|
||||
} else if minutes < 30 {
|
||||
write!(out, "somewhat long {minutes}")?;
|
||||
} else if minutes < 60 {
|
||||
write!(out, "long {minutes}")?;
|
||||
} else {
|
||||
let hours = minutes / 60;
|
||||
let minutes = minutes % 60;
|
||||
write!(
|
||||
out,
|
||||
"very long {hours} hour and {minutes} minute"
|
||||
)?;
|
||||
}
|
||||
} else if label == "content" {
|
||||
while let Some(label) = renderer.next(out)? {
|
||||
if !fill_metadata_label(out, label, meta)? {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -526,13 +531,13 @@ impl<'s> Writer<'s> {
|
|||
}
|
||||
HighlightEvent::HighlightStart(Highlight(index)) => {
|
||||
write!(
|
||||
&mut out,
|
||||
out,
|
||||
r#"<span class="{}">"#,
|
||||
highlight_classes[index]
|
||||
)?;
|
||||
}
|
||||
HighlightEvent::HighlightEnd => {
|
||||
write!(&mut out, r#"</span>"#)?;
|
||||
write!(out, r#"</span>"#)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -619,14 +624,14 @@ impl<'s> Writer<'s> {
|
|||
Event::Softbreak => out.write_char('\n')?,
|
||||
// }}}
|
||||
Event::ThematicBreak(_) => out.write_str("<hr>")?,
|
||||
Event::Escape | Event::Blankline | Event::Attributes(..) => {}
|
||||
Event::Escape | Event::Blankline | Event::Attributes(_) => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// {{{ Render epilogue
|
||||
fn render_epilogue(&mut self, mut out: impl std::fmt::Write) -> anyhow::Result<()> {
|
||||
fn render_epilogue(&mut self, out: &mut impl std::fmt::Write) -> anyhow::Result<()> {
|
||||
if self.footnotes.reference_encountered() {
|
||||
// TODO: rewrite this using a template
|
||||
out.write_str("<section role=\"doc-endnotes\"><hr><ol>")?;
|
||||
|
@ -635,19 +640,17 @@ impl<'s> Writer<'s> {
|
|||
write!(out, r#"<li id="fn{number}">"#)?;
|
||||
|
||||
for e in events.iter().flatten() {
|
||||
self.render_event(e, &mut out)?;
|
||||
self.render_event(e, out)?;
|
||||
}
|
||||
|
||||
write!(
|
||||
out,
|
||||
r##"
|
||||
<a href="#fnref{number}" role="doc-backlink">
|
||||
Return to content
|
||||
Return to content ↩︎
|
||||
</a></li>
|
||||
"##,
|
||||
)?;
|
||||
|
||||
println!("\u{21A9}\u{FE0E}");
|
||||
}
|
||||
|
||||
out.write_str("</ol></section>")?;
|
||||
|
@ -702,12 +705,12 @@ impl<'s> Display for Attr<'s> {
|
|||
// {{{ Render datetimes
|
||||
#[inline]
|
||||
fn write_datetime<T: TimeZone>(
|
||||
mut out: impl std::fmt::Write,
|
||||
out: &mut impl std::fmt::Write,
|
||||
datetime: &DateTime<T>,
|
||||
) -> std::fmt::Result {
|
||||
let datetime = datetime.to_utc();
|
||||
write!(
|
||||
&mut out,
|
||||
out,
|
||||
r#"<time datetime="{}">{}</time>"#,
|
||||
datetime.to_rfc3339(),
|
||||
datetime.format("%a, %d %b %Y")
|
||||
|
@ -716,7 +719,7 @@ fn write_datetime<T: TimeZone>(
|
|||
// }}}
|
||||
// {{{ Jotdown attribute helpers
|
||||
#[inline]
|
||||
fn write_attribute(mut out: impl std::fmt::Write, attr: &AttributeValue) -> std::fmt::Result {
|
||||
fn write_attribute(out: &mut impl std::fmt::Write, attr: &AttributeValue) -> std::fmt::Result {
|
||||
attr.parts()
|
||||
.try_for_each(|part| write!(out, "{}", Escaped(part)))
|
||||
}
|
||||
|
@ -774,3 +777,55 @@ impl<'s> Iterator for Footnotes<'s> {
|
|||
}
|
||||
}
|
||||
// }}}
|
||||
// {{{ Fill in metadata labels
|
||||
fn fill_metadata_label(
|
||||
out: &mut impl std::fmt::Write,
|
||||
label: &str,
|
||||
meta: &PageMetadata<'_>,
|
||||
) -> anyhow::Result<bool> {
|
||||
if label == "posted_on" {
|
||||
if let Some(d) = meta.config.created_at {
|
||||
write!(out, "Posted on ")?;
|
||||
write_datetime(out, &d)?;
|
||||
} else {
|
||||
write!(out, "Being conjured by ")?;
|
||||
}
|
||||
} else if label == "updated_on" {
|
||||
write_datetime(out, &meta.last_modified)?;
|
||||
} else if label == "word_count" {
|
||||
let wc = meta.word_count;
|
||||
if wc < 400 {
|
||||
write!(out, "{}", wc)?;
|
||||
} else if wc < 1000 {
|
||||
write!(out, "{}", wc / 10 * 10)?;
|
||||
} else if wc < 2000 {
|
||||
write!(out, "{}", wc / 100 * 100)?;
|
||||
} else {
|
||||
write!(out, "{} thousand", wc / 1000)?;
|
||||
}
|
||||
} else if label == "reading_duration" {
|
||||
let minutes = meta.word_count / 200;
|
||||
if minutes == 0 {
|
||||
let seconds = meta.word_count * 60 / 200;
|
||||
write!(out, "very short {seconds} second")?;
|
||||
} else if minutes < 10 {
|
||||
write!(out, "short {minutes} minute")?;
|
||||
} else if minutes < 20 {
|
||||
write!(out, "somewhat short {minutes} minute")?;
|
||||
} else if minutes < 30 {
|
||||
write!(out, "somewhat long {minutes}")?;
|
||||
} else if minutes < 60 {
|
||||
write!(out, "long {minutes}")?;
|
||||
} else {
|
||||
let hours = minutes / 60;
|
||||
let minutes = minutes % 60;
|
||||
write!(out, "very long {hours} hour and {minutes} minute")?;
|
||||
}
|
||||
} else {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
// }}}
|
||||
|
|
62
src/main.rs
62
src/main.rs
|
@ -4,7 +4,7 @@ use std::path::{Path, PathBuf};
|
|||
use std::process::Command;
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use anyhow::{anyhow, bail, Context};
|
||||
use html::render_html;
|
||||
use metadata::PageMetadata;
|
||||
|
||||
|
@ -19,30 +19,42 @@ fn copy_recursively(from: &Path, to: &Path) -> anyhow::Result<()> {
|
|||
}
|
||||
|
||||
// {{{ Generate single page
|
||||
fn generate_page(path: &Path) -> anyhow::Result<()> {
|
||||
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(events, content_path)?;
|
||||
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, events, &mut out)?;
|
||||
page_renderer.next(&mut out)?;
|
||||
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>"#)?;
|
||||
page_renderer.next(&mut out)?;
|
||||
} else {
|
||||
break;
|
||||
bail!("Unknown label {label} in page template")
|
||||
}
|
||||
|
||||
page_renderer.next(&mut out)?;
|
||||
}
|
||||
|
||||
page_renderer.finish(&mut out)?;
|
||||
|
@ -57,7 +69,7 @@ fn generate_page(path: &Path) -> anyhow::Result<()> {
|
|||
));
|
||||
std::fs::write(out_path, out).with_context(|| "Failed to write `arcaea.html` post")?;
|
||||
|
||||
Ok(())
|
||||
Ok(metadata)
|
||||
}
|
||||
// }}}
|
||||
// {{{ Generate an entire directory
|
||||
|
@ -66,20 +78,44 @@ fn generate_dir(path: &Path) -> anyhow::Result<()> {
|
|||
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<_>, _>>()?;
|
||||
|
||||
for file in fs::read_dir(&content_path)? {
|
||||
let file_path = file?.path();
|
||||
// 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") {
|
||||
generate_page(&path)?;
|
||||
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(())
|
||||
}
|
||||
// }}}
|
||||
|
|
|
@ -37,7 +37,6 @@ impl PageConfig {
|
|||
pub enum PageRoute {
|
||||
Home,
|
||||
Posts,
|
||||
#[allow(dead_code)]
|
||||
Post(String),
|
||||
}
|
||||
|
||||
|
@ -86,7 +85,6 @@ pub struct PageMetadata<'s> {
|
|||
pub route: PageRoute,
|
||||
|
||||
pub title: Heading<'s>,
|
||||
#[allow(dead_code)]
|
||||
pub description: Vec<jotdown::Event<'s>>,
|
||||
#[allow(dead_code)]
|
||||
pub toc: Vec<Heading<'s>>,
|
||||
|
@ -96,15 +94,7 @@ pub struct PageMetadata<'s> {
|
|||
}
|
||||
|
||||
impl<'a> PageMetadata<'a> {
|
||||
pub fn new(mut events: impl Iterator<Item = Event<'a>>, path: PathBuf) -> anyhow::Result<Self> {
|
||||
let mut w = Writer::new();
|
||||
events.try_for_each(|e| w.render_event(&e))?;
|
||||
|
||||
let title = w
|
||||
.toc
|
||||
.first()
|
||||
.ok_or_else(|| anyhow!("No heading found to infer title from"))?;
|
||||
|
||||
pub fn new(path: PathBuf, mut events: impl Iterator<Item = Event<'a>>) -> anyhow::Result<Self> {
|
||||
let last_modified_output = Command::new("git")
|
||||
.arg("log")
|
||||
.arg("-1")
|
||||
|
@ -126,19 +116,27 @@ impl<'a> PageMetadata<'a> {
|
|||
})?
|
||||
};
|
||||
|
||||
let mut w = Writer::new();
|
||||
events.try_for_each(|e| w.render_event(&e))?;
|
||||
|
||||
let title = w
|
||||
.toc
|
||||
.first()
|
||||
.ok_or_else(|| anyhow!("No heading found to infer title from"))?;
|
||||
|
||||
Ok(Self {
|
||||
title: title.to_owned(),
|
||||
description: w.description,
|
||||
route: PageRoute::from_path(&path)?,
|
||||
title: title.clone(),
|
||||
last_modified,
|
||||
config: w.config,
|
||||
description: w.description,
|
||||
toc: w.toc,
|
||||
word_count: w.word_count,
|
||||
last_modified,
|
||||
})
|
||||
}
|
||||
}
|
||||
// }}}
|
||||
|
||||
// {{{ Metadata parsing
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
enum State {
|
||||
Toplevel,
|
||||
|
@ -244,3 +242,4 @@ impl<'s> Writer<'s> {
|
|||
Ok(())
|
||||
}
|
||||
}
|
||||
// }}}
|
||||
|
|
|
@ -63,7 +63,7 @@ pub struct TemplateRenderer<'a> {
|
|||
|
||||
impl<'a> TemplateRenderer<'a> {
|
||||
#[inline]
|
||||
pub fn start(template: &'a Template, mut w: impl std::fmt::Write) -> anyhow::Result<Self> {
|
||||
pub fn start(template: &'a Template, w: &mut impl std::fmt::Write) -> anyhow::Result<Self> {
|
||||
let stop_index = if !template.stops.is_empty() {
|
||||
Some(0)
|
||||
} else {
|
||||
|
@ -87,7 +87,7 @@ impl<'a> TemplateRenderer<'a> {
|
|||
}
|
||||
|
||||
/// Attempt to finish rendering.
|
||||
pub fn finish(mut self, w: impl std::fmt::Write) -> anyhow::Result<()> {
|
||||
pub fn finish(mut self, w: &mut impl std::fmt::Write) -> anyhow::Result<()> {
|
||||
if let Some(label) = self.next(w)? {
|
||||
bail!("Attempting to finish template rendering before label `{label}` was handled");
|
||||
}
|
||||
|
@ -97,7 +97,7 @@ impl<'a> TemplateRenderer<'a> {
|
|||
|
||||
// {{{ Advance to the next placeholder
|
||||
/// Move onto the next placeholder
|
||||
pub fn next(&mut self, mut w: impl std::fmt::Write) -> anyhow::Result<Option<&'a str>> {
|
||||
pub fn next(&mut self, w: &mut impl std::fmt::Write) -> anyhow::Result<Option<&'a str>> {
|
||||
let Some(stop_index) = self.stop_index else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
@ -128,6 +128,22 @@ impl<'a> TemplateRenderer<'a> {
|
|||
}
|
||||
}
|
||||
// }}}
|
||||
|
||||
pub fn feed<W: std::fmt::Write>(
|
||||
&mut self,
|
||||
out: &mut W,
|
||||
mut f: impl FnMut(&str, &mut W) -> anyhow::Result<bool>,
|
||||
) -> anyhow::Result<()> {
|
||||
while let Some(label) = self.current() {
|
||||
if f(label, out)? {
|
||||
self.next(out)?;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
// }}}
|
||||
// {{{ Macro
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
<article>
|
||||
<h2>
|
||||
<a href="/echoes/{{id}}" rel="bookmark">{{title}}</a>
|
||||
</h2>
|
||||
<li>
|
||||
<article>
|
||||
<h2>
|
||||
<a href="/echoes/{{id}}" rel="bookmark">{{title}}</a>
|
||||
</h2>
|
||||
|
||||
<ul>
|
||||
<li>{{posted_on}} by <a href="about:blank">prescientmoon</a></li>
|
||||
<li>Last updated on {{updated_on}}.</li>
|
||||
<li>About {{word_count}} words; a {{reading_duration}} read</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>{{posted_on}} by <a href="about:blank">prescientmoon</a></li>
|
||||
<li>Last updated on {{updated_on}}.</li>
|
||||
<li>About {{word_count}} words; a {{reading_duration}} read</li>
|
||||
</ul>
|
||||
|
||||
{{description}}
|
||||
</article>
|
||||
{{description}}
|
||||
</article>
|
||||
</li>
|
||||
|
|
Loading…
Reference in a new issue