From febde8d449e0438b8d2a52cb3eeec82f12b435de Mon Sep 17 00:00:00 2001 From: Maciej Jur Date: Sun, 21 Apr 2024 11:45:19 +0200 Subject: [PATCH] render outline --- src/html/home.rs | 3 ++- src/html/post.rs | 25 ++++++++++++++++++++--- src/html/wiki.rs | 7 ++++++- src/main.rs | 37 +++++++++++++++++---------------- src/text/md.rs | 53 ++++++++++++++++++++++++++++++++++++++++++++---- 5 files changed, 98 insertions(+), 27 deletions(-) diff --git a/src/html/home.rs b/src/html/home.rs index 3248de2..20e2975 100644 --- a/src/html/home.rs +++ b/src/html/home.rs @@ -19,9 +19,10 @@ const INTRO: &str = r#" fn intro() -> impl Renderable { + let (_, html) = parse(INTRO); maud!( section .p-card.intro-jp lang="ja-JP" { - (Raw(parse(INTRO))) + (Raw(html)) } ) } diff --git a/src/html/post.rs b/src/html/post.rs index 7b5afee..b3724dc 100644 --- a/src/html/post.rs +++ b/src/html/post.rs @@ -1,9 +1,14 @@ use hypertext::{html_elements, maud_move, GlobalAttributes, Renderable}; use crate::md::Post; use crate::html::page; +use crate::text::md::Outline; -pub fn post<'fm, 'md, 'post, T>(fm: &'fm Post, content: T) -> impl Renderable + 'post +pub fn post<'fm, 'md, 'post, T>( + fm: &'fm Post, + content: T, + outline: Outline, +) -> impl Renderable + 'post where 'fm: 'post, 'md: 'post, @@ -20,8 +25,22 @@ pub fn post<'fm, 'md, 'post, T>(fm: &'fm Post, content: T) -> impl Renderable + label .wiki-aside__slider for="wiki-aside-shown" { img .wiki-icon src="/static/svg/double-arrow.svg" width="24" height="24"; } - // Navigation tree - // + section .link-tree { + h2 .link-tree__heading { + a .link-tree__heading-text href="#top" { "Content" } + } + nav #table-of-contents .link-tree__nav { + ul .link-tree__nav-list { + @for (title, id) in outline.0 { + li .link-tree__nav-list-item { + a .link-tree__nav-list-text.link href=(format!("#{id}")) { + (title) + } + } + } + } + } + } } article .wiki-article /*class:list={classlist)*/ { diff --git a/src/html/wiki.rs b/src/html/wiki.rs index c193905..b3dbee0 100644 --- a/src/html/wiki.rs +++ b/src/html/wiki.rs @@ -1,9 +1,14 @@ use hypertext::{html_elements, maud_move, GlobalAttributes, Renderable}; use crate::md::Wiki; use crate::html::page; +use crate::text::md::Outline; -pub fn wiki<'data, 'html, T>(fm: &'data Wiki, content: T) -> impl Renderable + 'html +pub fn wiki<'data, 'html, T>( + fm: &'data Wiki, + content: T, + outline: Outline, +) -> impl Renderable + 'html where 'data: 'html, T: Renderable + 'data diff --git a/src/main.rs b/src/main.rs index 6f22ee9..271113e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ use grass; use html::LinkableData; use hypertext::{Raw, Renderable}; use once_cell::sync::Lazy; +use text::md::Outline; mod md; mod html; @@ -61,7 +62,7 @@ impl Everything<'_> { trait Transformable { - fn transform<'f, 'm, 'html, T>(&'f self, content: T) -> impl Renderable + 'html + fn transform<'f, 'm, 'html, T>(&'f self, content: T, outline: Outline) -> impl Renderable + 'html where 'f: 'html, 'm: 'html, @@ -69,16 +70,16 @@ trait Transformable { fn as_link(&self, path: String) -> Option; - fn render(data: &str) -> String; + fn render(data: &str) -> (Outline, String); } impl Transformable for md::Post { - fn transform<'f, 'm, 'html, T>(&'f self, content: T) -> impl Renderable + 'html + fn transform<'f, 'm, 'html, T>(&'f self, content: T, outline: Outline) -> impl Renderable + 'html where 'f: 'html, 'm: 'html, T: Renderable + 'm { - html::post(self, content) + html::post(self, content, outline) } fn as_link(&self, path: String) -> Option { @@ -90,13 +91,13 @@ impl Transformable for md::Post { }) } - fn render(data: &str) -> String { + fn render(data: &str) -> (Outline, String) { text::md::parse(data) } } impl Transformable for md::Slide { - fn transform<'f, 'm, 'html, T>(&'f self, content: T) -> impl Renderable + 'html + fn transform<'f, 'm, 'html, T>(&'f self, content: T, _: Outline) -> impl Renderable + 'html where 'f: 'html, 'm: 'html, @@ -113,32 +114,33 @@ impl Transformable for md::Slide { }) } - fn render(data: &str) -> String { - data + fn render(data: &str) -> (Outline, String) { + let html = data .split("\n-----\n") - .map(|chunk| chunk.split("\n---\n").map(text::md::parse).collect::>()) + .map(|chunk| chunk.split("\n---\n").map(text::md::parse).map(|e| e.1).collect::>()) .map(|stack| match stack.len() > 1 { true => format!("
{}
", stack.into_iter().map(|slide| format!("
{slide}
")).collect::()), false => format!("
{}
", stack[0]) }) - .collect::() + .collect::(); + (Outline(vec![]), html) } } impl Transformable for md::Wiki { - fn transform<'f, 'm, 'html, T>(&'f self, content: T) -> impl Renderable + 'html + fn transform<'f, 'm, 'html, T>(&'f self, content: T, outline: Outline) -> impl Renderable + 'html where 'f: 'html, 'm: 'html, T: Renderable + 'm { - html::wiki(self, content) + html::wiki(self, content, outline) } fn as_link(&self, _: String) -> Option { None } - fn render(data: &str) -> String { + fn render(data: &str) -> (Outline, String) { text::md::parse(data) } } @@ -182,9 +184,8 @@ fn transform(meta: gen::Source) -> gen::Asset let link = T::as_link(&fm, Path::new("/").join(&loc).to_str().unwrap().to_owned()); let call = move |_: &Everything| { - let data = T::render(&md); - let data = T::transform(&fm, Raw(data)).render().into(); - data + let (outline, html) = T::render(&md); + T::transform(&fm, Raw(html), outline).render().into() }; gen::Asset { @@ -247,8 +248,8 @@ fn main() { gen::Asset { kind: gen::AssetKind::Html(Box::new(|_| { let data = std::fs::read_to_string("content/index.md").unwrap(); - let data = text::md::parse(&data); - html::home(Raw(data)).render().to_owned().into() + let (outline, html) = text::md::parse(&data); + html::home(Raw(html)).render().to_owned().into() })), out: "index.html".into(), link: None, diff --git a/src/text/md.rs b/src/text/md.rs index 6e56756..c2ee34d 100644 --- a/src/text/md.rs +++ b/src/text/md.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use hypertext::Renderable; use once_cell::sync::Lazy; use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag, TagEnd, TextMergeStream}; @@ -30,10 +32,18 @@ static KATEX_B: Lazy = Lazy::new(|| .unwrap() ); +pub struct Outline(pub Vec<(String, String)>); -pub fn parse(text: &str) -> String { - let stream = Parser::new_ext(text, *OPTS); - let stream = TextMergeStream::new(stream) + +pub fn parse(text: &str) -> (Outline, String) { + let (outline, stream) = { + let stream = Parser::new_ext(text, *OPTS); + let mut stream: Vec<_> = TextMergeStream::new(stream).collect(); + let outline = set_heading_ids(&mut stream); + (outline, stream) + }; + + let stream = stream.into_iter() .map(make_math) .map(make_emoji) .collect::>(); @@ -44,7 +54,42 @@ pub fn parse(text: &str) -> String { let mut html = String::new(); pulldown_cmark::html::push_html(&mut html, stream.into_iter()); - html + + (outline, html) +} + +fn set_heading_ids(events: &mut [Event]) -> Outline { + let mut cnt = HashMap::::new(); + let mut out = Vec::new(); + let mut buf = String::new(); + let mut ptr = None; + + for event in events { + match event { + Event::Start(ref mut tag @ Tag::Heading {..}) => { + ptr = Some(tag); + }, + Event::Text(ref text) if ptr.is_some() => { + buf.push_str(text) + }, + Event::End(TagEnd::Heading(..)) => { + let txt = std::mem::take(&mut buf); + let url = txt.to_lowercase().replace(' ', "-"); + let url = match cnt.get_mut(&url) { + Some(ptr) => { *ptr += 1; format!("{url}-{ptr}") }, + None => { cnt.insert(url.clone(), 0); url }, + }; + match ptr.take().unwrap() { + Tag::Heading { ref mut id, .. } => *id = Some(url.clone().into()), + _ => unreachable!(), + } + out.push((txt, url)); + }, + _ => (), + } + }; + + Outline(out) } fn make_math(event: Event) -> Event {