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 }
|
{ 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
|
# 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."
|
> "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.
|
This page contains a list of all my long-form blog posts.
|
||||||
|
|
||||||
- [Why I love Arcaea](./arcaea)
|
::: posts
|
||||||
- [Games I love](./games)
|
:::
|
||||||
- [The rhythm of the moon](./the-realm-s-secrets)
|
|
||||||
|
|
|
@ -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
|
# The realm's secrets
|
||||||
|
|
||||||
## Djot (why not markdown?)
|
## Djot (why not markdown?)
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
[home](/)
|
|
||||||
|
|
||||||
# Why I love Yu-Gi-Oh!
|
# 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.
|
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;
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.article-list {
|
||||||
|
max-width: 36em;
|
||||||
|
margin: auto;
|
||||||
|
list-style-type: none;
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style-type: disc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* {{{ General element tweaks*/
|
/* {{{ General element tweaks*/
|
||||||
blockquote {
|
blockquote {
|
||||||
padding-left: 1.25rem;
|
padding-left: 1.25rem;
|
||||||
|
@ -19,7 +29,7 @@ blockquote {
|
||||||
|
|
||||||
ul,
|
ul,
|
||||||
ol {
|
ol {
|
||||||
padding-left: 1rem;
|
padding-left: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1,
|
h1,
|
||||||
|
@ -28,7 +38,7 @@ h3,
|
||||||
h4,
|
h4,
|
||||||
h5,
|
h5,
|
||||||
h6 {
|
h6 {
|
||||||
a {
|
.heading-anchor {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
/* Note: I need to check whether this only aligns things better with the font I use */
|
/* 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 */
|
/* {{{ Light theme */
|
||||||
@media (prefers-color-scheme: light) {
|
@media (prefers-color-scheme: light) {
|
||||||
body {
|
|
||||||
color: #4c4f69;
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
code {
|
||||||
background: #eff1f5;
|
background: #eff1f5;
|
||||||
}
|
}
|
||||||
|
|
225
src/html.rs
225
src/html.rs
|
@ -28,12 +28,13 @@ use crate::template::TemplateRenderer;
|
||||||
/// Render djot content as HTML.
|
/// Render djot content as HTML.
|
||||||
pub fn render_html<'s>(
|
pub fn render_html<'s>(
|
||||||
metadata: &'s PageMetadata,
|
metadata: &'s PageMetadata,
|
||||||
|
pages: Option<&'s [PageMetadata]>,
|
||||||
mut events: impl Iterator<Item = Event<'s>>,
|
mut events: impl Iterator<Item = Event<'s>>,
|
||||||
mut out: impl std::fmt::Write,
|
out: &mut impl std::fmt::Write,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let mut w = Writer::new(Some(metadata));
|
let mut w = Writer::new(Some(metadata), pages);
|
||||||
events.try_for_each(|e| w.render_event(&e, &mut out))?;
|
events.try_for_each(|e| w.render_event(&e, out))?;
|
||||||
w.render_epilogue(&mut out)?;
|
w.render_epilogue(out)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -44,6 +45,7 @@ pub struct Writer<'s> {
|
||||||
states: Vec<State<'s>>,
|
states: Vec<State<'s>>,
|
||||||
footnotes: Footnotes<'s>,
|
footnotes: Footnotes<'s>,
|
||||||
metadata: Option<&'s PageMetadata<'s>>,
|
metadata: Option<&'s PageMetadata<'s>>,
|
||||||
|
pages: Option<&'s [PageMetadata<'s>]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
@ -59,12 +61,13 @@ enum State<'s> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'s> Writer<'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 {
|
Self {
|
||||||
list_tightness: Vec::new(),
|
list_tightness: Vec::new(),
|
||||||
states: Vec::new(),
|
states: Vec::new(),
|
||||||
footnotes: Footnotes::default(),
|
footnotes: Footnotes::default(),
|
||||||
metadata,
|
metadata,
|
||||||
|
pages,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,7 +75,7 @@ impl<'s> Writer<'s> {
|
||||||
pub fn render_event(
|
pub fn render_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
e: &Event<'s>,
|
e: &Event<'s>,
|
||||||
mut out: impl std::fmt::Write,
|
out: &mut impl std::fmt::Write,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
// {{{ Handle "footnote" states
|
// {{{ Handle "footnote" states
|
||||||
if matches!(self.states.last(), Some(State::Footnote(_))) {
|
if matches!(self.states.last(), Some(State::Footnote(_))) {
|
||||||
|
@ -126,16 +129,16 @@ impl<'s> Writer<'s> {
|
||||||
match &c {
|
match &c {
|
||||||
// {{{ Section
|
// {{{ Section
|
||||||
Container::Section { id } => match self.metadata {
|
Container::Section { id } => match self.metadata {
|
||||||
Some(meta)
|
Some(meta) if meta.title.id == *id => {
|
||||||
if meta.title.id == *id && matches!(meta.route, PageRoute::Post(_)) =>
|
if matches!(meta.route, PageRoute::Post(_)) {
|
||||||
{
|
let mut renderer = template!("templates/post.html", out)?;
|
||||||
let mut renderer = template!("templates/post.html", &mut out)?;
|
|
||||||
|
|
||||||
assert_eq!(renderer.current(), Some("attrs"));
|
assert_eq!(renderer.current(), Some("attrs"));
|
||||||
write!(out, "{}", Attr("aria-labeledby", id))?;
|
write!(out, "{}", Attr("aria-labeledby", id))?;
|
||||||
renderer.next(&mut out)?;
|
renderer.next(out)?;
|
||||||
|
|
||||||
self.states.push(State::Article(renderer));
|
self.states.push(State::Article(renderer));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
write!(out, "<section {}>", Attr("aria-labeledby", id))?;
|
write!(out, "<section {}>", Attr("aria-labeledby", id))?;
|
||||||
|
@ -147,11 +150,11 @@ impl<'s> Writer<'s> {
|
||||||
class: class @ ("aside" | "long-aside" | "char-aside"),
|
class: class @ ("aside" | "long-aside" | "char-aside"),
|
||||||
} => {
|
} => {
|
||||||
let mut renderer = if *class == "aside" {
|
let mut renderer = if *class == "aside" {
|
||||||
template!("templates/aside.html", &mut out)?
|
template!("templates/aside.html", out)?
|
||||||
} else if *class == "char-aside" {
|
} else if *class == "char-aside" {
|
||||||
template!("templates/character-aside.html", &mut out)?
|
template!("templates/character-aside.html", out)?
|
||||||
} else {
|
} else {
|
||||||
template!("templates/long-aside.html", &mut out)?
|
template!("templates/long-aside.html", out)?
|
||||||
};
|
};
|
||||||
|
|
||||||
while let Some(label) = renderer.current() {
|
while let Some(label) = renderer.current() {
|
||||||
|
@ -162,18 +165,18 @@ impl<'s> Writer<'s> {
|
||||||
anyhow!("Cannot find `character` attribute on `aside` element")
|
anyhow!("Cannot find `character` attribute on `aside` element")
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
write_attribute(&mut out, &character)?;
|
write_attribute(out, &character)?;
|
||||||
}
|
}
|
||||||
"title" => {
|
"title" => {
|
||||||
let title = attrs.get_value("title").ok_or_else(|| {
|
let title = attrs.get_value("title").ok_or_else(|| {
|
||||||
anyhow!("Cannot find `title` attribute on `aside` element")
|
anyhow!("Cannot find `title` attribute on `aside` element")
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
write_attribute(&mut out, &title)?;
|
write_attribute(out, &title)?;
|
||||||
}
|
}
|
||||||
_ => break,
|
_ => break,
|
||||||
}
|
}
|
||||||
renderer.next(&mut out)?;
|
renderer.next(out)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.states.push(State::Aside(renderer));
|
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, .. } => {
|
Container::Heading { level, id, .. } => {
|
||||||
write!(
|
write!(
|
||||||
out,
|
out,
|
||||||
r##"<h{level} id="{}"><a href="#{}">◇</a> "##,
|
r##"<h{level} id="{}"><a class="heading-anchor" href="#{}">◇</a> "##,
|
||||||
Escaped(id),
|
Escaped(id),
|
||||||
Escaped(id)
|
Escaped(id)
|
||||||
)?;
|
)?;
|
||||||
|
@ -267,6 +270,50 @@ impl<'s> Writer<'s> {
|
||||||
out.write_str("<p>")?;
|
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
|
// {{{ Div
|
||||||
Container::Div { class } => {
|
Container::Div { class } => {
|
||||||
if attrs
|
if attrs
|
||||||
|
@ -365,15 +412,14 @@ impl<'s> Writer<'s> {
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Section
|
// {{{ Section
|
||||||
Container::Section { id, .. } => match self.metadata {
|
Container::Section { id, .. } => match self.metadata {
|
||||||
Some(meta)
|
Some(meta) if meta.title.id == *id => {
|
||||||
if meta.title.id == *id
|
if matches!(self.states.last(), Some(State::Article(_))) {
|
||||||
&& matches!(self.states.last(), Some(State::Article(_))) =>
|
let Some(State::Article(renderer)) = self.states.pop() else {
|
||||||
{
|
unreachable!()
|
||||||
let Some(State::Article(renderer)) = self.states.pop() else {
|
};
|
||||||
unreachable!()
|
|
||||||
};
|
|
||||||
|
|
||||||
renderer.finish(&mut out)?;
|
renderer.finish(out)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => out.write_str("</section>")?,
|
_ => out.write_str("</section>")?,
|
||||||
},
|
},
|
||||||
|
@ -387,7 +433,7 @@ impl<'s> Writer<'s> {
|
||||||
panic!("Finished `aside` element without being in the `Aside` state.")
|
panic!("Finished `aside` element without being in the `Aside` state.")
|
||||||
};
|
};
|
||||||
|
|
||||||
renderer.finish(&mut out)?;
|
renderer.finish(out)?;
|
||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
Container::Heading { level, .. } => {
|
Container::Heading { level, .. } => {
|
||||||
|
@ -399,49 +445,8 @@ impl<'s> Writer<'s> {
|
||||||
// SAFETY: we can never enter into the `article` state without having
|
// SAFETY: we can never enter into the `article` state without having
|
||||||
// some metadata on hand.
|
// some metadata on hand.
|
||||||
let meta = self.metadata.unwrap();
|
let meta = self.metadata.unwrap();
|
||||||
while let Some(label) = renderer.next(&mut out)? {
|
while let Some(label) = renderer.next(out)? {
|
||||||
if label == "posted_on" {
|
if !fill_metadata_label(out, label, meta)? {
|
||||||
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" {
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -526,13 +531,13 @@ impl<'s> Writer<'s> {
|
||||||
}
|
}
|
||||||
HighlightEvent::HighlightStart(Highlight(index)) => {
|
HighlightEvent::HighlightStart(Highlight(index)) => {
|
||||||
write!(
|
write!(
|
||||||
&mut out,
|
out,
|
||||||
r#"<span class="{}">"#,
|
r#"<span class="{}">"#,
|
||||||
highlight_classes[index]
|
highlight_classes[index]
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
HighlightEvent::HighlightEnd => {
|
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::Softbreak => out.write_char('\n')?,
|
||||||
// }}}
|
// }}}
|
||||||
Event::ThematicBreak(_) => out.write_str("<hr>")?,
|
Event::ThematicBreak(_) => out.write_str("<hr>")?,
|
||||||
Event::Escape | Event::Blankline | Event::Attributes(..) => {}
|
Event::Escape | Event::Blankline | Event::Attributes(_) => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// {{{ Render epilogue
|
// {{{ 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() {
|
if self.footnotes.reference_encountered() {
|
||||||
// TODO: rewrite this using a template
|
// TODO: rewrite this using a template
|
||||||
out.write_str("<section role=\"doc-endnotes\"><hr><ol>")?;
|
out.write_str("<section role=\"doc-endnotes\"><hr><ol>")?;
|
||||||
|
@ -635,19 +640,17 @@ impl<'s> Writer<'s> {
|
||||||
write!(out, r#"<li id="fn{number}">"#)?;
|
write!(out, r#"<li id="fn{number}">"#)?;
|
||||||
|
|
||||||
for e in events.iter().flatten() {
|
for e in events.iter().flatten() {
|
||||||
self.render_event(e, &mut out)?;
|
self.render_event(e, out)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
write!(
|
write!(
|
||||||
out,
|
out,
|
||||||
r##"
|
r##"
|
||||||
<a href="#fnref{number}" role="doc-backlink">
|
<a href="#fnref{number}" role="doc-backlink">
|
||||||
Return to content
|
Return to content ↩︎
|
||||||
</a></li>
|
</a></li>
|
||||||
"##,
|
"##,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
println!("\u{21A9}\u{FE0E}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
out.write_str("</ol></section>")?;
|
out.write_str("</ol></section>")?;
|
||||||
|
@ -702,12 +705,12 @@ impl<'s> Display for Attr<'s> {
|
||||||
// {{{ Render datetimes
|
// {{{ Render datetimes
|
||||||
#[inline]
|
#[inline]
|
||||||
fn write_datetime<T: TimeZone>(
|
fn write_datetime<T: TimeZone>(
|
||||||
mut out: impl std::fmt::Write,
|
out: &mut impl std::fmt::Write,
|
||||||
datetime: &DateTime<T>,
|
datetime: &DateTime<T>,
|
||||||
) -> std::fmt::Result {
|
) -> std::fmt::Result {
|
||||||
let datetime = datetime.to_utc();
|
let datetime = datetime.to_utc();
|
||||||
write!(
|
write!(
|
||||||
&mut out,
|
out,
|
||||||
r#"<time datetime="{}">{}</time>"#,
|
r#"<time datetime="{}">{}</time>"#,
|
||||||
datetime.to_rfc3339(),
|
datetime.to_rfc3339(),
|
||||||
datetime.format("%a, %d %b %Y")
|
datetime.format("%a, %d %b %Y")
|
||||||
|
@ -716,7 +719,7 @@ fn write_datetime<T: TimeZone>(
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Jotdown attribute helpers
|
// {{{ Jotdown attribute helpers
|
||||||
#[inline]
|
#[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()
|
attr.parts()
|
||||||
.try_for_each(|part| write!(out, "{}", Escaped(part)))
|
.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::process::Command;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use anyhow::{anyhow, Context};
|
use anyhow::{anyhow, bail, Context};
|
||||||
use html::render_html;
|
use html::render_html;
|
||||||
use metadata::PageMetadata;
|
use metadata::PageMetadata;
|
||||||
|
|
||||||
|
@ -19,30 +19,42 @@ fn copy_recursively(from: &Path, to: &Path) -> anyhow::Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// {{{ Generate single page
|
// {{{ 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 content_path = PathBuf::from_str("content")?.join(path);
|
||||||
|
|
||||||
let djot_input = std::fs::read_to_string(&content_path).unwrap();
|
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 out = String::new();
|
||||||
|
|
||||||
let mut page_renderer = template!("templates/page.html", &mut out)?;
|
let mut page_renderer = template!("templates/page.html", &mut out)?;
|
||||||
|
|
||||||
let events = jotdown::Parser::new(&djot_input);
|
let events = jotdown::Parser::new(djot_input);
|
||||||
let metadata = PageMetadata::new(events, content_path)?;
|
let metadata = PageMetadata::new(content_path, events)?;
|
||||||
|
|
||||||
while let Some(label) = page_renderer.current() {
|
while let Some(label) = page_renderer.current() {
|
||||||
if label == "content" {
|
if label == "content" {
|
||||||
let events = jotdown::Parser::new(&djot_input);
|
let events = jotdown::Parser::new(djot_input);
|
||||||
render_html(&metadata, events, &mut out)?;
|
render_html(&metadata, pages, events, &mut out)?;
|
||||||
page_renderer.next(&mut out)?;
|
|
||||||
} else if label == "navigation" {
|
} else if label == "navigation" {
|
||||||
out.write_str(r#"<a href="/"><code>~</code></a>"#)?;
|
out.write_str(r#"<a href="/"><code>~</code></a>"#)?;
|
||||||
out.write_str(" / ")?;
|
out.write_str(" / ")?;
|
||||||
out.write_str(r#"<a href="/echoes"><code>echoes</code></a>"#)?;
|
out.write_str(r#"<a href="/echoes"><code>echoes</code></a>"#)?;
|
||||||
page_renderer.next(&mut out)?;
|
|
||||||
} else {
|
} else {
|
||||||
break;
|
bail!("Unknown label {label} in page template")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
page_renderer.next(&mut out)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
page_renderer.finish(&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")?;
|
std::fs::write(out_path, out).with_context(|| "Failed to write `arcaea.html` post")?;
|
||||||
|
|
||||||
Ok(())
|
Ok(metadata)
|
||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Generate an entire directory
|
// {{{ Generate an entire directory
|
||||||
|
@ -66,20 +78,44 @@ fn generate_dir(path: &Path) -> anyhow::Result<()> {
|
||||||
let out_path = PathBuf::from_str("dist")?.join(path);
|
let out_path = PathBuf::from_str("dist")?.join(path);
|
||||||
fs::create_dir_all(&out_path)
|
fs::create_dir_all(&out_path)
|
||||||
.with_context(|| format!("Could not generate directory {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)? {
|
// Iterates over the files, removing the `index.dj` file if it does exist.
|
||||||
let file_path = file?.path();
|
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 filename = file_path.file_name().unwrap();
|
||||||
let path = path.join(filename);
|
let path = path.join(filename);
|
||||||
if file_path.is_dir() {
|
if file_path.is_dir() {
|
||||||
generate_dir(&path)?;
|
generate_dir(&path)?;
|
||||||
} else if file_path.extension().map_or(false, |ext| ext == "dj") {
|
} else if file_path.extension().map_or(false, |ext| ext == "dj") {
|
||||||
generate_page(&path)?;
|
pages.push(generate_page(&path, None)?);
|
||||||
} else {
|
} else {
|
||||||
fs::copy(content_path.join(filename), out_path.join(filename))?;
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
|
|
|
@ -37,7 +37,6 @@ impl PageConfig {
|
||||||
pub enum PageRoute {
|
pub enum PageRoute {
|
||||||
Home,
|
Home,
|
||||||
Posts,
|
Posts,
|
||||||
#[allow(dead_code)]
|
|
||||||
Post(String),
|
Post(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,7 +85,6 @@ pub struct PageMetadata<'s> {
|
||||||
pub route: PageRoute,
|
pub route: PageRoute,
|
||||||
|
|
||||||
pub title: Heading<'s>,
|
pub title: Heading<'s>,
|
||||||
#[allow(dead_code)]
|
|
||||||
pub description: Vec<jotdown::Event<'s>>,
|
pub description: Vec<jotdown::Event<'s>>,
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub toc: Vec<Heading<'s>>,
|
pub toc: Vec<Heading<'s>>,
|
||||||
|
@ -96,15 +94,7 @@ pub struct PageMetadata<'s> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> PageMetadata<'a> {
|
impl<'a> PageMetadata<'a> {
|
||||||
pub fn new(mut events: impl Iterator<Item = Event<'a>>, path: PathBuf) -> anyhow::Result<Self> {
|
pub fn new(path: PathBuf, mut events: impl Iterator<Item = Event<'a>>) -> 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"))?;
|
|
||||||
|
|
||||||
let last_modified_output = Command::new("git")
|
let last_modified_output = Command::new("git")
|
||||||
.arg("log")
|
.arg("log")
|
||||||
.arg("-1")
|
.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 {
|
Ok(Self {
|
||||||
title: title.to_owned(),
|
|
||||||
description: w.description,
|
|
||||||
route: PageRoute::from_path(&path)?,
|
route: PageRoute::from_path(&path)?,
|
||||||
|
title: title.clone(),
|
||||||
|
last_modified,
|
||||||
config: w.config,
|
config: w.config,
|
||||||
|
description: w.description,
|
||||||
toc: w.toc,
|
toc: w.toc,
|
||||||
word_count: w.word_count,
|
word_count: w.word_count,
|
||||||
last_modified,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
|
// {{{ Metadata parsing
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||||
enum State {
|
enum State {
|
||||||
Toplevel,
|
Toplevel,
|
||||||
|
@ -244,3 +242,4 @@ impl<'s> Writer<'s> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// }}}
|
||||||
|
|
|
@ -63,7 +63,7 @@ pub struct TemplateRenderer<'a> {
|
||||||
|
|
||||||
impl<'a> TemplateRenderer<'a> {
|
impl<'a> TemplateRenderer<'a> {
|
||||||
#[inline]
|
#[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() {
|
let stop_index = if !template.stops.is_empty() {
|
||||||
Some(0)
|
Some(0)
|
||||||
} else {
|
} else {
|
||||||
|
@ -87,7 +87,7 @@ impl<'a> TemplateRenderer<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempt to finish rendering.
|
/// 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)? {
|
if let Some(label) = self.next(w)? {
|
||||||
bail!("Attempting to finish template rendering before label `{label}` was handled");
|
bail!("Attempting to finish template rendering before label `{label}` was handled");
|
||||||
}
|
}
|
||||||
|
@ -97,7 +97,7 @@ impl<'a> TemplateRenderer<'a> {
|
||||||
|
|
||||||
// {{{ Advance to the next placeholder
|
// {{{ Advance to the next placeholder
|
||||||
/// Move onto 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 {
|
let Some(stop_index) = self.stop_index else {
|
||||||
return Ok(None);
|
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
|
// {{{ Macro
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
<article>
|
<li>
|
||||||
<h2>
|
<article>
|
||||||
<a href="/echoes/{{id}}" rel="bookmark">{{title}}</a>
|
<h2>
|
||||||
</h2>
|
<a href="/echoes/{{id}}" rel="bookmark">{{title}}</a>
|
||||||
|
</h2>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li>{{posted_on}} by <a href="about:blank">prescientmoon</a></li>
|
<li>{{posted_on}} by <a href="about:blank">prescientmoon</a></li>
|
||||||
<li>Last updated on {{updated_on}}.</li>
|
<li>Last updated on {{updated_on}}.</li>
|
||||||
<li>About {{word_count}} words; a {{reading_duration}} read</li>
|
<li>About {{word_count}} words; a {{reading_duration}} read</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{{description}}
|
{{description}}
|
||||||
</article>
|
</article>
|
||||||
|
</li>
|
||||||
|
|
Loading…
Reference in a new issue