summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2022-11-09 15:56:55 +0200
committerLars Wirzenius <liw@liw.fi>2022-11-09 15:56:55 +0200
commit8f17b2a721d2369691ff2e3d4950a436aa9533ab (patch)
tree1041e81264145d66a5b168b82c546234b36d80cc
parent249323a6009a35dd2e77be794ed45b5f1cf4263f (diff)
downloadriki-8f17b2a721d2369691ff2e3d4950a436aa9533ab.tar.gz
feat: implement rudimentary table of contents, without links
The links will be added later. Sponsored-by: author
-rw-r--r--riki.md20
-rw-r--r--src/directive/mod.rs3
-rw-r--r--src/directive/toc.rs72
-rw-r--r--src/error.rs3
-rw-r--r--src/html.rs8
-rw-r--r--src/page.rs21
-rw-r--r--src/wikitext.rs21
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 "<li>Introduction</li>"
+then file output/index.html contains "<li>Acknowledgements</li>"
+~~~
+
+~~~{#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<Processed, SiteError> {
- 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("<ol>");
+ } else if *level < prev_level {
+ html.push_str("</ol>\n");
+ }
+ html.push_str("<li>");
+ Self::stringify(&mut html, text);
+ html.push_str("</li>\n");
+ prev_level = *level;
+ }
+ for _ in 0..prev_level {
+ html.push_str("</ol>\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<MarkdownPage, SiteError> {
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::<Vec<&str>>()
+ .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<String, SiteError> {
+ pub fn process(&self, site: &mut Site, meta: &mut PageMeta) -> Result<Processed, SiteError> {
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)
}
}