1
Fork 0

Code highlighting, and a major refactor of the rendering code

This commit is contained in:
prescientmoon 2024-11-06 04:26:30 +01:00
parent e6895391f6
commit 4ca0d221d5
Signed by: prescientmoon
SSH key fingerprint: SHA256:WFp/cO76nbarETAoQcQXuV+0h7XJsEsOCI0UsyPIy6U
30 changed files with 689 additions and 392 deletions

1
.gitignore vendored
View file

@ -1,3 +1,4 @@
target target
dist dist
oldicons oldicons
result

121
Cargo.lock generated
View file

@ -2,6 +2,15 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 version = 3
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "android-tzdata" name = "android-tzdata"
version = "0.1.1" version = "0.1.1"
@ -131,6 +140,12 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.161" version = "0.2.161"
@ -160,6 +175,9 @@ dependencies = [
"pulldown-latex", "pulldown-latex",
"serde", "serde",
"toml", "toml",
"tree-sitter",
"tree-sitter-highlight",
"tree-sitter-rust",
] ]
[[package]] [[package]]
@ -204,6 +222,35 @@ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "regex"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.214" version = "1.0.214"
@ -240,16 +287,42 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]] [[package]]
name = "syn" name = "streaming-iterator"
version = "2.0.85" version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520"
[[package]]
name = "syn"
version = "2.0.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "thiserror"
version = "1.0.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02dd99dc800bbb97186339685293e1cc5d9df1f8fae2d0aecd9ff1c77efea892"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7c61ec9a6f64d2793d8a45faba21efbe3ced62a886d44c36a009b2b519b4c7e"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "toml" name = "toml"
version = "0.8.19" version = "0.8.19"
@ -284,6 +357,48 @@ dependencies = [
"winnow", "winnow",
] ]
[[package]]
name = "tree-sitter"
version = "0.24.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9871f16d6cf5c4757dcf30d5d2172a2df6987c510c017bbb7abfb7f9aa24d06"
dependencies = [
"cc",
"regex",
"regex-syntax",
"streaming-iterator",
"tree-sitter-language",
]
[[package]]
name = "tree-sitter-highlight"
version = "0.24.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48859aa39513716018d81904220960f415dbb72e071234a721304d20bf245e4c"
dependencies = [
"lazy_static",
"regex",
"streaming-iterator",
"thiserror",
"tree-sitter",
]
[[package]]
name = "tree-sitter-language"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8ddffe35a0e5eeeadf13ff7350af564c6e73993a24db62caee1822b185c2600"
[[package]]
name = "tree-sitter-rust"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cffbbcb780348fbae8395742ae5b34c1fd794e4085d43aac9f259387f9a84dc8"
dependencies = [
"cc",
"tree-sitter-language",
]
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.13" version = "1.0.13"

View file

@ -14,3 +14,6 @@ toml = "0.8.19"
# before switching to the std version. # before switching to the std version.
once_cell = "1.20.2" once_cell = "1.20.2"
chrono = { version = "0.4.38", features = ["serde"] } chrono = { version = "0.4.38", features = ["serde"] }
tree-sitter = "0.24.3"
tree-sitter-rust = "0.23.0"
tree-sitter-highlight = "0.24.3"

View file

@ -3,6 +3,11 @@
created_at = "2024-11-02T05:13:44+01:00" 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.
:::
# Why I love arcaea # Why I love arcaea
## What is arcaea ## What is arcaea

View file

Before

(image error) Size: 498 KiB

After

(image error) Size: 498 KiB

View file

Before

(image error) Size: 32 KiB

After

(image error) Size: 32 KiB

View file

Before

(image error) Size: 251 KiB

After

(image error) Size: 251 KiB

View file

Before

(image error) Size: 101 KiB

After

(image error) Size: 101 KiB

View file

Before

(image error) Size: 1.7 MiB

After

(image error) Size: 1.7 MiB

View file

Before

(image error) Size: 422 KiB

After

(image error) Size: 422 KiB

View file

Before

(image error) Size: 424 KiB

After

(image error) Size: 424 KiB

View file

Before

(image error) Size: 86 KiB

After

(image error) Size: 86 KiB

View file

Before

(image error) Size: 31 KiB

After

(image error) Size: 31 KiB

View file

Before

(image error) Size: 11 KiB

After

(image error) Size: 11 KiB

View file

Before

(image error) Size: 202 KiB

After

(image error) Size: 202 KiB

View file

Before

(image error) Size: 117 KiB

After

(image error) Size: 117 KiB

View file

Before

(image error) Size: 121 KiB

After

(image error) Size: 121 KiB

View file

Before

(image error) Size: 54 KiB

After

(image error) Size: 54 KiB

9
content/echoes/index.dj Normal file
View file

@ -0,0 +1,9 @@
# Echoes
> "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)

View file

@ -8,4 +8,8 @@ You can message me in the following places \^-\^
- via email at <hi@moonythm.dev> - via email at <hi@moonythm.dev>
- on discord as `@prescientmoon` - on discord as `@prescientmoon`
- on the ~ IRC network as `prescientmoon` - on certain IRC networks as `prescientmoon`
## Lore
Some pages will have lore bits sprinkled throughout. English is not my native tongue, so writing in a more archaic-sounding style comes difficult to me, but I'm doing my best. That is, some of the lore snippets might sound utterly meaningless, although I guess that's half the vibe.

View file

@ -17,6 +17,7 @@
pkgs.rust-analyzer pkgs.rust-analyzer
pkgs.rustfmt pkgs.rustfmt
pkgs.imagemagick pkgs.imagemagick
pkgs.http-server
]; ];
buildInputs = with pkgs; [ ]; buildInputs = with pkgs; [ ];

View file

