diff options
| author | Laurenz <laurmaedje@gmail.com> | 2023-05-17 14:38:03 +0200 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2023-05-17 14:38:03 +0200 |
| commit | 42afa410ae561eb5b267080d088bca529a5d0b54 (patch) | |
| tree | bdea7348daef7409490ba542f9bdec5d52732d03 /docs/src/html.rs | |
| parent | 8971588486b6ffa9269344b4bda71de86af9d908 (diff) | |
Better documentation outlines
Diffstat (limited to 'docs/src/html.rs')
| -rw-r--r-- | docs/src/html.rs | 114 |
1 files changed, 104 insertions, 10 deletions
diff --git a/docs/src/html.rs b/docs/src/html.rs index cd47d75b..a9bbfb1c 100644 --- a/docs/src/html.rs +++ b/docs/src/html.rs @@ -3,6 +3,7 @@ use std::ops::Range; use comemo::Prehashed; use md::escape::escape_html; use pulldown_cmark as md; +use typed_arena::Arena; use typst::diag::FileResult; use typst::font::{Font, FontBook}; use typst::geom::{Point, Size}; @@ -22,17 +23,35 @@ pub struct Html { md: String, #[serde(skip)] description: Option<String>, + #[serde(skip)] + outline: Vec<OutlineItem>, } impl Html { /// Create HTML from a raw string. pub fn new(raw: String) -> Self { - Self { md: String::new(), raw, description: None } + Self { + md: String::new(), + raw, + description: None, + outline: vec![], + } } /// Convert markdown to HTML. #[track_caller] pub fn markdown(resolver: &dyn Resolver, md: &str) -> Self { + Self::markdown_with_id_base(resolver, md, "") + } + + /// Convert markdown to HTML, preceding all fragment identifiers with the + /// `id_base`. + #[track_caller] + pub fn markdown_with_id_base( + resolver: &dyn Resolver, + md: &str, + id_base: &str, + ) -> Self { let mut text = md; let mut description = None; let document = YamlFrontMatter::parse::<Metadata>(md); @@ -43,7 +62,8 @@ impl Html { let options = md::Options::ENABLE_TABLES | md::Options::ENABLE_HEADING_ATTRIBUTES; - let mut handler = Handler::new(resolver); + let ids = Arena::new(); + let mut handler = Handler::new(resolver, id_base.into(), &ids); let iter = md::Parser::new_ext(text, options) .filter_map(|mut event| handler.handle(&mut event).then_some(event)); @@ -51,7 +71,12 @@ impl Html { md::html::push_html(&mut raw, iter); raw.truncate(raw.trim_end().len()); - Html { md: text.into(), raw, description } + Html { + md: text.into(), + raw, + description, + outline: handler.outline, + } } /// The raw HTML. @@ -72,6 +97,11 @@ impl Html { s.eat_if("<h1>").then(|| s.eat_until("</h1>")) } + /// The outline of the HTML. + pub fn outline(&self) -> Vec<OutlineItem> { + self.outline.clone() + } + /// The description from the front matter. pub fn description(&self) -> Option<String> { self.description.clone() @@ -93,14 +123,23 @@ struct Metadata { struct Handler<'a> { resolver: &'a dyn Resolver, lang: Option<String>, + outline: Vec<OutlineItem>, + id_base: String, + ids: &'a Arena<String>, } impl<'a> Handler<'a> { - fn new(resolver: &'a dyn Resolver) -> Self { - Self { resolver, lang: None } + fn new(resolver: &'a dyn Resolver, id_base: String, ids: &'a Arena<String>) -> Self { + Self { + resolver, + lang: None, + outline: vec![], + id_base, + ids, + } } - fn handle(&mut self, event: &mut md::Event) -> bool { + fn handle(&mut self, event: &mut md::Event<'a>) -> bool { let lang = self.lang.take(); match event { // Rewrite Markdown images. @@ -117,7 +156,19 @@ impl<'a> Handler<'a> { *html = buf.into(); } - // Rewrite contributor sectinos. + // Register HTML headings for the outline. + md::Event::Start(md::Tag::Heading(level, Some(id), _)) => { + self.handle_heading(id, level); + } + + // Also handle heading closings. + md::Event::End(md::Tag::Heading(level, Some(_), _)) => { + if *level > md::HeadingLevel::H1 && !self.id_base.is_empty() { + nest_heading(level); + } + } + + // Rewrite contributor sections. md::Event::Html(html) if html.starts_with("<contributors") => { let from = html_attr(html, "from").unwrap(); let to = html_attr(html, "to").unwrap(); @@ -184,6 +235,36 @@ impl<'a> Handler<'a> { } } + fn handle_heading(&mut self, id: &mut &'a str, level: &mut md::HeadingLevel) { + if *level == md::HeadingLevel::H1 { + return; + } + + // Special case for things like "v0.3.0". + let name = if id.starts_with('v') && id.contains('.') { + id.to_string() + } else { + id.to_title_case() + }; + + let mut children = &mut self.outline; + let mut depth = *level as usize; + while depth > 2 { + if !children.is_empty() { + children = &mut children.last_mut().unwrap().children; + } + depth -= 1; + } + + // Put base before id. + if !self.id_base.is_empty() { + nest_heading(level); + *id = self.ids.alloc(format!("{}-{id}", self.id_base)).as_str(); + } + + children.push(OutlineItem { id: id.to_string(), name, children: vec![] }); + } + fn handle_link(&self, link: &str) -> Option<String> { if link.starts_with('#') || link.starts_with("http") { return Some(link.into()); @@ -205,6 +286,7 @@ impl<'a> Handler<'a> { "$types" => "/docs/reference/types/", "$type" => "/docs/reference/types/", "$func" => "/docs/reference/", + "$guides" => "/docs/guides/", "$changelog" => "/docs/changelog/", "$community" => "/docs/community/", _ => panic!("unknown link root: {root}"), @@ -216,7 +298,7 @@ impl<'a> Handler<'a> { let ty = parts.next()?; let method = parts.next()?; route.push_str(ty); - route.push_str("/#methods--"); + route.push_str("/#methods-"); route.push_str(method); } else if root == "$func" { let mut parts = rest.split('.'); @@ -237,7 +319,7 @@ impl<'a> Handler<'a> { route.push_str("/#"); route.push_str(info.name); if let Some(param) = param { - route.push_str("-parameters--"); + route.push_str("-parameters-"); route.push_str(param); } else { route.push_str("-summary"); @@ -246,7 +328,7 @@ impl<'a> Handler<'a> { route.push_str(name); route.push('/'); if let Some(param) = param { - route.push_str("#parameters--"); + route.push_str("#parameters-"); route.push_str(param); } } @@ -347,6 +429,18 @@ fn html_attr_range(html: &str, attr: &str) -> Option<Range<usize>> { Some(offset..offset + len) } +/// Increase the nesting level of a Markdown heading. +fn nest_heading(level: &mut md::HeadingLevel) { + *level = match &level { + md::HeadingLevel::H1 => md::HeadingLevel::H2, + md::HeadingLevel::H2 => md::HeadingLevel::H3, + md::HeadingLevel::H3 => md::HeadingLevel::H4, + md::HeadingLevel::H4 => md::HeadingLevel::H5, + md::HeadingLevel::H5 => md::HeadingLevel::H6, + v => **v, + }; +} + /// World for example compilations. struct DocWorld(Source); |
