diff options
| author | Laurenz <laurmaedje@gmail.com> | 2023-09-11 15:53:20 +0200 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2023-09-11 15:53:20 +0200 |
| commit | 3a979e88da7be6240821ba8acfdd376b7f4e0f09 (patch) | |
| tree | b41a095a11d710f0fc5547e5bd0627deafa02af2 /crates/typst-docs/src/html.rs | |
| parent | 305524d005df075d53575552ee090fb53192a3fe (diff) | |
Docs generation for types
Diffstat (limited to 'crates/typst-docs/src/html.rs')
| -rw-r--r-- | crates/typst-docs/src/html.rs | 238 |
1 files changed, 91 insertions, 147 deletions
diff --git a/crates/typst-docs/src/html.rs b/crates/typst-docs/src/html.rs index bde69d89..c64649e3 100644 --- a/crates/typst-docs/src/html.rs +++ b/crates/typst-docs/src/html.rs @@ -1,17 +1,22 @@ +use std::fmt::{self, Debug, Formatter}; use std::ops::Range; use comemo::Prehashed; +use ecow::EcoString; +use heck::{ToKebabCase, ToTitleCase}; use pulldown_cmark as md; +use serde::{Deserialize, Serialize}; use typed_arena::Arena; -use typst::diag::FileResult; -use typst::eval::{Bytes, Datetime, Tracer}; +use typst::diag::{FileResult, StrResult}; +use typst::eval::{Bytes, Datetime, Library, Tracer}; use typst::font::{Font, FontBook}; -use typst::geom::{Point, Size}; +use typst::geom::{Abs, Point, Size}; use typst::syntax::{FileId, Source, VirtualPath}; use typst::World; +use unscanny::Scanner; use yaml_front_matter::YamlFrontMatter; -use super::*; +use super::{contributors, OutlineItem, Resolver, FILE_DIR, FONTS, LIBRARY}; /// HTML documentation. #[derive(Serialize)] @@ -21,7 +26,7 @@ pub struct Html { #[serde(skip)] md: String, #[serde(skip)] - description: Option<String>, + description: Option<EcoString>, #[serde(skip)] outline: Vec<OutlineItem>, } @@ -39,18 +44,7 @@ impl Html { /// 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 { + pub fn markdown(resolver: &dyn Resolver, md: &str, nesting: Option<usize>) -> Self { let mut text = md; let mut description = None; let document = YamlFrontMatter::parse::<Metadata>(md); @@ -62,9 +56,18 @@ impl Html { let options = md::Options::ENABLE_TABLES | md::Options::ENABLE_HEADING_ATTRIBUTES; 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)); + let mut handler = Handler::new(text, resolver, nesting, &ids); + let mut events = md::Parser::new_ext(text, options).peekable(); + let iter = std::iter::from_fn(|| loop { + let mut event = events.next()?; + handler.peeked = events.peek().and_then(|event| match event { + md::Event::Text(text) => Some(text.clone()), + _ => None, + }); + if handler.handle(&mut event) { + return Some(event); + } + }); let mut raw = String::new(); md::html::push_html(&mut raw, iter); @@ -93,7 +96,11 @@ impl Html { /// Returns `None` if the HTML doesn't start with an `h1` tag. pub fn title(&self) -> Option<&str> { let mut s = Scanner::new(&self.raw); - s.eat_if("<h1>").then(|| s.eat_until("</h1>")) + s.eat_if("<h1").then(|| { + s.eat_until('>'); + s.eat_if('>'); + s.eat_until("</h1>") + }) } /// The outline of the HTML. @@ -102,7 +109,7 @@ impl Html { } /// The description from the front matter. - pub fn description(&self) -> Option<String> { + pub fn description(&self) -> Option<EcoString> { self.description.clone() } } @@ -116,26 +123,35 @@ impl Debug for Html { /// Front matter metadata. #[derive(Deserialize)] struct Metadata { - description: String, + description: EcoString, } struct Handler<'a> { + text: &'a str, resolver: &'a dyn Resolver, - lang: Option<String>, - code: String, + peeked: Option<md::CowStr<'a>>, + lang: Option<EcoString>, + code: EcoString, outline: Vec<OutlineItem>, - id_base: String, + nesting: Option<usize>, ids: &'a Arena<String>, } impl<'a> Handler<'a> { - fn new(resolver: &'a dyn Resolver, id_base: String, ids: &'a Arena<String>) -> Self { + fn new( + text: &'a str, + resolver: &'a dyn Resolver, + nesting: Option<usize>, + ids: &'a Arena<String>, + ) -> Self { Self { + text, resolver, + peeked: None, lang: None, - code: String::new(), + code: EcoString::new(), outline: vec![], - id_base, + nesting, ids, } } @@ -157,15 +173,13 @@ impl<'a> Handler<'a> { } // Register HTML headings for the outline. - md::Event::Start(md::Tag::Heading(level, Some(id), _)) => { + md::Event::Start(md::Tag::Heading(level, 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); - } + md::Event::End(md::Tag::Heading(level, _, _)) => { + nest_heading(level, self.nesting()); } // Rewrite contributor sections. @@ -185,10 +199,10 @@ impl<'a> Handler<'a> { "unsupported link type: {ty:?}", ); - *dest = self - .handle_link(dest) - .unwrap_or_else(|| panic!("invalid link: {dest}")) - .into(); + *dest = match self.handle_link(dest) { + Ok(link) => link.into(), + Err(err) => panic!("invalid link: {dest} ({err})"), + }; } // Inline raw. @@ -208,7 +222,7 @@ impl<'a> Handler<'a> { // Code blocks. md::Event::Start(md::Tag::CodeBlock(md::CodeBlockKind::Fenced(lang))) => { self.lang = Some(lang.as_ref().into()); - self.code = String::new(); + self.code = EcoString::new(); return false; } md::Event::End(md::Tag::CodeBlock(md::CodeBlockKind::Fenced(_))) => { @@ -232,7 +246,7 @@ impl<'a> Handler<'a> { } fn handle_image(&self, link: &str) -> String { - if let Some(file) = FILES.get_file(link) { + if let Some(file) = FILE_DIR.get_file(link) { self.resolver.image(link, file.contents()) } else if let Some(url) = self.resolver.link(link) { url @@ -241,16 +255,35 @@ impl<'a> Handler<'a> { } } - fn handle_heading(&mut self, id: &mut &'a str, level: &mut md::HeadingLevel) { + fn handle_heading( + &mut self, + id_slot: &mut Option<&'a str>, + level: &mut md::HeadingLevel, + ) { + nest_heading(level, self.nesting()); if *level == md::HeadingLevel::H1 { return; } + let default = self.peeked.as_ref().map(|text| text.to_kebab_case()); + let id: &'a str = match (&id_slot, default) { + (Some(id), default) => { + if Some(*id) == default.as_deref() { + eprintln!("heading id #{id} was specified unnecessarily"); + } + id + } + (None, Some(default)) => self.ids.alloc(default).as_str(), + (None, None) => panic!("missing heading id {}", self.text), + }; + + *id_slot = (!id.is_empty()).then_some(id); + // Special case for things like "v0.3.0". let name = if id.starts_with('v') && id.contains('.') { - id.to_string() + id.into() } else { - id.to_title_case() + id.to_title_case().into() }; let mut children = &mut self.outline; @@ -262,106 +295,22 @@ impl<'a> Handler<'a> { 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![] }); + children.push(OutlineItem { id: id.into(), name, children: vec![] }); } - fn handle_link(&self, link: &str) -> Option<String> { - if link.starts_with('#') || link.starts_with("http") { - return Some(link.into()); + fn handle_link(&self, link: &str) -> StrResult<String> { + if let Some(link) = self.resolver.link(link) { + return Ok(link); } - if !link.starts_with('$') { - return self.resolver.link(link); - } - - let root = link.split('/').next()?; - let rest = &link[root.len()..].trim_matches('/'); - let base = match root { - "$tutorial" => "/docs/tutorial/", - "$reference" => "/docs/reference/", - "$category" => "/docs/reference/", - "$syntax" => "/docs/reference/syntax/", - "$styling" => "/docs/reference/styling/", - "$scripting" => "/docs/reference/scripting/", - "$types" => "/docs/reference/types/", - "$type" => "/docs/reference/types/", - "$func" => "/docs/reference/", - "$guides" => "/docs/guides/", - "$packages" => "/docs/packages/", - "$changelog" => "/docs/changelog/", - "$community" => "/docs/community/", - _ => panic!("unknown link root: {root}"), - }; - - let mut route = base.to_string(); - if root == "$type" && rest.contains('.') { - let mut parts = rest.split('.'); - let ty = parts.next()?; - let method = parts.next()?; - route.push_str(ty); - route.push_str("/#methods-"); - route.push_str(method); - } else if root == "$func" { - let mut parts = rest.split('.').peekable(); - let first = parts.peek().copied(); - let mut focus = &LIBRARY.global; - while let Some(m) = first.and_then(|name| module(focus, name).ok()) { - focus = m; - parts.next(); - } - - let name = parts.next()?; - - let value = focus.get(name).ok()?; - let Value::Func(func) = value else { return None }; - let info = func.info()?; - route.push_str(info.category); - route.push('/'); - - if let Some(group) = GROUPS - .iter() - .filter(|_| first == Some("math")) - .find(|group| group.functions.iter().any(|func| func == info.name)) - { - route.push_str(&group.name); - route.push_str("/#"); - route.push_str(info.name); - if let Some(param) = parts.next() { - route.push_str("-parameters-"); - route.push_str(param); - } - } else { - route.push_str(name); - route.push('/'); - if let Some(next) = parts.next() { - if info.params.iter().any(|param| param.name == next) { - route.push_str("#parameters-"); - route.push_str(next); - } else if info.scope.iter().any(|(name, _)| name == next) { - route.push('#'); - route.push_str(info.name); - route.push('-'); - route.push_str(next); - } else { - return None; - } - } - } - } else { - route.push_str(rest); - } + crate::link::resolve(link) + } - if !route.contains('#') && !route.ends_with('/') { - route.push('/'); + fn nesting(&self) -> usize { + match self.nesting { + Some(nesting) => nesting, + None => panic!("headings are not allowed here:\n{}", self.text), } - - Some(route) } } @@ -466,15 +415,10 @@ fn html_attr_range(html: &str, attr: &str) -> Option<Range<usize>> { } /// 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, - }; +fn nest_heading(level: &mut md::HeadingLevel, nesting: usize) { + *level = ((*level as usize) + nesting) + .try_into() + .unwrap_or(md::HeadingLevel::H6); } /// A world for example compilations. @@ -499,7 +443,7 @@ impl World for DocWorld { fn file(&self, id: FileId) -> FileResult<Bytes> { assert!(id.package().is_none()); - Ok(FILES + Ok(FILE_DIR .get_file(id.vpath().as_rootless_path()) .unwrap_or_else(|| panic!("failed to load {:?}", id.vpath())) .contents() |
