From 8f17b2a721d2369691ff2e3d4950a436aa9533ab Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Wed, 9 Nov 2022 15:56:55 +0200 Subject: feat: implement rudimentary table of contents, without links The links will be added later. Sponsored-by: author --- riki.md | 20 +++++++++++++++ src/directive/mod.rs | 3 ++- src/directive/toc.rs | 72 +++++++++++++++++++++++++++++++++++++++++++++++++--- src/error.rs | 3 +++ src/html.rs | 8 ++++-- src/page.rs | 21 +++++++++++++-- src/wikitext.rs | 21 ++++++++------- 7 files changed, 128 insertions(+), 20 deletions(-) diff --git a/riki.md b/riki.md index 4c78f11..980e5b4 100644 --- a/riki.md +++ b/riki.md @@ -677,6 +677,26 @@ goodbye | cruel world """]] ~~~ +### `toc` + +_Requirement: the `toc` directive creates a table of contents._ + +~~~scenario +given an installed riki +given file site/index.mdwn from toc +when I run riki build site output +when I run cat output/index.html +then file output/index.html contains "
  • Introduction
  • " +then file output/index.html contains "
  • Acknowledgements
  • " +~~~ + +~~~{#toc .file .markdown} +[[!toc]] + +# Introduction +## Acknowledgements +~~~ + ## Source file tree ### Listing source files diff --git a/src/directive/mod.rs b/src/directive/mod.rs index c8fe1f7..ae2dc09 100644 --- a/src/directive/mod.rs +++ b/src/directive/mod.rs @@ -8,6 +8,7 @@ use std::collections::HashSet; pub enum Processed { Markdown(String), + Toc(usize), } pub trait DirectiveImplementation { @@ -279,7 +280,7 @@ mod tag; use tag::Tag; mod toc; -use toc::Toc; +pub use toc::Toc; mod traillink; use traillink::TrailLink; diff --git a/src/directive/toc.rs b/src/directive/toc.rs index e00cb24..f027931 100644 --- a/src/directive/toc.rs +++ b/src/directive/toc.rs @@ -1,22 +1,86 @@ use crate::directive::{DirectiveImplementation, Processed}; use crate::error::SiteError; +use crate::html::{Content, Element, ElementTag}; use crate::page::PageMeta; use crate::site::Site; use crate::wikitext::ParsedDirective; #[derive(Debug, Default, Eq, PartialEq)] -pub struct Toc {} +pub struct Toc { + levels: String, +} impl DirectiveImplementation for Toc { const REQUIRED: &'static [&'static str] = &[]; const ALLOWED: &'static [&'static str] = &["levels"]; const ALLOW_ANY_UNNAMED: bool = true; - fn from_parsed(_: &ParsedDirective) -> Self { - Self::default() + fn from_parsed(p: &ParsedDirective) -> Self { + let args = p.args(); + let levels = args.get("levels").unwrap_or(&"9"); + Self::new(levels.to_string()) } fn process(&self, _site: &Site, _meta: &mut PageMeta) -> Result { - Err(SiteError::UnimplementedDirective("toc".into())) + let levels: usize = self + .levels + .parse() + .map_err(|e| SiteError::LevelsParse(self.levels.clone(), e))?; + Ok(Processed::Toc(levels)) + } +} + +impl Toc { + fn new(levels: String) -> Self { + Self { levels } + } + + pub fn post_process(html: &Element, levels: usize) -> String { + let headings: Vec<(usize, &[Content])> = html + .children() + .iter() + .filter_map(|c| match c { + Content::Elt(e) => Some(e), + _ => None, + }) + .filter_map(|e| match e.tag() { + ElementTag::H1 => Some((1, e.children())), + ElementTag::H2 => Some((2, e.children())), + ElementTag::H3 => Some((3, e.children())), + ElementTag::H4 => Some((4, e.children())), + ElementTag::H5 => Some((5, e.children())), + ElementTag::H6 => Some((6, e.children())), + _ => None, + }) + .collect(); + let mut html = String::new(); + let mut prev_level: usize = 0; + for (level, text) in headings.iter() { + if *level > levels { + continue; + } else if *level > prev_level { + html.push_str("
      "); + } else if *level < prev_level { + html.push_str("
    \n"); + } + html.push_str("
  • "); + Self::stringify(&mut html, text); + html.push_str("
  • \n"); + prev_level = *level; + } + for _ in 0..prev_level { + html.push_str("\n"); + } + html + } + + fn stringify(buf: &mut String, bits: &[Content]) { + for c in bits.iter() { + match c { + Content::Text(s) => buf.push_str(s), + Content::Elt(e) => Self::stringify(buf, e.children()), + Content::Html(h) => buf.push_str(h), + } + } } } diff --git a/src/error.rs b/src/error.rs index 90046b2..d58924e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -91,6 +91,9 @@ pub enum SiteError { #[error("directive isn't implemented yet: {0}")] UnimplementedDirective(String), + + #[error("toc directive arguments 'levels' could not be parsed as an integer: {0}")] + LevelsParse(String, #[source] std::num::ParseIntError), } impl SiteError { diff --git a/src/html.rs b/src/html.rs index 6605333..6d4d009 100644 --- a/src/html.rs +++ b/src/html.rs @@ -236,7 +236,11 @@ impl Element { self.children.push(child); } - fn children(&self) -> &[Content] { + pub fn tag(&self) -> ElementTag { + self.tag + } + + pub fn children(&self) -> &[Content] { &self.children } @@ -306,7 +310,7 @@ impl Element { } } -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum ElementTag { Html, Head, diff --git a/src/page.rs b/src/page.rs index fb2159d..832e71e 100644 --- a/src/page.rs +++ b/src/page.rs @@ -1,3 +1,4 @@ +use crate::directive::{Processed, Toc}; use crate::error::SiteError; use crate::html::{parse, Content, Element, ElementTag, HtmlPage}; use crate::name::Name; @@ -84,10 +85,26 @@ impl UnprocessedPage { pub fn process(&self, site: &mut Site) -> Result { let mut meta = self.meta.clone(); - let mut m = String::new(); + let mut processed = vec![]; trace!("UnprocessedPage: processing snippets"); for snippet in self.snippets.iter() { - m.push_str(&snippet.process(site, &mut meta)?); + processed.push(snippet.process(site, &mut meta)?); + } + let page_text = processed + .iter() + .filter_map(|p| match p { + Processed::Markdown(s) => Some(s.as_str()), + _ => None, + }) + .collect::>() + .join(""); + let body = parse(&page_text)?; + let mut m = String::new(); + for p in processed { + match p { + Processed::Markdown(s) => m.push_str(&s), + Processed::Toc(levels) => m.push_str(&Toc::post_process(&body, levels)), + } } Ok(MarkdownPage::new(m, meta)) } diff --git a/src/wikitext.rs b/src/wikitext.rs index 25da818..b78cd66 100644 --- a/src/wikitext.rs +++ b/src/wikitext.rs @@ -29,34 +29,33 @@ impl Snippet { Ok(()) } - pub fn process(&self, site: &mut Site, meta: &mut PageMeta) -> Result { + pub fn process(&self, site: &mut Site, meta: &mut PageMeta) -> Result { trace!("Snippet::process: self={:?}", self); - let s = match self { - Snippet::Markdown(text) => text.into(), + let processed = match self { + Snippet::Markdown(text) => Processed::Markdown(text.into()), Snippet::WikiLink(w) => { let resolved = site .resolve(meta.path(), Path::new(w.target())) .map_err(|e| SiteError::PageProblem(meta.path().into(), Box::new(e)))?; trace!("resolved {} to {}", w.target(), resolved.display()); - format!("[{}]({})", w.link_text(), resolved.display()) + let link = format!("[{}]({})", w.link_text(), resolved.display()); + Processed::Markdown(link) } Snippet::Directive(p) => { let e = Directive::try_from(p); if let Ok(d) = e { - let processed = d.process(site, meta) - .map_err(|e| SiteError::PageProblem(meta.path().into(), Box::new(e)))?; - match processed { - Processed::Markdown(s) => s, - } + d.process(site, meta) + .map_err(|e| SiteError::PageProblem(meta.path().into(), Box::new(e)))? } else if let Some(shortcut) = site.shortcut(p.name()) { let arg = p.unnamed_args().first().unwrap().to_string(); - format!("[{}]({})", shortcut.desc(&arg), shortcut.url(&arg)) + let link = format!("[{}]({})", shortcut.desc(&arg), shortcut.url(&arg)); + Processed::Markdown(link) } else { return Err(e.unwrap_err()); } } }; - Ok(s) + Ok(processed) } } -- cgit v1.2.1