diff --git a/src/misc/markdown.rs b/src/misc/markdown.rs index df3da6e..1a0b0c5 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; @@ -119,6 +121,7 @@ impl Metadata { pub struct File { pub metadata: Metadata, pub content: String, + pub toc_data: String, } /// Options used for parser and compiler MD --> HTML @@ -290,6 +293,8 @@ pub fn read_md( html_content = custom_img_size(&html_content); (html_content, mail_obfsucated) = mail_obfuscation(&html_content); + let toc = toc_to_html(&generate_toc(root)); + let mut final_metadata = Metadata { info: metadata, mermaid: check_mermaid(root, mermaid_name), @@ -302,6 +307,7 @@ pub fn read_md( File { metadata: final_metadata, content: html_content, + toc_data: toc, } } @@ -488,3 +494,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]) -> String { + if toc.is_empty() { + return String::new(); + } + + let mut html = Vec::with_capacity(20 + 20 * toc.len()); + + html.extend_from_slice(b""); + + String::from_utf8(html).unwrap() +} diff --git a/src/misc/utils.rs b/src/misc/utils.rs index 96699d3..0607fda 100644 --- a/src/misc/utils.rs +++ b/src/misc/utils.rs @@ -81,5 +81,6 @@ fn read_pdf(data: Vec) -> File { style="width: 100%; height: 79vh"; >"# ), + toc_data: String::new(), } } diff --git a/src/routes/blog.rs b/src/routes/blog.rs index 252dc72..9523f70 100644 --- a/src/routes/blog.rs +++ b/src/routes/blog.rs @@ -186,7 +186,6 @@ fn get_posts(location: &str) -> Vec { struct BlogPostTemplate { navbar: NavBar, post: Option, - toc: String, } #[get("/blog/p/{id}")] @@ -199,7 +198,7 @@ pub async fn page(path: web::Path<(String,)>, config: web::Data) -> impl fn build_post(file: &str, config: Config) -> String { let mut post = None; - let (infos, toc) = get_post( + let infos = get_post( &mut post, file, &config.fc.name.unwrap_or_default(), @@ -214,18 +213,12 @@ fn build_post(file: &str, config: Config) -> String { ..NavBar::default() }, post, - toc, }, infos, ) } -fn get_post( - post: &mut Option, - filename: &str, - name: &str, - data_dir: &str, -) -> (InfosPage, String) { +fn get_post(post: &mut Option, filename: &str, name: &str, data_dir: &str) -> InfosPage { let blog_dir = format!("{data_dir}/{BLOG_DIR}/{POST_DIR}"); let ext = ".md"; @@ -234,13 +227,8 @@ fn get_post( &TypeFileMetadata::Blog, ); - let default = ( - filename, - &format!("Blog d'{name}"), - Vec::new(), - String::new(), - ); - let (title, desc, tags, toc) = match post { + let default = (filename, &format!("Blog d'{name}"), Vec::new()); + let (title, desc, tags) = match post { Some(data) => ( match &data.metadata.info.blog.as_ref().unwrap().title { Some(text) => text, @@ -254,28 +242,20 @@ fn get_post( Some(tags) => tags.clone(), None => default.2, }, - match &data.metadata.info.blog.as_ref().unwrap().toc { - // TODO: Generate TOC - Some(true) => String::new(), - _ => default.3, - }, ), None => default, }; - ( - InfosPage { - title: Some(format!("Post: {title}")), - desc: Some(desc.clone()), - kw: Some(make_kw( - &["blog", "blogging", "write", "writing"] - .into_iter() - .chain(tags.iter().map(|t| t.name.as_str())) - .collect::>(), - )), - }, - toc, - ) + InfosPage { + title: Some(format!("Post: {title}")), + desc: Some(desc.clone()), + kw: Some(make_kw( + &["blog", "blogging", "write", "writing"] + .into_iter() + .chain(tags.iter().map(|t| t.name.as_str())) + .collect::>(), + )), + } } #[routes] diff --git a/templates/blog/post.html b/templates/blog/post.html index bf48205..9264c8b 100644 --- a/templates/blog/post.html +++ b/templates/blog/post.html @@ -28,7 +28,9 @@
{{^post}}

This post doesn't exist... sorry

- {{/post}} {{#post}} {{&toc}} + {{/post}} {{#post}} {{#metadata}} {{#info}} {{#blog}} {{#toc}} + + {{/toc}} {{/blog}} {{/info}} {{/metadata}}
{{&content}}
{{/post}}