From b46b20e693499d1a4f0a432568de568b0ceb2286 Mon Sep 17 00:00:00 2001 From: Mylloon Date: Sun, 16 Jun 2024 15:31:44 +0200 Subject: [PATCH 1/2] wip: quick and dumb implementation of toc --- src/misc/markdown.rs | 105 ++++++++++++++++++++++++++++++++++++++++--- src/routes/blog.rs | 3 +- 2 files changed, 99 insertions(+), 9 deletions(-) diff --git a/src/misc/markdown.rs b/src/misc/markdown.rs index e83848b..d4742dd 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; @@ -19,7 +21,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 @@ -338,10 +340,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); @@ -508,3 +515,87 @@ fn mail_obfuscation(html: &str) -> (String, bool) { (new_html, 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, }, ), -- 2.45.2 From 98fd99f7022e18663bf1f914105e6ee9b8fffdda Mon Sep 17 00:00:00 2001 From: Mylloon Date: Sun, 16 Jun 2024 15:58:21 +0200 Subject: [PATCH 2/2] Respect toc attribute in metadata --- src/misc/markdown.rs | 25 ++++++++++----------- src/misc/utils.rs | 1 + src/routes/blog.rs | 47 ++++++++++++---------------------------- templates/blog/post.html | 4 +++- 4 files changed, 30 insertions(+), 47 deletions(-) diff --git a/src/misc/markdown.rs b/src/misc/markdown.rs index d4742dd..c1ec17a 100644 --- a/src/misc/markdown.rs +++ b/src/misc/markdown.rs @@ -21,7 +21,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 @@ -119,6 +119,7 @@ impl Metadata { pub struct File { pub metadata: Metadata, pub content: String, + pub toc_data: String, } /// Options used for parser and compiler MD --> HTML @@ -314,6 +315,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), @@ -326,6 +329,7 @@ pub fn read_md( File { metadata: final_metadata, content: html_content, + toc_data: toc, } } @@ -340,15 +344,10 @@ 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 => { - let mut metadata: FileMetadataBlog = deserialize_metadata(text); - metadata.toc = toc_to_html(&generate_toc(root)); - - FileMetadata { - blog: Some(metadata), - ..FileMetadata::default() - } - } + TypeFileMetadata::Blog => FileMetadata { + blog: Some(deserialize_metadata(text)), + ..FileMetadata::default() + }, TypeFileMetadata::Contact => { let mut metadata: FileMetadataContact = deserialize_metadata(text); @@ -575,9 +574,9 @@ fn generate_toc<'a>(root: &'a AstNode<'a>) -> Vec { toc } -fn toc_to_html(toc: &[TOCEntry]) -> Option { +fn toc_to_html(toc: &[TOCEntry]) -> String { if toc.is_empty() { - return None; + return String::new(); } let mut html = Vec::with_capacity(20 + 20 * toc.len()); @@ -597,5 +596,5 @@ fn toc_to_html(toc: &[TOCEntry]) -> Option { html.extend_from_slice(b""); - Some(String::from_utf8(html).unwrap()) + 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 a97a2c7..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,27 +242,20 @@ fn get_post( Some(tags) => tags.clone(), None => default.2, }, - match &data.metadata.info.blog.as_ref().unwrap().toc { - Some(toc) => toc.into(), - _ => 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}}
-- 2.45.2