@ -195,3 +195,112 @@ math[display="block"] {
/* }}}*/ /* }}}*/
} }
/* }}}*/ /* }}}*/
/* {{{ Light theme */
@media (prefers-color-scheme: light) {
body {
color: #4c4f69;
}
code {
background: #eff1f5;
}
pre > code {
display: block;
padding: 1rem;
}
/* {{{ Syntax highlighting*/
span.variable {
color: #4c4f69;
}
span.attribute,
span.constant {
color: #fe640b;
}
span.comment,
span.comment-documentation {
color: #9ca0b0;
font-style: italic;
}
span.constant-builtin {
color: #fe640b;
}
span.constructor {
color: #209fb5;
}
span.function,
span.function-method {
color: #1e66f5;
}
span.function-builtin {
color: #fe640b;
}
span.function-macro {
color: #179299;
}
span.keyword {
color: #8839ef;
}
span.label {
color: #209fb5;
}
span.operator {
color: #04a5e5;
}
span.property {
color: #7287fd;
}
span.punctuation,
span.punctuation-delimiter {
color: #7c7f93;
}
span.punctuation-bracket {
color: #7c7f93;
}
span.string {
color: #40a02b;
}
span.string-special {
color: #ea76cb;
}
span.tag {
color: #8839ef;
}
span.type,
span.type-builtin {
color: #df8e1d;
}
span.variable {
color: #4c4f69;
}
span.variable-builtin {
color: #d20f39;
}
span.variable-parameter {
color: #e64553;
}
/* }}}*/
}
/* }}} */

View file

