summaryrefslogtreecommitdiff
path: root/src/doc.rs
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2023-10-21 09:39:58 +0300
committerLars Wirzenius <liw@liw.fi>2023-10-21 13:29:36 +0300
commit4a403caa2792e578374964d9f27fe51e95fa1dcf (patch)
treeb33aa7e9d641fe6c31346c13fe19ebc98242a8d7 /src/doc.rs
parent6250b614fee797aa0eff49568232ec727c6e66b4 (diff)
downloadsubplot-4a403caa2792e578374964d9f27fe51e95fa1dcf.tar.gz
feat: insert a Table of Contents into generated HTML
Signed-off-by: Lars Wirzenius <liw@liw.fi> Sponsored-by: author
Diffstat (limited to 'src/doc.rs')
-rw-r--r--src/doc.rs183
1 files changed, 181 insertions, 2 deletions
diff --git a/src/doc.rs b/src/doc.rs
index e448051..f3f9641 100644
--- a/src/doc.rs
+++ b/src/doc.rs
@@ -1,6 +1,7 @@
use crate::bindings::CaptureType;
use crate::generate_test_program;
use crate::get_basedir_from;
+use crate::html::Attribute;
use crate::html::HtmlPage;
use crate::html::{Content, Element, ElementTag};
use crate::md::Markdown;
@@ -14,6 +15,7 @@ use crate::SubplotError;
use crate::{Metadata, YamlMetadata};
use crate::{Warning, Warnings};
+use std::cmp::Ordering;
use std::collections::HashSet;
use std::default::Default;
use std::fmt::Debug;
@@ -138,30 +140,153 @@ impl Document {
/// Return Document as an HTML page serialized into HTML text
pub fn to_html(&mut self, date: &str) -> Result<String, SubplotError> {
+ const CSS: &str = r#"
+ div.toc ol {
+ list-style-type: none;
+ padding: 0;
+ padding-inline-start: 2ch;
+ }
+ "#;
+
let mut head = Element::new(crate::html::ElementTag::Head);
let mut title = Element::new(crate::html::ElementTag::Title);
title.push_child(crate::html::Content::Text(self.meta().title().into()));
head.push_child(crate::html::Content::Elt(title));
+ let mut css = Element::new(ElementTag::Style);
+ css.push_child(Content::Text(CSS.into()));
+ head.push_child(Content::Elt(css));
+
self.meta.set_date(date.into());
- let mut body = self.typeset_meta();
+ let mut body_content = Element::new(crate::html::ElementTag::Div);
for md in self.markdowns.iter() {
- body.push_child(Content::Elt(md.root_element().clone()));
+ body_content.push_child(Content::Elt(md.root_element().clone()));
}
+
+ let mut body = Element::new(crate::html::ElementTag::Div);
+ body.push_child(Content::Elt(self.typeset_meta()));
+ body.push_child(Content::Elt(self.typeset_toc(&body_content)));
+ body.push_child(Content::Elt(body_content));
+
let page = HtmlPage::new(head, body);
page.serialize().map_err(SubplotError::ParseMarkdown)
}
+ fn typeset_toc(&self, body: &Element) -> Element {
+ let mut toc = Element::new(ElementTag::Div);
+ toc.push_attribute(Attribute::new("class", "toc"));
+
+ let mut heading = Element::new(ElementTag::H1);
+ heading.push_child(Content::Text("Table of Contents".into()));
+ toc.push_child(Content::Elt(heading));
+
+ let heading_elements: Vec<&Element> = crate::md::Markdown::visit(body)
+ .iter()
+ .filter(|e| {
+ matches!(
+ e.tag(),
+ ElementTag::H1
+ | ElementTag::H2
+ | ElementTag::H3
+ | ElementTag::H4
+ | ElementTag::H5
+ | ElementTag::H6
+ )
+ })
+ .cloned()
+ .collect();
+
+ let mut headings = vec![];
+ for e in heading_elements {
+ let id = e
+ .attr("id")
+ .expect("heading has id")
+ .value()
+ .expect("id attribute has value");
+ match e.tag() {
+ ElementTag::H1 => headings.push((1, e.content(), id)),
+ ElementTag::H2 => headings.push((2, e.content(), id)),
+ ElementTag::H3 => headings.push((3, e.content(), id)),
+ ElementTag::H4 => headings.push((4, e.content(), id)),
+ ElementTag::H5 => headings.push((5, e.content(), id)),
+ ElementTag::H6 => headings.push((6, e.content(), id)),
+ _ => (),
+ }
+ }
+
+ let mut stack = vec![];
+ let mut numberer = HeadingNumberer::default();
+ for (level, text, id) in headings {
+ assert!(level >= 1);
+ assert!(level <= 6);
+
+ let mut number = Element::new(ElementTag::Span);
+ number.push_child(Content::Text(numberer.number(level)));
+
+ let mut a = Element::new(ElementTag::A);
+ a.push_attribute(crate::html::Attribute::new("href", &format!("#{}", id)));
+ a.push_child(Content::Elt(number));
+ a.push_child(Content::Text(" ".into()));
+ a.push_child(Content::Text(text));
+
+ let mut li = Element::new(ElementTag::Li);
+ li.push_child(Content::Elt(a));
+
+ match level.cmp(&stack.len()) {
+ Ordering::Equal => (),
+ Ordering::Greater => stack.push(Element::new(ElementTag::Ol)),
+ Ordering::Less => {
+ assert!(!stack.is_empty());
+ let child = stack.pop().unwrap();
+ assert!(child.tag() == ElementTag::Ol);
+ let mut li = Element::new(ElementTag::Li);
+ li.push_child(Content::Elt(child));
+ assert!(!stack.is_empty());
+ let mut parent = stack.pop().unwrap();
+ parent.push_child(Content::Elt(li));
+ stack.push(parent);
+ }
+ }
+
+ assert!(!stack.is_empty());
+ let mut ol = stack.pop().unwrap();
+ ol.push_child(Content::Elt(li));
+ stack.push(ol);
+ }
+
+ while stack.len() > 1 {
+ let child = stack.pop().unwrap();
+ assert!(child.tag() == ElementTag::Ol);
+ let mut li = Element::new(ElementTag::Li);
+ li.push_child(Content::Elt(child));
+
+ let mut parent = stack.pop().unwrap();
+ parent.push_child(Content::Elt(li));
+ stack.push(parent);
+ }
+
+ assert!(stack.len() <= 1);
+ if let Some(ol) = stack.pop() {
+ toc.push_child(Content::Elt(ol));
+ }
+
+ toc
+ }
+
fn typeset_meta(&self) -> Element {
let mut div = Element::new(ElementTag::Div);
+
div.push_child(Content::Elt(Self::title(self.meta.title())));
+
if let Some(authors) = self.meta.authors() {
div.push_child(Content::Elt(Self::authors(authors)));
}
+
if let Some(date) = self.meta.date() {
div.push_child(Content::Elt(Self::date(date)));
}
+
div
}
@@ -609,3 +734,57 @@ impl CodegenOutput {
Self { template, doc }
}
}
+
+#[derive(Debug, Default)]
+struct HeadingNumberer {
+ prev: Vec<usize>,
+}
+
+impl HeadingNumberer {
+ fn number(&mut self, level: usize) -> String {
+ match level.cmp(&self.prev.len()) {
+ Ordering::Equal => {
+ if let Some(n) = self.prev.pop() {
+ self.prev.push(n + 1);
+ } else {
+ self.prev.push(1);
+ }
+ }
+ Ordering::Greater => {
+ self.prev.push(1);
+ }
+ Ordering::Less => {
+ assert!(!self.prev.is_empty());
+ self.prev.pop();
+ if let Some(n) = self.prev.pop() {
+ self.prev.push(n + 1);
+ } else {
+ self.prev.push(1);
+ }
+ }
+ }
+
+ let mut s = String::new();
+ for i in self.prev.iter() {
+ if !s.is_empty() {
+ s.push('.');
+ }
+ s.push_str(&i.to_string());
+ }
+ s
+ }
+}
+
+#[cfg(test)]
+mod test_numberer {
+ use super::HeadingNumberer;
+
+ #[test]
+ fn numbering() {
+ let mut n = HeadingNumberer::default();
+ assert_eq!(n.number(1), "1");
+ assert_eq!(n.number(2), "1.1");
+ assert_eq!(n.number(1), "2");
+ assert_eq!(n.number(2), "2.1");
+ }
+}