From 17f49c2107bbabfbd4c3d4ebac24c06e7aa98ed7 Mon Sep 17 00:00:00 2001 From: prescientmoon <git@moonythm.dev> Date: Fri, 8 Nov 2024 07:45:00 +0100 Subject: [PATCH] Implement base table of contents logic --- content/echoes/arcaea.dj | 10 ++++- src/html.rs | 59 ++++++++++++++++++++++++++++ src/template.rs | 13 ++++++ src/templates/table-of-contents.html | 9 +++++ 4 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 src/templates/table-of-contents.html diff --git a/content/echoes/arcaea.dj b/content/echoes/arcaea.dj index 739ebbf..0f36505 100644 --- a/content/echoes/arcaea.dj +++ b/content/echoes/arcaea.dj @@ -10,8 +10,14 @@ A discussion on pretty much every aspect of the mobile rhythm game "Arcaea". # Why I love arcaea -## What is arcaea -- explain the base mechanics +## Introduction + +- What is arcaea +- What this article is +- How to read this article + +::: toc +::: ## What makes a good rhythm game - I don't need to reinvent the wheel here, I can link that one `mental checkpoint` video diff --git a/src/html.rs b/src/html.rs index 837bee8..2836d20 100644 --- a/src/html.rs +++ b/src/html.rs @@ -1,3 +1,4 @@ +use std::cmp::Ordering; use std::collections::HashMap; use std::fmt::Display; @@ -311,6 +312,64 @@ impl<'s> Writer<'s> { self.states.push(State::Ignore); } // }}} + // {{{ Table of contents + Container::Div { class: "toc" } => { + template!("templates/table-of-contents.html", out)?.feed_fully( + out, + |label, out| { + if label == "content" { + let mut level_stack = Vec::with_capacity(6); + level_stack.push(1); + + for (i, heading) in self.metadata.toc.iter().enumerate() { + loop { + let level = level_stack.last().unwrap(); + match heading.level.cmp(level) { + Ordering::Greater => { + writeln!(out, "<ol>")?; + level_stack.push(heading.level); + break; + } + Ordering::Equal => { + if i != 0 { + writeln!(out, "</li>")?; + } + + break; + } + Ordering::Less => { + writeln!(out, "</li></ol>")?; + level_stack.pop(); + } + } + } + + write!(out, r##"<li><a href="#{}">"##, heading.id)?; + + for event in &heading.events { + self.render_event(event, out)?; + } + + writeln!(out, "</a>")?; + } + + for _ in 0..level_stack.len() - 1 { + writeln!(out, "</li></ol>")?; + } + + writeln!(out, "</li>")?; + + Ok(true) + } else { + Ok(false) + } + }, + )?; + + // We don't care about the contents of this block + self.states.push(State::Ignore); + } + // }}} // {{{ Div Container::Div { class } => { if has_role(attrs, "description") { diff --git a/src/template.rs b/src/template.rs index fac862a..489ddf1 100644 --- a/src/template.rs +++ b/src/template.rs @@ -129,6 +129,7 @@ impl<'a> TemplateRenderer<'a> { } // }}} + /// Automatically fill in placeholders until the provided lambda returns false. pub fn feed<W: std::fmt::Write>( &mut self, out: &mut W, @@ -144,6 +145,18 @@ impl<'a> TemplateRenderer<'a> { Ok(()) } + + /// Equivalent to running [Self::feed] and then [Self::finish]. + pub fn feed_fully<W: std::fmt::Write>( + mut self, + out: &mut W, + f: impl FnMut(&str, &mut W) -> anyhow::Result<bool>, + ) -> anyhow::Result<()> { + self.feed(out, f)?; + self.finish(out)?; + + Ok(()) + } } // }}} // {{{ Macro diff --git a/src/templates/table-of-contents.html b/src/templates/table-of-contents.html new file mode 100644 index 0000000..f27d8de --- /dev/null +++ b/src/templates/table-of-contents.html @@ -0,0 +1,9 @@ +<details> + <summary>Toggle table of contents</summary> + <nav role="doc-toc" aria-labelledby="toc-title"> + <h2 id="toc-title">Table of Contents</h2> + <ol> + {{content}} + </ol> + </nav> +</details>