diff --git a/src/misc/markdown.rs b/src/misc/markdown.rs index df3da6e..eee488b 100644 --- a/src/misc/markdown.rs +++ b/src/misc/markdown.rs @@ -1,8 +1,10 @@ use crate::misc::date::Date; use base64::engine::general_purpose; use base64::Engine; -use comrak::nodes::{AstNode, NodeValue}; -use comrak::{format_html, parse_document, Arena, ComrakOptions, ListStyleType, Options}; +use comrak::nodes::{AstNode, NodeCode, NodeMath, NodeValue}; +use comrak::{ + format_html, parse_document, Anchorizer, Arena, ComrakOptions, ListStyleType, Options, +}; use lol_html::html_content::ContentType; use lol_html::{element, rewrite_str, HtmlRewriter, RewriteStrSettings, Settings}; use ramhorns::Content; @@ -21,7 +23,7 @@ pub struct FileMetadataBlog { pub description: Option, pub publish: Option, pub tags: Option>, - pub toc: Option, + pub toc: Option, } /// A tag, related to post blog @@ -316,10 +318,15 @@ pub fn get_metadata<'a>(root: &'a AstNode<'a>, mtype: &TypeFileMetadata) -> File .find_map(|node| match &node.data.borrow().value { // Extract metadata from frontmatter NodeValue::FrontMatter(text) => Some(match mtype { - TypeFileMetadata::Blog => FileMetadata { - blog: Some(deserialize_metadata(text)), - ..FileMetadata::default() - }, + TypeFileMetadata::Blog => { + let mut metadata: FileMetadataBlog = deserialize_metadata(text); + metadata.toc = toc_to_html(&generate_toc(root)); + + FileMetadata { + blog: Some(metadata), + ..FileMetadata::default() + } + } TypeFileMetadata::Contact => { let mut metadata: FileMetadataContact = deserialize_metadata(text); @@ -488,3 +495,87 @@ fn mail_obfuscation(html: &str) -> (String, bool) { (new_html, is_modified) } } + +#[derive(Debug)] +struct TOCEntry { + id: String, + title: String, + depth: u8, +} + +fn generate_toc<'a>(root: &'a AstNode<'a>) -> Vec { + /// See + fn collect_text<'a>(node: &'a AstNode<'a>, output: &mut String) { + match node.data.borrow().value { + NodeValue::Text(ref literal) + | NodeValue::Code(NodeCode { ref literal, .. }) + | NodeValue::Math(NodeMath { ref literal, .. }) => { + *output = literal.to_string(); + } + _ => { + for n in node.children() { + if !output.is_empty() { + break; + } + + collect_text(n, output); + } + } + } + } + + let mut toc = vec![]; + + let mut anchorizer = Anchorizer::new(); + + // Collect headings first to avoid mutable borrow conflicts + let headings: Vec<_> = root + .children() + .filter_map(|node| { + if let NodeValue::Heading(ref nch) = &node.data.borrow().value { + Some((*nch, node)) + } else { + None + } + }) + .collect(); + + // Now process each heading + for (nch, node) in headings { + let mut title = String::with_capacity(20); + collect_text(node, &mut title); + + toc.push(TOCEntry { + id: anchorizer.anchorize(title.clone()), + title, + depth: nch.level, + }); + } + + toc +} + +fn toc_to_html(toc: &[TOCEntry]) -> Option { + if toc.is_empty() { + return None; + } + + let mut html = Vec::with_capacity(20 + 20 * toc.len()); + + html.extend_from_slice(b"
    "); + + for entry in toc { + // TODO: Use depth + html.extend_from_slice( + format!( + "
  • {} (dbg/depth/{})
  • ", + entry.id, entry.title, entry.depth + ) + .as_bytes(), + ); + } + + html.extend_from_slice(b"
"); + + Some(String::from_utf8(html).unwrap()) +} diff --git a/src/routes/blog.rs b/src/routes/blog.rs index 252dc72..a97a2c7 100644 --- a/src/routes/blog.rs +++ b/src/routes/blog.rs @@ -255,8 +255,7 @@ fn get_post( None => default.2, }, match &data.metadata.info.blog.as_ref().unwrap().toc { - // TODO: Generate TOC - Some(true) => String::new(), + Some(toc) => toc.into(), _ => default.3, }, ),