@ -1,16 +1,23 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt::Display;
use anyhow::anyhow; use anyhow::anyhow;
use anyhow::bail; use anyhow::bail;
use chrono::DateTime; use chrono::DateTime;
use chrono::TimeZone; use chrono::TimeZone;
use jotdown::Alignment; use jotdown::Alignment;
use jotdown::AttributeValue;
use jotdown::Container; use jotdown::Container;
use jotdown::Event; use jotdown::Event;
use jotdown::LinkType; use jotdown::LinkType;
use jotdown::ListKind; use jotdown::ListKind;
use jotdown::OrderedListNumbering::*; use jotdown::OrderedListNumbering::*;
use jotdown::SpanLinkType; use jotdown::SpanLinkType;
use tree_sitter::Language;
use tree_sitter_highlight::Highlight;
use tree_sitter_highlight::HighlightConfiguration;
use tree_sitter_highlight::HighlightEvent;
use tree_sitter_highlight::Highlighter;
use crate::metadata::PageMetadata; use crate::metadata::PageMetadata;
use crate::metadata::PageRoute; use crate::metadata::PageRoute;
@ -36,7 +43,7 @@ pub struct Writer<'s> {
list_tightness: Vec<bool>, list_tightness: Vec<bool>,
states: Vec<State<'s>>, states: Vec<State<'s>>,
footnotes: Footnotes<'s>, footnotes: Footnotes<'s>,
metadata: Option<&'s PageMetadata>, metadata: Option<&'s PageMetadata<'s>>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -45,8 +52,10 @@ enum State<'s> {
Ignore, Ignore,
Raw, Raw,
Math(bool), Math(bool),
CodeBlock(String),
Aside(TemplateRenderer<'s>), Aside(TemplateRenderer<'s>),
Article(TemplateRenderer<'s>), Article(TemplateRenderer<'s>),
Footnote(Vec<jotdown::Event<'s>>),
} }
impl<'s> Writer<'s> { impl<'s> Writer<'s> {
@ -65,80 +74,72 @@ impl<'s> Writer<'s> {
e: &Event<'s>, e: &Event<'s>,
mut out: impl std::fmt::Write, mut out: impl std::fmt::Write,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
// {{{ Handle footnotes // {{{ Handle "footnote" states
if let Event::Start(Container::Footnote { label }, ..) = e { if matches!(self.states.last(), Some(State::Footnote(_))) {
self.footnotes.start(label, Vec::new()); if let Event::End(Container::Footnote { label }) = e {
return Ok(()); let Some(State::Footnote(events)) = self.states.pop() else {
} else if let Some(events) = self.footnotes.current() { unreachable!()
if matches!(e, Event::End(Container::Footnote { .. })) {
self.footnotes.end();
} else {
events.push(e.clone());
}
return Ok(());
}
// }}}
// {{{ Handle important state changes
match e {
Event::Start(Container::LinkDefinition { .. }, ..) => {
self.states.push(State::Ignore);
return Ok(());
}
Event::End(Container::LinkDefinition { .. }) => {
// Sanity check
assert!(matches!(self.states.last(), Some(State::Ignore)));
self.states.pop();
}
Event::Start(Container::RawBlock { format } | Container::RawInline { format }, ..) => {
if format == &"html" {
self.states.push(State::Raw);
} else {
self.states.push(State::Ignore);
}; };
self.footnotes.insert(label, events);
} else {
let Some(State::Footnote(events)) = self.states.last_mut() else {
unreachable!()
};
events.push(e.clone());
return Ok(()); return Ok(());
} }
Event::End(Container::RawBlock { .. } | Container::RawInline { .. }) => {
// Sanity check
assert!(matches!(
self.states.last(),
Some(State::Raw | State::Ignore)
));
self.states.pop();
}
_ => {}
} }
// }}} // }}}
// {{{ Handle "text-only" states
if matches!(self.states.last(), Some(State::Ignore)) { if matches!(self.states.last(), Some(State::TextOnly)) {
return Ok(()); match e {
Event::End(Container::Image(..)) => {
self.states.pop();
}
Event::Str(s) => write!(out, "{}", Escaped(s))?,
_ => return Ok(()),
}
} }
// }}}
// {{{ Handle "ignore" states
if matches!(self.states.last(), Some(State::Ignore)) {
match e {
Event::End(
Container::RawBlock { .. }
| Container::RawInline { .. }
| Container::LinkDefinition { .. }
| Container::Div { .. },
) => {
self.states.pop();
return Ok(());
}
_ => return Ok(()),
}
}
// }}}
match e { match e {
// {{{ Container start // {{{ Container start
Event::Start(c, attrs) => { Event::Start(c, attrs) => {
if matches!(self.states.last(), Some(&State::TextOnly)) {
return Ok(());
}
match &c { match &c {
Container::RawBlock { .. } => {}
Container::RawInline { .. } => unreachable!(),
Container::Footnote { .. } => unreachable!(),
// {{{ Section // {{{ Section
Container::Section { id } => match self.metadata { Container::Section { id } => match self.metadata {
Some(meta) Some(meta)
if &meta.title.id == id && matches!(meta.route, PageRoute::Post(_)) => if meta.title.id == *id && matches!(meta.route, PageRoute::Post(_)) =>
{ {
let renderer = template!("templates/post.html", &mut out)?; let mut renderer = template!("templates/post.html", &mut out)?;
// Sanity check
assert_eq!(renderer.current(), Some("attrs")); assert_eq!(renderer.current(), Some("attrs"));
write!(out, "{}", Attr("aria-labeledby", id))?;
renderer.next(&mut out)?;
self.states.push(State::Article(renderer)); self.states.push(State::Article(renderer));
} }
_ => out.write_str("<section")?, _ => {
write!(out, "<section {}>", Attr("aria-labeledby", id))?;
}
}, },
// }}} // }}}
// {{{ Aside // {{{ Aside
@ -154,27 +155,25 @@ impl<'s> Writer<'s> {
}; };
while let Some(label) = renderer.current() { while let Some(label) = renderer.current() {
if label == "character" { match label {
let character = attrs.get_value("character").ok_or_else(|| { "character" => {
anyhow!("Cannot find `character` attribute on `aside` element") let character =
})?; attrs.get_value("character").ok_or_else(|| {
anyhow!("Cannot find `character` attribute on `aside` element")
})?;
character write_attribute(&mut out, &character)?;
.parts() }
.try_for_each(|part| write_attr_contents(part, &mut out))?; "title" => {
renderer.next(&mut out)?; let title = attrs.get_value("title").ok_or_else(|| {
} else if label == "title" { anyhow!("Cannot find `title` attribute on `aside` element")
let title = attrs.get_value("title").ok_or_else(|| { })?;
anyhow!("Cannot find `title` attribute on `aside` element")
})?;
title write_attribute(&mut out, &title)?;
.parts() }
.try_for_each(|part| write_attr_contents(part, &mut out))?; _ => break,
renderer.next(&mut out)?;
} else {
break;
} }
renderer.next(&mut out)?;
} }
self.states.push(State::Aside(renderer)); self.states.push(State::Aside(renderer));
@ -184,7 +183,7 @@ impl<'s> Writer<'s> {
Container::List { kind, tight } => { Container::List { kind, tight } => {
self.list_tightness.push(*tight); self.list_tightness.push(*tight);
match kind { match kind {
ListKind::Unordered(..) => out.write_str("<ul")?, ListKind::Unordered(..) => out.write_str("<ul>")?,
ListKind::Ordered { ListKind::Ordered {
numbering, start, .. numbering, start, ..
} => { } => {
@ -202,6 +201,8 @@ impl<'s> Writer<'s> {
} { } {
write!(out, r#" type="{}""#, ty)?; write!(out, r#" type="{}""#, ty)?;
} }
write!(out, ">")?;
} }
ListKind::Task(_) => bail!("Task lists are not supported"), ListKind::Task(_) => bail!("Task lists are not supported"),
} }
@ -210,179 +211,130 @@ impl<'s> Writer<'s> {
// {{{ Link // {{{ Link
Container::Link(dst, ty) => { Container::Link(dst, ty) => {
if matches!(ty, LinkType::Span(SpanLinkType::Unresolved)) { if matches!(ty, LinkType::Span(SpanLinkType::Unresolved)) {
out.write_str("<a")?; out.write_str("<a>")?;
} else { } else {
out.write_str(r#"<a href=""#)?; let prefix = if matches!(ty, LinkType::Email) {
if matches!(ty, LinkType::Email) { "mailto:"
out.write_str("mailto:")?; } else {
} ""
write_attr_contents(dst, &mut out)?; };
out.write_char('"')?;
write!(out, r#"<a href="{prefix}{}"">"#, Escaped(dst))?;
} }
} }
// }}} // }}}
// {{{ Table cell
Container::TableCell {
head, alignment, ..
} => {
if *head {
out.write_str("<td")?;
} else {
out.write_str("<th")?;
}
if !matches!(alignment, Alignment::Unspecified) {
// TODO: move this to css
let a = match alignment {
Alignment::Unspecified => unreachable!(),
Alignment::Left => "left",
Alignment::Center => "center",
Alignment::Right => "right",
};
write!(out, r#" style="text-align: {};""#, a)?;
}
write!(out, ">")?;
}
// }}}
// {{{ Heading
Container::Heading { level, id, .. } => {
write!(
out,
r##"<h{level} id="{}"><a href="#{}">◇</a> "##,
Escaped(id),
Escaped(id)
)?;
}
// }}}
// {{{ Paragraph // {{{ Paragraph
Container::Paragraph => { Container::Paragraph => {
if self.list_tightness.last() == Some(&true) { if self.list_tightness.last() == Some(&true) {
return Ok(()); return Ok(());
} }
out.write_str("<p")?; out.write_str("<p>")?;
} }
// }}} // }}}
Container::Blockquote => out.write_str("<blockquote")?, // {{{ Div
Container::ListItem { .. } => out.write_str("<li")?, Container::Div { class } => {
Container::DescriptionList => out.write_str("<dl")?, if attrs
Container::DescriptionDetails => out.write_str("<dd")?, .get_value("role")
Container::Table => out.write_str("<table")?, .map_or(false, |role| format!("{role}") == "description")
Container::TableRow { .. } => out.write_str("<tr")?, {
Container::Div { .. } => out.write_str("<div")?, self.states.push(State::Ignore);
Container::Heading { level, .. } => write!(out, "<h{}", level)?, } else {
Container::TableCell { head: false, .. } => out.write_str("<td")?, write!(out, "<div{}>", Attr("class", class))?;
Container::TableCell { head: true, .. } => out.write_str("<th")?, }
Container::Caption => out.write_str("<caption")?, }
Container::Image(..) => out.write_str("<img")?, // }}}
Container::DescriptionTerm => out.write_str("<dt")?, // {{{ Raw block
Container::CodeBlock { .. } => out.write_str("<pre")?, Container::RawBlock { format } | Container::RawInline { format } => {
Container::Span | Container::Math { .. } => out.write_str("<span")?, if format == &"html" {
Container::Verbatim => out.write_str("<code")?, self.states.push(State::Raw);
Container::Subscript => out.write_str("<sub")?, } else {
Container::Superscript => out.write_str("<sup")?, self.states.push(State::Ignore);
Container::Insert => out.write_str("<ins")?, };
Container::Delete => out.write_str("<del")?, }
Container::Strong => out.write_str("<strong")?, // }}}
Container::Emphasis => out.write_str("<em")?, Container::CodeBlock { .. } => {
Container::Mark => out.write_str("<mark")?, self.states.push(State::CodeBlock(String::new()));
out.write_str("<pre><code>")?;
}
Container::Math { display } => {
self.states.push(State::Math(*display));
out.write_str("<math>")?;
}
Container::Image(_, _) => {
self.states.push(State::TextOnly);
out.write_str(r#"<img alt=""#)?;
}
Container::Footnote { .. } => self.states.push(State::Footnote(Vec::new())),
Container::Blockquote => out.write_str("<blockquote>")?,
Container::ListItem { .. } => out.write_str("<li>")?,
Container::DescriptionList => out.write_str("<dl>")?,
Container::DescriptionDetails => out.write_str("<dd>")?,
Container::Table => out.write_str("<table>")?,
Container::TableRow { .. } => out.write_str("<tr>")?,
Container::Caption => out.write_str("<caption>")?,
Container::DescriptionTerm => out.write_str("<dt>")?,
Container::Span => out.write_str("<span>")?,
Container::Verbatim => out.write_str("<code>")?,
Container::Subscript => out.write_str("<sub>")?,
Container::Superscript => out.write_str("<sup>")?,
Container::Insert => out.write_str("<ins>")?,
Container::Delete => out.write_str("<del>")?,
Container::Strong => out.write_str("<strong>")?,
Container::Emphasis => out.write_str("<em>")?,
Container::Mark => out.write_str("<mark>")?,
Container::LinkDefinition { .. } => return Ok(()), Container::LinkDefinition { .. } => return Ok(()),
e => bail!("DJot element {e:?} is not supported"), e => bail!("DJot element {e:?} is not supported"),
} }
// {{{ Decide whether this element supports attributes
let mut write_attr_contentsibs = true;
if matches!(
c,
Container::Div {
class: "aside" | "long-aside" | "char-aside"
}
) {
write_attr_contentsibs = false;
}
// }}}
if write_attr_contentsibs {
// {{{ Write attributes
let mut id_written = false;
let mut class_written = false;
if write_attr_contentsibs {
for (a, v) in attrs.unique_pairs() {
let is_class = a == "class";
let is_id = a == "id";
if (!is_id || !id_written) && (!is_class || !class_written) {
write!(out, r#" {}=""#, a)?;
v.parts()
.try_for_each(|part| write_attr_contents(part, &mut out))?;
out.write_char('"')?;
id_written |= is_id;
class_written |= is_class;
};
}
}
// }}}
// {{{ Write default ids/classes
match c {
Container::Heading { id, .. } if !id_written => {
write_attr("id", id, &mut out)?;
}
Container::Section { id, .. } => {
write_attr("aria-labeledby", id, &mut out)?;
}
Container::Div { class } if !class.is_empty() && !class_written => {
write_attr("class", class, &mut out)?;
}
_ => {}
}
// }}}
// {{{ Write special attributes
match c {
// {{{ Write css for aligning table cell text
Container::TableCell { alignment, .. }
if !matches!(alignment, Alignment::Unspecified) =>
{
let a = match alignment {
Alignment::Unspecified => unreachable!(),
Alignment::Left => "left",
Alignment::Center => "center",
Alignment::Right => "right",
};
write!(out, r#" style="text-align: {};">"#, a)?;
}
// }}}
// {{{ Write language for codeblock
Container::CodeBlock { language } => {
if language.is_empty() {
out.write_str("><code>")?;
} else {
out.write_str(r#"><code class="language-"#)?;
write_attr_contents(language, &mut out)?;
out.write_str(r#"">"#)?;
}
}
// }}}
Container::Image(..) => out.write_str(r#" alt=""#)?,
Container::Math { display } => {
out.write_str(r#">"#)?;
self.states.push(State::Math(*display));
}
_ => match self.states.last_mut() {
Some(State::Article(renderer))
if renderer.current() == Some("attrs") =>
{
renderer.next(&mut out)?;
}
_ => out.write_char('>')?,
},
}
// }}}
}
// {{{ Post-start effects
match &c {
Container::Heading { id, .. } => {
out.write_str(r##"<a href="#"##)?;
write_attr_contents(id, &mut out)?;
out.write_str(r#"">◇</a> "#)?;
}
Container::Image(..) => {
self.states.push(State::TextOnly);
}
_ => {}
}
// }}}
} }
// }}} // }}}
// {{{ Container end // {{{ Container end
Event::End(c) => { Event::End(c) => {
// {{{ Pre-end effects match c {
match &c { Container::Footnote { .. } => unreachable!(),
Container::Image(..) => { // {{{ Raw block
Container::RawBlock { .. } | Container::RawInline { .. } => {
// Sanity check // Sanity check
assert!(matches!(self.states.last(), Some(State::TextOnly))); assert!(matches!(self.states.last(), Some(State::Raw)));
self.states.pop(); self.states.pop();
} }
_ => {} // }}}
}
// }}}
if matches!(self.states.last(), Some(State::TextOnly)) {
return Ok(());
}
match c {
Container::RawBlock { .. } => {}
Container::RawInline { .. } => unreachable!(),
Container::Footnote { .. } => unreachable!(),
// {{{ List // {{{ List
Container::List { kind, .. } => { Container::List { kind, .. } => {
self.list_tightness.pop(); self.list_tightness.pop();
@ -403,28 +355,18 @@ impl<'s> Writer<'s> {
out.write_str("</p>")?; out.write_str("</p>")?;
} }
// }}} // }}}
// {{{ Image
Container::Image(src, ..) => {
if !src.is_empty() {
out.write_str(r#"" src=""#)?;
write_attr_contents(src, &mut out)?;
}
out.write_str(r#"">"#)?;
}
// }}}
// {{{ Math // {{{ Math
Container::Math { .. } => { Container::Math { .. } => {
// Sanity check // Sanity check
assert!(matches!(self.states.last(), Some(State::Math(_)))); assert!(matches!(self.states.last(), Some(State::Math(_))));
self.states.pop(); self.states.pop();
out.write_str(r#"</span>"#)?; out.write_str(r#"</math>"#)?;
} }
// }}} // }}}
// {{{ 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!(self.states.last(), Some(State::Article(_))) => && matches!(self.states.last(), Some(State::Article(_))) =>
{ {
let Some(State::Article(renderer)) = self.states.pop() else { let Some(State::Article(renderer)) = self.states.pop() else {
@ -445,8 +387,6 @@ impl<'s> Writer<'s> {
panic!("Finished `aside` element without being in the `Aside` state.") panic!("Finished `aside` element without being in the `Aside` state.")
}; };
// Sanity check
assert_eq!(renderer.current(), Some("content"));
renderer.finish(&mut out)?; renderer.finish(&mut out)?;
} }
// }}} // }}}
@ -462,42 +402,42 @@ impl<'s> Writer<'s> {
while let Some(label) = renderer.next(&mut out)? { while let Some(label) = renderer.next(&mut out)? {
if label == "posted_on" { if label == "posted_on" {
if let Some(d) = meta.config.created_at { if let Some(d) = meta.config.created_at {
write!(&mut out, "Posted on ")?; write!(out, "Posted on ")?;
write_datetime(&d, &mut out)?; write_datetime(&mut out, &d)?;
} else { } else {
write!(&mut out, "Being conjured by ")?; write!(out, "Being conjured by ")?;
} }
} else if label == "updated_on" { } else if label == "updated_on" {
write_datetime(&meta.last_modified, &mut out)?; write_datetime(&mut out, &meta.last_modified)?;
} else if label == "word_count" { } else if label == "word_count" {
let wc = meta.word_count; let wc = meta.word_count;
if wc < 400 { if wc < 400 {
write!(&mut out, "{}", wc)?; write!(out, "{}", wc)?;
} else if wc < 1000 { } else if wc < 1000 {
write!(&mut out, "{}", wc / 10 * 10)?; write!(out, "{}", wc / 10 * 10)?;
} else if wc < 2000 { } else if wc < 2000 {
write!(&mut out, "{}", wc / 100 * 100)?; write!(out, "{}", wc / 100 * 100)?;
} else { } else {
write!(&mut out, "{} thousand", wc / 1000)?; write!(out, "{} thousand", wc / 1000)?;
} }
} else if label == "reading_duration" { } else if label == "reading_duration" {
let minutes = meta.word_count / 200; let minutes = meta.word_count / 200;
if minutes == 0 { if minutes == 0 {
let seconds = meta.word_count * 60 / 200; let seconds = meta.word_count * 60 / 200;
write!(&mut out, "very short {seconds} second")?; write!(out, "very short {seconds} second")?;
} else if minutes < 10 { } else if minutes < 10 {
write!(&mut out, "short {minutes} minute")?; write!(out, "short {minutes} minute")?;
} else if minutes < 20 { } else if minutes < 20 {
write!(&mut out, "somewhat short {minutes} minute")?; write!(out, "somewhat short {minutes} minute")?;
} else if minutes < 30 { } else if minutes < 30 {
write!(&mut out, "somewhat long {minutes}")?; write!(out, "somewhat long {minutes}")?;
} else if minutes < 60 { } else if minutes < 60 {
write!(&mut out, "long {minutes}")?; write!(out, "long {minutes}")?;
} else { } else {
let hours = minutes / 60; let hours = minutes / 60;
let minutes = minutes % 60; let minutes = minutes % 60;
write!( write!(
&mut out, out,
"very long {hours} hour and {minutes} minute" "very long {hours} hour and {minutes} minute"
)?; )?;
} }
@ -509,6 +449,8 @@ impl<'s> Writer<'s> {
} }
// }}} // }}}
} }
Container::Image(src, ..) => write!(out, r#" {}>"#, Attr("src", src))?,
Container::LinkDefinition { .. } => self.states.push(State::Ignore),
Container::Blockquote => out.write_str("</blockquote>")?, Container::Blockquote => out.write_str("</blockquote>")?,
Container::ListItem { .. } => out.write_str("</li>")?, Container::ListItem { .. } => out.write_str("</li>")?,
Container::DescriptionList => out.write_str("</dl>")?, Container::DescriptionList => out.write_str("</dl>")?,
@ -520,7 +462,87 @@ impl<'s> Writer<'s> {
Container::TableCell { head: true, .. } => out.write_str("</th>")?, Container::TableCell { head: true, .. } => out.write_str("</th>")?,
Container::Caption => out.write_str("</caption>")?, Container::Caption => out.write_str("</caption>")?,
Container::DescriptionTerm => out.write_str("</dt>")?, Container::DescriptionTerm => out.write_str("</dt>")?,
Container::CodeBlock { .. } => out.write_str("</code></pre>")?, // {{{ Syntax highlighting
Container::CodeBlock { language } => {
let Some(State::CodeBlock(buffer)) = self.states.pop() else {
panic!("Arrived at end of code block without being in the approriate state.");
};
if *language == "rust" {
let mut highlighter = Highlighter::new();
let language = Language::new(tree_sitter_rust::LANGUAGE);
let mut config = HighlightConfiguration::new(
language,
"rust",
tree_sitter_rust::HIGHLIGHTS_QUERY,
tree_sitter_rust::INJECTIONS_QUERY,
"",
)?;
let highlight_names = [
"attribute",
"comment",
"comment.documentation",
"constant",
"constant.builtin",
"constructor",
"function",
"function.builtin",
"function.macro",
"function.method",
"keyword",
"label",
"operator",
"property",
"punctuation",
"punctuation.bracket",
"punctuation.delimiter",
"string",
"string.special",
"tag",
"type",
"type.builtin",
"variable",
"variable.builtin",
"variable.parameter",
];
let highlight_classes = highlight_names
.iter()
.map(|s| s.replace(".", "-"))
.collect::<Vec<_>>();
config.configure(&highlight_names);
let highlights =
highlighter
.highlight(&config, buffer.as_bytes(), None, |_| None)?;
for event in highlights {
match event? {
HighlightEvent::Source { start, end } => {
write!(out, "{}", Escaped(&buffer[start..end]))?;
}
HighlightEvent::HighlightStart(Highlight(index)) => {
write!(
&mut out,
r#"<span class="{}">"#,
highlight_classes[index]
)?;
}
HighlightEvent::HighlightEnd => {
write!(&mut out, r#"</span>"#)?;
}
}
}
} else {
write!(out, "{}", Escaped(&buffer))?;
}
out.write_str("</code></pre>")?
}
// }}}
Container::Span => out.write_str("</span>")?, Container::Span => out.write_str("</span>")?,
Container::Link(..) => out.write_str("</a>")?, Container::Link(..) => out.write_str("</a>")?,
Container::Verbatim => out.write_str("</code>")?, Container::Verbatim => out.write_str("</code>")?,
@ -531,15 +553,14 @@ impl<'s> Writer<'s> {
Container::Strong => out.write_str("</strong>")?, Container::Strong => out.write_str("</strong>")?,
Container::Emphasis => out.write_str("</em>")?, Container::Emphasis => out.write_str("</em>")?,
Container::Mark => out.write_str("</mark>")?, Container::Mark => out.write_str("</mark>")?,
Container::LinkDefinition { .. } => unreachable!(),
e => bail!("DJot element {e:?} is not supported"), e => bail!("DJot element {e:?} is not supported"),
} }
} }
// }}} // }}}
// {{{ Raw string // {{{ Raw string
Event::Str(s) => match self.states.last() { Event::Str(s) => match self.states.last_mut() {
Some(State::TextOnly) => write_attr_contents(s, &mut out)?,
Some(State::Raw) => out.write_str(s)?, Some(State::Raw) => out.write_str(s)?,
Some(State::CodeBlock(buffer)) => buffer.push_str(s),
// {{{ Math // {{{ Math
Some(State::Math(display)) => { Some(State::Math(display)) => {
let config = pulldown_latex::RenderConfig { let config = pulldown_latex::RenderConfig {
@ -564,7 +585,7 @@ impl<'s> Writer<'s> {
out.write_str(&mathml)?; out.write_str(&mathml)?;
} }
// }}} // }}}
_ => write_escape(s, false, &mut out)?, _ => write!(out, "{}", Escaped(s))?,
}, },
// }}} // }}}
// {{{ Footnote reference // {{{ Footnote reference
@ -573,8 +594,13 @@ impl<'s> Writer<'s> {
if !matches!(self.states.last(), Some(State::TextOnly)) { if !matches!(self.states.last(), Some(State::TextOnly)) {
write!( write!(
out, out,
r##"<sup><a id="fnref{}" href="#fn{}" role="doc-noteref">{}</a></sup>"##, r##"
number, number, number <sup>
<a id="fnref{number}" href="#fn{number}" role="doc-noteref">
{number}
</a>
</sup>
"##
)?; )?;
} }
} }
@ -592,18 +618,7 @@ impl<'s> Writer<'s> {
Event::Hardbreak => out.write_str("<br>")?, Event::Hardbreak => out.write_str("<br>")?,
Event::Softbreak => out.write_char('\n')?, Event::Softbreak => out.write_char('\n')?,
// }}} // }}}
// {{{ Thematic break Event::ThematicBreak(_) => out.write_str("<hr>")?,
Event::ThematicBreak(attrs) => {
out.write_str("<hr")?;
for (a, v) in attrs.unique_pairs() {
write!(out, r#" {}=""#, a)?;
v.parts()
.try_for_each(|part| write_attr_contents(part, &mut out))?;
out.write_char('"')?;
}
out.write_str(">")?;
}
// }}}
Event::Escape | Event::Blankline | Event::Attributes(..) => {} Event::Escape | Event::Blankline | Event::Attributes(..) => {}
} }
@ -617,7 +632,7 @@ impl<'s> Writer<'s> {
out.write_str("<section role=\"doc-endnotes\"><hr><ol>")?; out.write_str("<section role=\"doc-endnotes\"><hr><ol>")?;
while let Some((number, events)) = self.footnotes.next() { while let Some((number, events)) = self.footnotes.next() {
write!(out, "<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, &mut out)?;
@ -625,9 +640,14 @@ impl<'s> Writer<'s> {
write!( write!(
out, out,
"<a href=\"#fnref{}\" role=\"doc-backlink\">Return to content \u{21A9}\u{FE0E}</a></li>", r##"
number, <a href="#fnref{number}" role="doc-backlink">
Return to content
</a></li>
"##,
)?; )?;
println!("\u{21A9}\u{FE0E}");
} }
out.write_str("</ol></section>")?; out.write_str("</ol></section>")?;
@ -638,50 +658,52 @@ impl<'s> Writer<'s> {
// }}} // }}}
} }
// {{{ Writing helpers // {{{ HTMl escaper
#[inline] pub struct Escaped<'a>(&'a str);
fn write_attr_contents(s: &str, out: impl std::fmt::Write) -> std::fmt::Result {
write_escape(s, true, out)
}
#[inline] impl<'s> Display for Escaped<'s> {
fn write_attr(attr: &str, content: &str, mut out: impl std::fmt::Write) -> std::fmt::Result { fn fmt(&self, out: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(&mut out, r#" {attr}=""#)?; let mut s = self.0;
write_attr_contents(content, &mut out)?; let mut ent = "";
out.write_char('"')?; while let Some(i) = s.find(|c| {
Ok(()) match c {
} '<' => Some("&lt;"),
'>' => Some("&gt;"),
fn write_escape( '&' => Some("&amp;"),
mut s: &str, '"' => Some("&quot;"),
escape_quotes: bool, _ => None,
mut out: impl std::fmt::Write, }
) -> std::fmt::Result { .map_or(false, |s| {
let mut ent = ""; ent = s;
while let Some(i) = s.find(|c| { true
match c { })
'<' => Some("&lt;"), }) {
'>' => Some("&gt;"), out.write_str(&s[..i])?;
'&' => Some("&amp;"), out.write_str(ent)?;
'"' if escape_quotes => Some("&quot;"), s = &s[i + 1..];
_ => None,
} }
.map_or(false, |s| { out.write_str(s)
ent = s;
true
})
}) {
out.write_str(&s[..i])?;
out.write_str(ent)?;
s = &s[i + 1..];
} }
out.write_str(s)
} }
// }}}
// {{{ Render attributes
pub struct Attr<'a>(&'static str, &'a str);
impl<'s> Display for Attr<'s> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if !self.1.is_empty() {
write!(f, r#" {}="{}""#, self.0, Escaped(self.1))?;
}
Ok(())
}
}
// }}}
// {{{ Render datetimes
#[inline] #[inline]
fn write_datetime<T: TimeZone>( fn write_datetime<T: TimeZone>(
datetime: &DateTime<T>,
mut out: impl std::fmt::Write, mut out: impl std::fmt::Write,
datetime: &DateTime<T>,
) -> std::fmt::Result { ) -> std::fmt::Result {
let datetime = datetime.to_utc(); let datetime = datetime.to_utc();
write!( write!(
@ -692,6 +714,13 @@ fn write_datetime<T: TimeZone>(
) )
} }
// }}} // }}}
// {{{ Jotdown attribute helpers
#[inline]
fn write_attribute(mut out: impl std::fmt::Write, attr: &AttributeValue) -> std::fmt::Result {
attr.parts()
.try_for_each(|part| write!(out, "{}", Escaped(part)))
}
// }}}
// {{{ Footnotes // {{{ Footnotes
/// Helper to aggregate footnotes for rendering at the end of the document. It will cache footnote /// Helper to aggregate footnotes for rendering at the end of the document. It will cache footnote
/// events until they should be emitted at the end. /// events until they should be emitted at the end.
@ -700,8 +729,6 @@ fn write_datetime<T: TimeZone>(
/// the order they were first referenced. /// the order they were first referenced.
#[derive(Default)] #[derive(Default)]
struct Footnotes<'s> { struct Footnotes<'s> {
/// Stack of current open footnotes, with label and staging buffer.
open: Vec<(&'s str, Vec<Event<'s>>)>,
/// Footnote references in the order they were first encountered. /// Footnote references in the order they were first encountered.
references: Vec<&'s str>, references: Vec<&'s str>,
/// Events for each footnote. /// Events for each footnote.
@ -730,20 +757,9 @@ impl<'s> Footnotes<'s> {
) )
} }
/// Start aggregating a footnote. /// Insert a new footnote to be renderer later
fn start(&mut self, label: &'s str, events: Vec<Event<'s>>) { fn insert(&mut self, label: &'s str, events: Vec<jotdown::Event<'s>>) {
self.open.push((label, events)); self.events.insert(label, events);
}
/// Obtain the current (most recently started) footnote.
fn current(&mut self) -> Option<&mut Vec<Event<'s>>> {
self.open.last_mut().map(|(_, e)| e)
}
/// End the current (most recently started) footnote.
fn end(&mut self) {
let (label, stage) = self.open.pop().unwrap();
self.events.insert(label, stage);
} }
} }

View file

@ -38,7 +38,7 @@ fn generate_page(path: &Path) -> anyhow::Result<()> {
} 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="/posts"><code>posts</code></a>"#)?; out.write_str(r#"<a href="/echoes"><code>echoes</code></a>"#)?;
page_renderer.next(&mut out)?; page_renderer.next(&mut out)?;
} else { } else {
break; break;

View file

@ -4,12 +4,11 @@ use std::path::{Component, Path, PathBuf};
use std::process::Command; use std::process::Command;
use anyhow::{anyhow, bail, Context}; use anyhow::{anyhow, bail, Context};
use chrono::{DateTime, FixedOffset}; use chrono::{DateTime, FixedOffset, Utc};
use jotdown::{Container, Event}; use jotdown::{Container, Event};
use serde::Deserialize; use serde::Deserialize;
use crate::html; // {{{ Config
#[derive(Deserialize, Debug, Default)] #[derive(Deserialize, Debug, Default)]
pub struct PageConfig { pub struct PageConfig {
pub created_at: Option<DateTime<FixedOffset>>, pub created_at: Option<DateTime<FixedOffset>>,
@ -32,7 +31,8 @@ impl PageConfig {
Ok(()) Ok(())
} }
} }
// }}}
// {{{ Routing
#[derive(Debug)] #[derive(Debug)]
pub enum PageRoute { pub enum PageRoute {
Home, Home,
@ -49,16 +49,19 @@ impl PageRoute {
let result = if first == OsStr::new("index.dj") { let result = if first == OsStr::new("index.dj") {
Self::Home Self::Home
} else if first == OsStr::new("posts") { } else if first == OsStr::new("echoes") {
if let Some(Component::Normal(second)) = path.components().nth(2) { let Some(Component::Normal(second)) = path.components().nth(2) else {
let mut slice = second.to_str().unwrap(); bail!("Cannot convert path '{:?}' to page route", path);
if slice.ends_with(".dj") { };
slice = slice.strip_suffix(".dj").unwrap(); let mut slice = second.to_str().unwrap();
} if slice.ends_with(".dj") {
slice = slice.strip_suffix(".dj").unwrap();
}
Self::Post(slice.to_owned()) if slice == "index" {
} else {
Self::Posts Self::Posts
} else {
Self::Post(slice.to_owned())
} }
} else { } else {
bail!("Cannot convert path '{:?}' to page route", path); bail!("Cannot convert path '{:?}' to page route", path);
@ -67,26 +70,33 @@ impl PageRoute {
Ok(result) Ok(result)
} }
} }
// }}}
#[derive(Debug)] // {{{ Metadata
pub struct PageMetadata { #[derive(Debug, Clone)]
pub title: Heading, pub struct Heading<'a> {
pub config: PageConfig,
pub word_count: usize,
pub last_modified: DateTime<FixedOffset>,
pub route: PageRoute,
#[allow(dead_code)] #[allow(dead_code)]
pub toc: Vec<Heading>, pub level: u8,
#[allow(dead_code)] pub id: String, // Heading events own their ID, so we have to clone
pub path: PathBuf, pub events: Vec<jotdown::Event<'a>>,
} }
impl PageMetadata { #[derive(Debug)]
pub fn new<'s>( pub struct PageMetadata<'s> {
mut events: impl Iterator<Item = Event<'s>>, pub config: PageConfig,
path: PathBuf, pub route: PageRoute,
) -> anyhow::Result<Self> {
pub title: Heading<'s>,
#[allow(dead_code)]
pub description: Vec<jotdown::Event<'s>>,
#[allow(dead_code)]
pub toc: Vec<Heading<'s>>,
pub word_count: usize,
pub last_modified: DateTime<FixedOffset>,
}
impl<'a> PageMetadata<'a> {
pub fn new(mut events: impl Iterator<Item = Event<'a>>, path: PathBuf) -> anyhow::Result<Self> {
let mut w = Writer::new(); let mut w = Writer::new();
events.try_for_each(|e| w.render_event(&e))?; events.try_for_each(|e| w.render_event(&e))?;
@ -104,56 +114,53 @@ impl PageMetadata {
.with_context(|| anyhow!("Could not read the last modification date for file"))? .with_context(|| anyhow!("Could not read the last modification date for file"))?
.stdout; .stdout;
let last_modified = String::from_utf8(last_modified_output)?; let last_modified = String::from_utf8(last_modified_output)?;
let last_modified = DateTime::parse_from_rfc3339(&last_modified).with_context(|| {
anyhow!( let last_modified = if last_modified.is_empty() {
"Failed to parse datetime returned by git '{}'", Utc::now().fixed_offset()
last_modified } else {
) DateTime::parse_from_rfc3339(&last_modified).with_context(|| {
})?; anyhow!(
"Failed to parse datetime returned by git '{}'",
last_modified
)
})?
};
Ok(Self { Ok(Self {
title: title.to_owned(), title: title.to_owned(),
description: w.description,
route: PageRoute::from_path(&path)?, route: PageRoute::from_path(&path)?,
config: w.config, config: w.config,
toc: w.toc, toc: w.toc,
word_count: w.word_count, word_count: w.word_count,
last_modified, last_modified,
path,
}) })
} }
} }
// }}}
#[derive(Debug, Clone)]
pub struct Heading {
#[allow(dead_code)]
pub level: u8,
pub id: String,
pub text: String,
pub html: String,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)] #[derive(Debug, PartialEq, Eq, Clone, Copy)]
enum State { enum State {
Toplevel, Toplevel,
Heading, Heading,
Config, Config,
Description,
} }
struct Writer<'s> { struct Writer<'s> {
/// This renderer is used for generating the html for the titles
html_renderer: html::Writer<'s>,
config: PageConfig, config: PageConfig,
toc: Vec<Heading>, toc: Vec<Heading<'s>>,
toml_text: String, toml_text: String,
state: State, state: State,
word_count: usize, word_count: usize,
description: Vec<jotdown::Event<'s>>,
} }
impl<'s> Writer<'s> { impl<'s> Writer<'s> {
fn new() -> Self { fn new() -> Self {
Self { Self {
html_renderer: html::Writer::new(None),
config: PageConfig::default(), config: PageConfig::default(),
description: Vec::new(),
toc: Vec::new(), toc: Vec::new(),
toml_text: String::new(), toml_text: String::new(),
state: State::Toplevel, state: State::Toplevel,
@ -161,7 +168,7 @@ impl<'s> Writer<'s> {
} }
} }
fn render_event(&mut self, e: &jotdown::Event<'s>) -> anyhow::Result<()> { fn render_event<'a>(&mut self, e: &'a jotdown::Event<'s>) -> anyhow::Result<()> {
if let Event::Str(content) = e { if let Event::Str(content) = e {
if self.state != State::Config { if self.state != State::Config {
self.word_count += content self.word_count += content
@ -172,20 +179,23 @@ impl<'s> Writer<'s> {
} }
match e { match e {
// {{{ Headings
Event::Start(Container::Heading { level, id, .. }, _) => { Event::Start(Container::Heading { level, id, .. }, _) => {
assert_eq!(self.state, State::Toplevel); assert_eq!(self.state, State::Toplevel);
self.state = State::Heading; self.state = State::Heading;
self.toc.push(Heading { self.toc.push(Heading {
level: *level as u8, level: *level as u8,
events: Vec::new(),
// These ids are always borrowed, unless modified by the user (i.e. me)
id: id.to_string(), id: id.to_string(),
text: String::new(),
html: String::new(),
}) })
} }
Event::End(Container::Heading { .. }) => { Event::End(Container::Heading { .. }) => {
assert_eq!(self.state, State::Heading); assert_eq!(self.state, State::Heading);
self.state = State::Toplevel; self.state = State::Toplevel;
} }
// }}}
// {{{ TOML config blocks
Event::Start(Container::RawBlock { format: "toml" }, attrs) => { Event::Start(Container::RawBlock { format: "toml" }, attrs) => {
assert_eq!(self.state, State::Toplevel); assert_eq!(self.state, State::Toplevel);
if let Some(role_attr) = attrs.get_value("role") { if let Some(role_attr) = attrs.get_value("role") {
@ -205,16 +215,28 @@ impl<'s> Writer<'s> {
self.toml_text.clear(); self.toml_text.clear();
} }
} }
// }}}
// {{{ Descriptions
Event::Start(Container::Div { .. }, attrs) if self.state == State::Toplevel => {
if let Some(role_attr) = attrs.get_value("role") {
if format!("{}", role_attr) == "description" {
self.state = State::Description
}
}
}
Event::End(Container::Div { .. }) if self.state == State::Description => {
self.state = State::Toplevel;
}
// }}}
Event::Str(str) if self.state == State::Config => { Event::Str(str) if self.state == State::Config => {
self.toml_text.write_str(str)?; self.toml_text.write_str(str)?;
} }
other if self.state == State::Heading => { _ if self.state == State::Description => {
self.description.push(e.clone());
}
_ if self.state == State::Heading => {
let last_heading = self.toc.last_mut().unwrap(); let last_heading = self.toc.last_mut().unwrap();
self.html_renderer.render_event(e, &mut last_heading.html)?; last_heading.events.push(e.clone());
if let Event::Str(str) = other {
last_heading.text.write_str(str)?;
}
} }
_ => {} _ => {}
} }

View file

@ -127,7 +127,6 @@ impl<'a> TemplateRenderer<'a> {
None => (self.template.text.len(), self.template.text.len()), None => (self.template.text.len(), self.template.text.len()),
} }
} }
// }}} // }}}
} }
// }}} // }}}

View file

@ -0,0 +1,13 @@
<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>
{{description}}
</article>