diff options
| author | Laurenz <laurmaedje@gmail.com> | 2024-02-28 11:37:52 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-02-28 10:37:52 +0000 |
| commit | a518e2dd4d829b45b0887da28acb77d0568894ab (patch) | |
| tree | e7387b27e299383a0bd1a26ad58f485630cb23b7 /crates/typst-docs | |
| parent | e16d3f5a67a31154797b4d56cdc6ed142ee2a7cf (diff) | |
Move docs generation code (#3519)
Diffstat (limited to 'crates/typst-docs')
| -rw-r--r-- | crates/typst-docs/Cargo.toml | 42 | ||||
| -rw-r--r-- | crates/typst-docs/src/contribs.rs | 86 | ||||
| -rw-r--r-- | crates/typst-docs/src/html.rs | 469 | ||||
| -rw-r--r-- | crates/typst-docs/src/lib.rs | 840 | ||||
| -rw-r--r-- | crates/typst-docs/src/link.rs | 108 | ||||
| -rw-r--r-- | crates/typst-docs/src/main.rs | 148 | ||||
| -rw-r--r-- | crates/typst-docs/src/model.rs | 173 |
7 files changed, 0 insertions, 1866 deletions
diff --git a/crates/typst-docs/Cargo.toml b/crates/typst-docs/Cargo.toml deleted file mode 100644 index af661bfb..00000000 --- a/crates/typst-docs/Cargo.toml +++ /dev/null @@ -1,42 +0,0 @@ -[package] -name = "typst-docs" -version = { workspace = true } -rust-version = { workspace = true } -authors = { workspace = true } -edition = { workspace = true } -publish = false - -[lib] -doctest = false -bench = false - -[[bin]] -name = "typst-docs" -required-features = ["cli"] - -[features] -cli = ["clap", "typst-render", "serde_json"] - -[dependencies] -typst = { workspace = true } -typst-assets = { workspace = true, features = ["fonts"] } -typst-dev-assets = { workspace = true } -comemo = { workspace = true } -ecow = { workspace = true } -heck = { workspace = true } -include_dir = { workspace = true } -once_cell = { workspace = true } -pulldown-cmark = { workspace = true } -serde = { workspace = true } -serde_yaml = { workspace = true } -syntect = { workspace = true, features = ["html"] } -typed-arena = { workspace = true } -unicode_names2 = { workspace = true } -unscanny = { workspace = true } -yaml-front-matter = { workspace = true } -clap = { workspace = true, optional = true } -typst-render = { workspace = true, optional = true } -serde_json = { workspace = true, optional = true } - -[lints] -workspace = true diff --git a/crates/typst-docs/src/contribs.rs b/crates/typst-docs/src/contribs.rs deleted file mode 100644 index 58a730e2..00000000 --- a/crates/typst-docs/src/contribs.rs +++ /dev/null @@ -1,86 +0,0 @@ -use std::cmp::Reverse; -use std::collections::HashMap; -use std::fmt::Write; - -use serde::{Deserialize, Serialize}; - -use crate::{Html, Resolver}; - -/// Build HTML detailing the contributors between two tags. -pub fn contributors(resolver: &dyn Resolver, from: &str, to: &str) -> Option<Html> { - let staff = ["laurmaedje", "reknih"]; - - // Determine number of contributions per person. - let mut contributors = HashMap::<String, Contributor>::new(); - for commit in resolver.commits(from, to) { - contributors - .entry(commit.author.login.clone()) - .or_insert_with(|| Contributor { - login: commit.author.login, - avatar: commit.author.avatar_url, - contributions: 0, - }) - .contributions += 1; - } - - // Keep only non-staff people. - let mut contributors: Vec<_> = contributors - .into_values() - .filter(|c| !staff.contains(&c.login.as_str())) - .collect(); - - // Sort by highest number of commits. - contributors.sort_by_key(|c| (Reverse(c.contributions), c.login.clone())); - if contributors.is_empty() { - return None; - } - - let mut html = "Thanks to everyone who contributed to this release!".to_string(); - html += "<ul class=\"contribs\">"; - - for Contributor { login, avatar, contributions } in contributors { - let login = login.replace('\"', """).replace('&', "&"); - let avatar = avatar.replace("?v=", "?s=64&v="); - let s = if contributions > 1 { "s" } else { "" }; - write!( - html, - r#"<li> - <a href="https://github.com/{login}" target="_blank"> - <img - width="64" - height="64" - src="{avatar}" - alt="GitHub avatar of {login}" - title="@{login} made {contributions} contribution{s}" - crossorigin="anonymous" - > - </a> - </li>"# - ) - .unwrap(); - } - - html += "</ul>"; - - Some(Html::new(html)) -} - -#[derive(Debug)] -struct Contributor { - login: String, - avatar: String, - contributions: usize, -} - -/// A commit on the `typst` repository. -#[derive(Debug, Serialize, Deserialize)] -pub struct Commit { - author: Author, -} - -/// A commit author. -#[derive(Debug, Serialize, Deserialize)] -pub struct Author { - login: String, - avatar_url: String, -} diff --git a/crates/typst-docs/src/html.rs b/crates/typst-docs/src/html.rs deleted file mode 100644 index 6c47b8df..00000000 --- a/crates/typst-docs/src/html.rs +++ /dev/null @@ -1,469 +0,0 @@ -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, StrResult}; -use typst::eval::Tracer; -use typst::foundations::{Bytes, Datetime}; -use typst::layout::{Abs, Point, Size}; -use typst::syntax::{FileId, Source, VirtualPath}; -use typst::text::{Font, FontBook}; -use typst::{Library, World}; -use unscanny::Scanner; -use yaml_front_matter::YamlFrontMatter; - -use crate::{contributors, OutlineItem, Resolver, FONTS, LIBRARY}; - -/// HTML documentation. -#[derive(Serialize)] -#[serde(transparent)] -pub struct Html { - raw: String, - #[serde(skip)] - md: String, - #[serde(skip)] - description: Option<EcoString>, - #[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, - outline: vec![], - } - } - - /// Convert markdown to HTML. - #[track_caller] - 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); - if let Ok(document) = &document { - text = &document.content; - description = Some(document.metadata.description.clone()) - } - - let options = md::Options::ENABLE_TABLES - | md::Options::ENABLE_FOOTNOTES - | md::Options::ENABLE_STRIKETHROUGH - | md::Options::ENABLE_HEADING_ATTRIBUTES; - - let ids = Arena::new(); - 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); - raw.truncate(raw.trim_end().len()); - - Html { - md: text.into(), - raw, - description, - outline: handler.outline, - } - } - - /// The raw HTML. - pub fn as_str(&self) -> &str { - &self.raw - } - - /// The original Markdown, if any. - pub fn md(&self) -> &str { - &self.md - } - - /// The title of the 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('>'); - s.eat_if('>'); - 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<EcoString> { - self.description.clone() - } -} - -impl Debug for Html { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "Html({:?})", self.title().unwrap_or("..")) - } -} - -/// Front matter metadata. -#[derive(Deserialize)] -struct Metadata { - description: EcoString, -} - -struct Handler<'a> { - text: &'a str, - resolver: &'a dyn Resolver, - peeked: Option<md::CowStr<'a>>, - lang: Option<EcoString>, - code: EcoString, - outline: Vec<OutlineItem>, - nesting: Option<usize>, - ids: &'a Arena<String>, -} - -impl<'a> Handler<'a> { - 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: EcoString::new(), - outline: vec![], - nesting, - ids, - } - } - - fn handle(&mut self, event: &mut md::Event<'a>) -> bool { - match event { - // Rewrite Markdown images. - md::Event::Start(md::Tag::Image(_, path, _)) => { - *path = self.handle_image(path).into(); - } - - // Rewrite HTML images. - md::Event::Html(html) if html.starts_with("<img") => { - let range = html_attr_range(html, "src").unwrap(); - let path = &html[range.clone()]; - let mut buf = html.to_string(); - buf.replace_range(range, &self.handle_image(path)); - *html = buf.into(); - } - - // Register HTML headings for the outline. - md::Event::Start(md::Tag::Heading(level, id, _)) => { - self.handle_heading(id, level); - } - - // Also handle heading closings. - md::Event::End(md::Tag::Heading(level, _, _)) => { - nest_heading(level, self.nesting()); - } - - // 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(); - let Some(output) = contributors(self.resolver, from, to) else { - return false; - }; - *html = output.raw.into(); - } - - // Rewrite links. - md::Event::Start(md::Tag::Link(ty, dest, _)) => { - assert!( - matches!(ty, md::LinkType::Inline | md::LinkType::Reference), - "unsupported link type: {ty:?}", - ); - - *dest = match self.handle_link(dest) { - Ok(link) => link.into(), - Err(err) => panic!("invalid link: {dest} ({err})"), - }; - } - - // Inline raw. - md::Event::Code(code) => { - let mut chars = code.chars(); - let parser = match (chars.next(), chars.next_back()) { - (Some('['), Some(']')) => typst::syntax::parse, - (Some('{'), Some('}')) => typst::syntax::parse_code, - _ => return true, - }; - - let root = parser(&code[1..code.len() - 1]); - let html = typst::syntax::highlight_html(&root); - *event = md::Event::Html(html.into()); - } - - // Code blocks. - md::Event::Start(md::Tag::CodeBlock(md::CodeBlockKind::Fenced(lang))) => { - self.lang = Some(lang.as_ref().into()); - self.code = EcoString::new(); - return false; - } - md::Event::End(md::Tag::CodeBlock(md::CodeBlockKind::Fenced(_))) => { - let Some(lang) = self.lang.take() else { return false }; - let html = code_block(self.resolver, &lang, &self.code); - *event = md::Event::Html(html.raw.into()); - } - - // Example with preview. - md::Event::Text(text) => { - if self.lang.is_some() { - self.code.push_str(text); - return false; - } - } - - _ => {} - } - - true - } - - fn handle_image(&self, link: &str) -> String { - if let Some(data) = typst_dev_assets::get(link) { - self.resolver.image(link, data) - } else if let Some(url) = self.resolver.link(link) { - url - } else { - panic!("missing image: {link}") - } - } - - 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.into() - } else { - id.to_title_case().into() - }; - - 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; - } - - children.push(OutlineItem { id: id.into(), name, children: vec![] }); - } - - fn handle_link(&self, link: &str) -> StrResult<String> { - if let Some(link) = self.resolver.link(link) { - return Ok(link); - } - - crate::link::resolve(link, self.resolver.base()) - } - - fn nesting(&self) -> usize { - match self.nesting { - Some(nesting) => nesting, - None => panic!("headings are not allowed here:\n{}", self.text), - } - } -} - -/// Render a code block to HTML. -fn code_block(resolver: &dyn Resolver, lang: &str, text: &str) -> Html { - let mut display = String::new(); - let mut compile = String::new(); - for line in text.lines() { - if let Some(suffix) = line.strip_prefix(">>>") { - compile.push_str(suffix); - compile.push('\n'); - } else if let Some(suffix) = line.strip_prefix("<<< ") { - display.push_str(suffix); - display.push('\n'); - } else { - display.push_str(line); - display.push('\n'); - compile.push_str(line); - compile.push('\n'); - } - } - - let mut parts = lang.split(':'); - let lang = parts.next().unwrap_or(lang); - - let mut zoom: Option<[Abs; 4]> = None; - let mut single = false; - if let Some(args) = parts.next() { - single = true; - if !args.contains("single") { - zoom = args - .split(',') - .take(4) - .map(|s| Abs::pt(s.parse().unwrap())) - .collect::<Vec<_>>() - .try_into() - .ok(); - } - } - - if lang.is_empty() { - let mut buf = String::from("<pre>"); - md::escape::escape_html(&mut buf, &display).unwrap(); - buf.push_str("</pre>"); - return Html::new(buf); - } else if !matches!(lang, "example" | "typ" | "preview") { - let set = &*typst::text::RAW_SYNTAXES; - let buf = syntect::html::highlighted_html_for_string( - &display, - set, - set.find_syntax_by_token(lang) - .unwrap_or_else(|| panic!("unsupported highlighting language: {lang}")), - &typst::text::RAW_THEME, - ) - .expect("failed to highlight code"); - return Html::new(buf); - } - - let mut highlighted = None; - if matches!(lang, "example" | "typ") { - let root = typst::syntax::parse(&display); - let html = Html::new(typst::syntax::highlight_html(&root)); - if lang == "typ" { - return Html::new(format!("<pre>{}</pre>", html.as_str())); - } - highlighted = Some(html); - } - - let id = FileId::new(None, VirtualPath::new("main.typ")); - let source = Source::new(id, compile); - let world = DocWorld(source); - - let mut tracer = Tracer::new(); - let mut document = match typst::compile(&world, &mut tracer) { - Ok(doc) => doc, - Err(err) => { - let msg = &err[0].message; - panic!("while trying to compile:\n{text}:\n\nerror: {msg}"); - } - }; - - if let Some([x, y, w, h]) = zoom { - document.pages[0].frame.translate(Point::new(-x, -y)); - *document.pages[0].frame.size_mut() = Size::new(w, h); - } - - if single { - document.pages.truncate(1); - } - - let hash = typst::util::hash128(text); - resolver.example(hash, highlighted, &document) -} - -/// Extract an attribute value from an HTML element. -fn html_attr<'a>(html: &'a str, attr: &str) -> Option<&'a str> { - html.get(html_attr_range(html, attr)?) -} - -/// Extract the range of the attribute value of an HTML element. -fn html_attr_range(html: &str, attr: &str) -> Option<Range<usize>> { - let needle = format!("{attr}=\""); - let offset = html.find(&needle)? + needle.len(); - let len = html[offset..].find('"')?; - Some(offset..offset + len) -} - -/// Increase the nesting level of a Markdown heading. -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. -struct DocWorld(Source); - -impl World for DocWorld { - fn library(&self) -> &Prehashed<Library> { - &LIBRARY - } - - fn book(&self) -> &Prehashed<FontBook> { - &FONTS.0 - } - - fn main(&self) -> Source { - self.0.clone() - } - - fn source(&self, _: FileId) -> FileResult<Source> { - Ok(self.0.clone()) - } - - fn file(&self, id: FileId) -> FileResult<Bytes> { - assert!(id.package().is_none()); - Ok(Bytes::from_static( - typst_dev_assets::get_by_name( - &id.vpath().as_rootless_path().to_string_lossy(), - ) - .unwrap_or_else(|| panic!("failed to load {:?}", id.vpath())), - )) - } - - fn font(&self, index: usize) -> Option<Font> { - Some(FONTS.1[index].clone()) - } - - fn today(&self, _: Option<i64>) -> Option<Datetime> { - Some(Datetime::from_ymd(1970, 1, 1).unwrap()) - } -} diff --git a/crates/typst-docs/src/lib.rs b/crates/typst-docs/src/lib.rs deleted file mode 100644 index 5439ad1b..00000000 --- a/crates/typst-docs/src/lib.rs +++ /dev/null @@ -1,840 +0,0 @@ -//! Documentation provider for Typst. - -mod contribs; -mod html; -mod link; -mod model; - -pub use self::contribs::*; -pub use self::html::*; -pub use self::model::*; - -use std::path::Path; - -use comemo::Prehashed; -use ecow::{eco_format, EcoString}; -use heck::ToTitleCase; -use include_dir::{include_dir, Dir}; -use once_cell::sync::Lazy; -use serde::de::DeserializeOwned; -use serde::Deserialize; -use serde_yaml as yaml; -use typst::diag::{bail, StrResult}; -use typst::foundations::Bytes; -use typst::foundations::{ - CastInfo, Category, Func, Module, ParamInfo, Repr, Scope, Smart, Type, Value, - FOUNDATIONS, -}; -use typst::introspection::INTROSPECTION; -use typst::layout::{Abs, Margin, PageElem, LAYOUT}; -use typst::loading::DATA_LOADING; -use typst::math::MATH; -use typst::model::Document; -use typst::model::MODEL; -use typst::symbols::SYMBOLS; -use typst::text::{Font, FontBook, TEXT}; -use typst::visualize::VISUALIZE; -use typst::Library; - -static DOCS_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../../docs"); - -static GROUPS: Lazy<Vec<GroupData>> = Lazy::new(|| { - let mut groups: Vec<GroupData> = yaml("reference/groups.yml"); - for group in &mut groups { - if group.filter.is_empty() { - group.filter = group - .module() - .scope() - .iter() - .filter(|(_, v)| matches!(v, Value::Func(_))) - .map(|(k, _)| k.clone()) - .collect(); - } - } - groups -}); - -static LIBRARY: Lazy<Prehashed<Library>> = Lazy::new(|| { - let mut lib = Library::default(); - lib.styles - .set(PageElem::set_width(Smart::Custom(Abs::pt(240.0).into()))); - lib.styles.set(PageElem::set_height(Smart::Auto)); - lib.styles.set(PageElem::set_margin(Margin::splat(Some(Smart::Custom( - Abs::pt(15.0).into(), - ))))); - Prehashed::new(lib) -}); - -static FONTS: Lazy<(Prehashed<FontBook>, Vec<Font>)> = Lazy::new(|| { - let fonts: Vec<_> = typst_assets::fonts() - .chain(typst_dev_assets::fonts()) - .flat_map(|data| Font::iter(Bytes::from_static(data))) - .collect(); - let book = FontBook::from_fonts(&fonts); - (Prehashed::new(book), fonts) -}); - -/// Build documentation pages. -pub fn provide(resolver: &dyn Resolver) -> Vec<PageModel> { - vec![ - markdown_page(resolver, resolver.base(), "overview.md") - .with_route(resolver.base()), - tutorial_pages(resolver), - reference_pages(resolver), - guide_pages(resolver), - packages_page(resolver), - markdown_page(resolver, resolver.base(), "changelog.md"), - markdown_page(resolver, resolver.base(), "roadmap.md"), - markdown_page(resolver, resolver.base(), "community.md"), - ] -} - -/// Resolve consumer dependencies. -pub trait Resolver { - /// Try to resolve a link that the system cannot resolve itself. - fn link(&self, link: &str) -> Option<String>; - - /// Produce an URL for an image file. - fn image(&self, filename: &str, data: &[u8]) -> String; - - /// Produce HTML for an example. - fn example(&self, hash: u128, source: Option<Html>, document: &Document) -> Html; - - /// Determine the commits between two tags. - fn commits(&self, from: &str, to: &str) -> Vec<Commit>; - - /// Get the base URL for the routes and links. This must end with a slash. - fn base(&self) -> &str; -} - -/// Create a page from a markdown file. -#[track_caller] -fn markdown_page( - resolver: &dyn Resolver, - parent: &str, - path: impl AsRef<Path>, -) -> PageModel { - assert!(parent.starts_with('/') && parent.ends_with('/')); - let md = DOCS_DIR.get_file(path).unwrap().contents_utf8().unwrap(); - let html = Html::markdown(resolver, md, Some(0)); - let title: EcoString = html.title().expect("chapter lacks a title").into(); - PageModel { - route: eco_format!("{parent}{}/", urlify(&title)), - title, - description: html.description().unwrap(), - part: None, - outline: html.outline(), - body: BodyModel::Html(html), - children: vec![], - } -} - -/// Build the tutorial. -fn tutorial_pages(resolver: &dyn Resolver) -> PageModel { - let mut page = markdown_page(resolver, resolver.base(), "tutorial/welcome.md"); - page.children = DOCS_DIR - .get_dir("tutorial") - .unwrap() - .files() - .filter(|file| file.path() != Path::new("tutorial/welcome.md")) - .map(|file| { - markdown_page(resolver, &format!("{}tutorial/", resolver.base()), file.path()) - }) - .collect(); - page -} - -/// Build the reference. -fn reference_pages(resolver: &dyn Resolver) -> PageModel { - let mut page = markdown_page(resolver, resolver.base(), "reference/welcome.md"); - page.children = vec![ - markdown_page( - resolver, - &format!("{}reference/", resolver.base()), - "reference/syntax.md", - ) - .with_part("Language"), - markdown_page( - resolver, - &format!("{}reference/", resolver.base()), - "reference/styling.md", - ), - markdown_page( - resolver, - &format!("{}reference/", resolver.base()), - "reference/scripting.md", - ), - markdown_page( - resolver, - &format!("{}reference/", resolver.base()), - "reference/context.md", - ), - category_page(resolver, FOUNDATIONS).with_part("Library"), - category_page(resolver, MODEL), - category_page(resolver, TEXT), - category_page(resolver, MATH), - category_page(resolver, SYMBOLS), - category_page(resolver, LAYOUT), - category_page(resolver, VISUALIZE), - category_page(resolver, INTROSPECTION), - category_page(resolver, DATA_LOADING), - ]; - page -} - -/// Build the guides section. -fn guide_pages(resolver: &dyn Resolver) -> PageModel { - let mut page = markdown_page(resolver, resolver.base(), "guides/welcome.md"); - page.children = vec![ - markdown_page( - resolver, - &format!("{}guides/", resolver.base()), - "guides/guide-for-latex-users.md", - ), - markdown_page( - resolver, - &format!("{}guides/", resolver.base()), - "guides/page-setup.md", - ), - ]; - page -} - -/// Build the packages section. -fn packages_page(resolver: &dyn Resolver) -> PageModel { - let md = DOCS_DIR - .get_file("reference/packages.md") - .unwrap() - .contents_utf8() - .unwrap(); - PageModel { - route: eco_format!("{}packages/", resolver.base()), - title: "Packages".into(), - description: "Packages for Typst.".into(), - part: None, - outline: vec![], - body: BodyModel::Packages(Html::markdown(resolver, md, Some(1))), - children: vec![], - } -} - -/// Create a page for a category. -#[track_caller] -fn category_page(resolver: &dyn Resolver, category: Category) -> PageModel { - let route = eco_format!("{}reference/{}/", resolver.base(), category.name()); - let mut children = vec![]; - let mut items = vec![]; - let mut shorthands = None; - let mut markup = vec![]; - let mut math = vec![]; - - let (module, path): (&Module, &[&str]) = if category == MATH { - (&LIBRARY.math, &["math"]) - } else { - (&LIBRARY.global, &[]) - }; - - // Add groups. - for group in GROUPS.iter().filter(|g| g.category == category.name()).cloned() { - if matches!(group.name.as_str(), "sym" | "emoji") { - let subpage = symbols_page(resolver, &route, &group); - let BodyModel::Symbols(model) = &subpage.body else { continue }; - let list = &model.list; - markup.extend( - list.iter() - .filter(|symbol| symbol.markup_shorthand.is_some()) - .cloned(), - ); - math.extend( - list.iter().filter(|symbol| symbol.math_shorthand.is_some()).cloned(), - ); - - items.push(CategoryItem { - name: group.name.clone(), - route: subpage.route.clone(), - oneliner: oneliner(category.docs()).into(), - code: true, - }); - children.push(subpage); - continue; - } - - let (child, item) = group_page(resolver, &route, &group); - children.push(child); - items.push(item); - } - - // Add symbol pages. These are ordered manually. - if category == SYMBOLS { - shorthands = Some(ShorthandsModel { markup, math }); - } - - // Add functions. - let scope = module.scope(); - for (name, value) in scope.iter() { - if scope.get_category(name) != Some(category) { - continue; - } - - if category == MATH { - // Skip grouped functions. - if GROUPS.iter().flat_map(|group| &group.filter).any(|f| f == name) { - continue; - } - - // Already documented in the text category. - if name == "text" { - continue; - } - } - - match value { - Value::Func(func) => { - let name = func.name().unwrap(); - - let subpage = func_page(resolver, &route, func, path); - items.push(CategoryItem { - name: name.into(), - route: subpage.route.clone(), - oneliner: oneliner(func.docs().unwrap_or_default()).into(), - code: true, - }); - children.push(subpage); - } - Value::Type(ty) => { - let subpage = type_page(resolver, &route, ty); - items.push(CategoryItem { - name: ty.short_name().into(), - route: subpage.route.clone(), - oneliner: oneliner(ty.docs()).into(), - code: true, - }); - children.push(subpage); - } - _ => {} - } - } - - if category != SYMBOLS { - children.sort_by_cached_key(|child| child.title.clone()); - items.sort_by_cached_key(|item| item.name.clone()); - } - - let name = category.title(); - let details = Html::markdown(resolver, category.docs(), Some(1)); - let mut outline = vec![OutlineItem::from_name("Summary")]; - outline.extend(details.outline()); - outline.push(OutlineItem::from_name("Definitions")); - if shorthands.is_some() { - outline.push(OutlineItem::from_name("Shorthands")); - } - - PageModel { - route, - title: name.into(), - description: eco_format!( - "Documentation for functions related to {name} in Typst." - ), - part: None, - outline, - body: BodyModel::Category(CategoryModel { - name: category.name(), - title: category.title(), - details, - items, - shorthands, - }), - children, - } -} - -/// Create a page for a function. -fn func_page( - resolver: &dyn Resolver, - parent: &str, - func: &Func, - path: &[&str], -) -> PageModel { - let model = func_model(resolver, func, path, false); - let name = func.name().unwrap(); - PageModel { - route: eco_format!("{parent}{}/", urlify(name)), - title: func.title().unwrap().into(), - description: eco_format!("Documentation for the `{name}` function."), - part: None, - outline: func_outline(&model, ""), - body: BodyModel::Func(model), - children: vec![], - } -} - -/// Produce a function's model. -fn func_model( - resolver: &dyn Resolver, - func: &Func, - path: &[&str], - nested: bool, -) -> FuncModel { - let name = func.name().unwrap(); - let scope = func.scope().unwrap(); - let docs = func.docs().unwrap(); - - let mut self_ = false; - let mut params = func.params().unwrap(); - if params.first().map_or(false, |first| first.name == "self") { - self_ = true; - params = ¶ms[1..]; - } - - let mut returns = vec![]; - casts(resolver, &mut returns, &mut vec![], func.returns().unwrap()); - returns.sort_by_key(|ty| type_index(ty)); - if returns == ["none"] { - returns.clear(); - } - - let nesting = if nested { None } else { Some(1) }; - let (details, example) = - if nested { split_details_and_example(docs) } else { (docs, None) }; - - FuncModel { - path: path.iter().copied().map(Into::into).collect(), - name: name.into(), - title: func.title().unwrap(), - keywords: func.keywords(), - oneliner: oneliner(details), - element: func.element().is_some(), - contextual: func.contextual().unwrap_or(false), - details: Html::markdown(resolver, details, nesting), - example: example.map(|md| Html::markdown(resolver, md, None)), - self_, - params: params.iter().map(|param| param_model(resolver, param)).collect(), - returns, - scope: scope_models(resolver, name, scope), - } -} - -/// Produce a parameter's model. -fn param_model(resolver: &dyn Resolver, info: &ParamInfo) -> ParamModel { - let (details, example) = split_details_and_example(info.docs); - - let mut types = vec![]; - let mut strings = vec![]; - casts(resolver, &mut types, &mut strings, &info.input); - if !strings.is_empty() && !types.contains(&"str") { - types.push("str"); - } - types.sort_by_key(|ty| type_index(ty)); - - ParamModel { - name: info.name, - details: Html::markdown(resolver, details, None), - example: example.map(|md| Html::markdown(resolver, md, None)), - types, - strings, - default: info.default.map(|default| { - let node = typst::syntax::parse_code(&default().repr()); - Html::new(typst::syntax::highlight_html(&node)) - }), - positional: info.positional, - named: info.named, - required: info.required, - variadic: info.variadic, - settable: info.settable, - } -} - -/// Split up documentation into details and an example. -fn split_details_and_example(docs: &str) -> (&str, Option<&str>) { - let mut details = docs; - let mut example = None; - if let Some(mut i) = docs.find("```") { - while docs[..i].ends_with('`') { - i -= 1; - } - details = &docs[..i]; - example = Some(&docs[i..]); - } - (details, example) -} - -/// Process cast information into types and strings. -fn casts( - resolver: &dyn Resolver, - types: &mut Vec<&'static str>, - strings: &mut Vec<StrParam>, - info: &CastInfo, -) { - match info { - CastInfo::Any => types.push("any"), - CastInfo::Value(Value::Str(string), docs) => strings.push(StrParam { - string: string.clone().into(), - details: Html::markdown(resolver, docs, None), - }), - CastInfo::Value(..) => {} - CastInfo::Type(ty) => types.push(ty.short_name()), - CastInfo::Union(options) => { - for option in options { - casts(resolver, types, strings, option); - } - } - } -} - -/// Produce models for a function's scope. -fn scope_models(resolver: &dyn Resolver, name: &str, scope: &Scope) -> Vec<FuncModel> { - scope - .iter() - .filter_map(|(_, value)| { - let Value::Func(func) = value else { return None }; - Some(func_model(resolver, func, &[name], true)) - }) - .collect() -} - -/// Produce an outline for a function page. -fn func_outline(model: &FuncModel, id_base: &str) -> Vec<OutlineItem> { - let mut outline = vec![]; - - if id_base.is_empty() { - outline.push(OutlineItem::from_name("Summary")); - outline.extend(model.details.outline()); - - if !model.params.is_empty() { - outline.push(OutlineItem { - id: "parameters".into(), - name: "Parameters".into(), - children: model - .params - .iter() - .map(|param| OutlineItem { - id: eco_format!("parameters-{}", urlify(param.name)), - name: param.name.into(), - children: vec![], - }) - .collect(), - }); - } - - outline.extend(scope_outline(&model.scope)); - } else { - outline.extend(model.params.iter().map(|param| OutlineItem { - id: eco_format!("{id_base}-{}", urlify(param.name)), - name: param.name.into(), - children: vec![], - })); - } - - outline -} - -/// Produce an outline for a function scope. -fn scope_outline(scope: &[FuncModel]) -> Option<OutlineItem> { - if scope.is_empty() { - return None; - } - - Some(OutlineItem { - id: "definitions".into(), - name: "Definitions".into(), - children: scope - .iter() - .map(|func| { - let id = urlify(&eco_format!("definitions-{}", func.name)); - let children = func_outline(func, &id); - OutlineItem { id, name: func.title.into(), children } - }) - .collect(), - }) -} - -/// Create a page for a group of functions. -fn group_page( - resolver: &dyn Resolver, - parent: &str, - group: &GroupData, -) -> (PageModel, CategoryItem) { - let mut functions = vec![]; - let mut outline = vec![OutlineItem::from_name("Summary")]; - - let path: Vec<_> = group.path.iter().map(|s| s.as_str()).collect(); - let details = Html::markdown(resolver, &group.details, Some(1)); - outline.extend(details.outline()); - - let mut outline_items = vec![]; - for name in &group.filter { - let value = group.module().scope().get(name).unwrap(); - let Value::Func(func) = value else { panic!("not a function") }; - let func = func_model(resolver, func, &path, true); - let id_base = urlify(&eco_format!("functions-{}", func.name)); - let children = func_outline(&func, &id_base); - outline_items.push(OutlineItem { - id: id_base, - name: func.title.into(), - children, - }); - functions.push(func); - } - - outline.push(OutlineItem { - id: "functions".into(), - name: "Functions".into(), - children: outline_items, - }); - - let model = PageModel { - route: eco_format!("{parent}{}", group.name), - title: group.title.clone(), - description: eco_format!("Documentation for the {} functions.", group.name), - part: None, - outline, - body: BodyModel::Group(GroupModel { - name: group.name.clone(), - title: group.title.clone(), - details, - functions, - }), - children: vec![], - }; - - let item = CategoryItem { - name: group.name.clone(), - route: model.route.clone(), - oneliner: oneliner(&group.details).into(), - code: false, - }; - - (model, item) -} - -/// Create a page for a type. -fn type_page(resolver: &dyn Resolver, parent: &str, ty: &Type) -> PageModel { - let model = type_model(resolver, ty); - PageModel { - route: eco_format!("{parent}{}/", urlify(ty.short_name())), - title: ty.title().into(), - description: eco_format!("Documentation for the {} type.", ty.title()), - part: None, - outline: type_outline(&model), - body: BodyModel::Type(model), - children: vec![], - } -} - -/// Produce a type's model. -fn type_model(resolver: &dyn Resolver, ty: &Type) -> TypeModel { - TypeModel { - name: ty.short_name(), - title: ty.title(), - keywords: ty.keywords(), - oneliner: oneliner(ty.docs()), - details: Html::markdown(resolver, ty.docs(), Some(1)), - constructor: ty - .constructor() - .ok() - .map(|func| func_model(resolver, &func, &[], true)), - scope: scope_models(resolver, ty.short_name(), ty.scope()), - } -} - -/// Produce an outline for a type page. -fn type_outline(model: &TypeModel) -> Vec<OutlineItem> { - let mut outline = vec![OutlineItem::from_name("Summary")]; - outline.extend(model.details.outline()); - - if let Some(func) = &model.constructor { - outline.push(OutlineItem { - id: "constructor".into(), - name: "Constructor".into(), - children: func_outline(func, "constructor"), - }); - } - - outline.extend(scope_outline(&model.scope)); - outline -} - -/// Create a page for symbols. -fn symbols_page(resolver: &dyn Resolver, parent: &str, group: &GroupData) -> PageModel { - let model = symbols_model(resolver, group); - PageModel { - route: eco_format!("{parent}{}/", group.name), - title: group.title.clone(), - description: eco_format!("Documentation for the `{}` module.", group.name), - part: None, - outline: vec![], - body: BodyModel::Symbols(model), - children: vec![], - } -} - -/// Produce a symbol list's model. -fn symbols_model(resolver: &dyn Resolver, group: &GroupData) -> SymbolsModel { - let mut list = vec![]; - for (name, value) in group.module().scope().iter() { - let Value::Symbol(symbol) = value else { continue }; - let complete = |variant: &str| { - if variant.is_empty() { - name.clone() - } else { - eco_format!("{}.{}", name, variant) - } - }; - - for (variant, c) in symbol.variants() { - let shorthand = |list: &[(&'static str, char)]| { - list.iter().copied().find(|&(_, x)| x == c).map(|(s, _)| s) - }; - - list.push(SymbolModel { - name: complete(variant), - markup_shorthand: shorthand(typst::syntax::ast::Shorthand::MARKUP_LIST), - math_shorthand: shorthand(typst::syntax::ast::Shorthand::MATH_LIST), - codepoint: c as u32, - accent: typst::symbols::Symbol::combining_accent(c).is_some(), - unicode_name: unicode_names2::name(c) - .map(|s| s.to_string().to_title_case().into()), - alternates: symbol - .variants() - .filter(|(other, _)| other != &variant) - .map(|(other, _)| complete(other)) - .collect(), - }); - } - } - - SymbolsModel { - name: group.name.clone(), - title: group.title.clone(), - details: Html::markdown(resolver, &group.details, Some(1)), - list, - } -} - -/// Extract a module from another module. -#[track_caller] -fn get_module<'a>(parent: &'a Module, name: &str) -> StrResult<&'a Module> { - match parent.scope().get(name) { - Some(Value::Module(module)) => Ok(module), - _ => bail!("module doesn't contain module `{name}`"), - } -} - -/// Load YAML from a path. -#[track_caller] -fn yaml<T: DeserializeOwned>(path: &str) -> T { - let file = DOCS_DIR.get_file(path).unwrap(); - yaml::from_slice(file.contents()).unwrap() -} - -/// Turn a title into an URL fragment. -pub fn urlify(title: &str) -> EcoString { - title - .chars() - .map(|c| c.to_ascii_lowercase()) - .map(|c| match c { - 'a'..='z' | '0'..='9' => c, - _ => '-', - }) - .collect() -} - -/// Extract the first line of documentation. -fn oneliner(docs: &str) -> &str { - docs.lines().next().unwrap_or_default() -} - -/// The order of types in the documentation. -fn type_index(ty: &str) -> usize { - TYPE_ORDER.iter().position(|&v| v == ty).unwrap_or(usize::MAX) -} - -const TYPE_ORDER: &[&str] = &[ - "any", - "none", - "auto", - "bool", - "int", - "float", - "length", - "angle", - "ratio", - "relative", - "fraction", - "color", - "gradient", - "datetime", - "duration", - "str", - "bytes", - "regex", - "label", - "content", - "array", - "dict", - "func", - "args", - "selector", - "location", - "direction", - "alignment", - "alignment2d", - "stroke", -]; - -/// Data about a collection of functions. -#[derive(Debug, Clone, Deserialize)] -struct GroupData { - name: EcoString, - title: EcoString, - category: EcoString, - #[serde(default)] - path: Vec<EcoString>, - #[serde(default)] - filter: Vec<EcoString>, - details: EcoString, -} - -impl GroupData { - fn module(&self) -> &'static Module { - let mut focus = &LIBRARY.global; - for path in &self.path { - focus = get_module(focus, path).unwrap(); - } - focus - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_docs() { - provide(&TestResolver); - } - - struct TestResolver; - - impl Resolver for TestResolver { - fn link(&self, _: &str) -> Option<String> { - None - } - - fn example(&self, _: u128, _: Option<Html>, _: &Document) -> Html { - Html::new(String::new()) - } - - fn image(&self, _: &str, _: &[u8]) -> String { - String::new() - } - - fn commits(&self, _: &str, _: &str) -> Vec<Commit> { - vec![] - } - - fn base(&self) -> &str { - "/" - } - } -} diff --git a/crates/typst-docs/src/link.rs b/crates/typst-docs/src/link.rs deleted file mode 100644 index f4d803c3..00000000 --- a/crates/typst-docs/src/link.rs +++ /dev/null @@ -1,108 +0,0 @@ -use typst::diag::{bail, StrResult}; -use typst::foundations::Func; - -use crate::{get_module, GROUPS, LIBRARY}; - -/// Resolve an intra-doc link. -pub fn resolve(link: &str, base: &str) -> StrResult<String> { - if link.starts_with('#') || link.starts_with("http") { - return Ok(link.to_string()); - } - - let (head, tail) = split_link(link)?; - let mut route = match resolve_known(head, base) { - Some(route) => route, - None => resolve_definition(head, base)?, - }; - - if !tail.is_empty() { - route.push('/'); - route.push_str(tail); - } - - if !route.contains('#') && !route.ends_with('/') { - route.push('/'); - } - - Ok(route) -} - -/// Split a link at the first slash. -fn split_link(link: &str) -> StrResult<(&str, &str)> { - let first = link.split('/').next().unwrap_or(link); - let rest = link[first.len()..].trim_start_matches('/'); - Ok((first, rest)) -} - -/// Resolve a `$` link head to a known destination. -fn resolve_known(head: &str, base: &str) -> Option<String> { - Some(match head { - "$tutorial" => format!("{base}tutorial"), - "$reference" => format!("{base}reference"), - "$category" => format!("{base}reference"), - "$syntax" => format!("{base}reference/syntax"), - "$styling" => format!("{base}reference/styling"), - "$scripting" => format!("{base}reference/scripting"), - "$context" => format!("{base}reference/context"), - "$guides" => format!("{base}guides"), - "$packages" => format!("{base}packages"), - "$changelog" => format!("{base}changelog"), - "$community" => format!("{base}community"), - _ => return None, - }) -} - -/// Resolve a `$` link to a global definition. -fn resolve_definition(head: &str, base: &str) -> StrResult<String> { - let mut parts = head.trim_start_matches('$').split('.').peekable(); - let mut focus = &LIBRARY.global; - let mut category = None; - - while let Some(name) = parts.peek() { - if category.is_none() { - category = focus.scope().get_category(name); - } - let Ok(module) = get_module(focus, name) else { break }; - focus = module; - parts.next(); - } - - let Some(category) = category else { bail!("{head} has no category") }; - - let name = parts.next().ok_or("link is missing first part")?; - let value = focus.field(name)?; - - // Handle grouped functions. - if let Some(group) = GROUPS.iter().find(|group| { - group.category == category.name() && group.filter.iter().any(|func| func == name) - }) { - let mut route = format!( - "{}reference/{}/{}/#functions-{}", - base, group.category, group.name, name - ); - if let Some(param) = parts.next() { - route.push('-'); - route.push_str(param); - } - return Ok(route); - } - - let mut route = format!("{}reference/{}/{name}/", base, category.name()); - if let Some(next) = parts.next() { - if value.field(next).is_ok() { - route.push_str("#definitions-"); - route.push_str(next); - } else if value - .clone() - .cast::<Func>() - .map_or(false, |func| func.param(next).is_some()) - { - route.push_str("#parameters-"); - route.push_str(next); - } else { - bail!("field {next} not found"); - } - } - - Ok(route) -} diff --git a/crates/typst-docs/src/main.rs b/crates/typst-docs/src/main.rs deleted file mode 100644 index f4414b10..00000000 --- a/crates/typst-docs/src/main.rs +++ /dev/null @@ -1,148 +0,0 @@ -use std::fs; -use std::path::{Path, PathBuf}; - -use clap::Parser; -use typst::model::Document; -use typst::visualize::Color; -use typst_docs::{provide, Html, Resolver}; -use typst_render::render; - -#[derive(Debug)] -struct CliResolver<'a> { - assets_dir: &'a Path, - verbose: bool, - base: &'a str, -} - -impl<'a> Resolver for CliResolver<'a> { - fn commits(&self, from: &str, to: &str) -> Vec<typst_docs::Commit> { - if self.verbose { - eprintln!("commits({from}, {to})"); - } - vec![] - } - - fn example( - &self, - hash: u128, - source: Option<Html>, - document: &Document, - ) -> typst_docs::Html { - if self.verbose { - eprintln!( - "example(0x{hash:x}, {:?} chars, Document)", - source.as_ref().map(|s| s.as_str().len()) - ); - } - - let frame = &document.pages.first().expect("page 0").frame; - let pixmap = render(frame, 2.0, Color::WHITE); - let filename = format!("{hash:x}.png"); - let path = self.assets_dir.join(&filename); - fs::create_dir_all(path.parent().expect("parent")).expect("create dir"); - pixmap.save_png(path.as_path()).expect("save png"); - let src = format!("{}assets/{filename}", self.base); - eprintln!("Generated example image {path:?}"); - - if let Some(code) = source { - let code_safe = code.as_str(); - Html::new(format!( - r#"<div class="previewed-code"><pre>{code_safe}</pre><div class="preview"><img src="{src}" alt="Preview" /></div></div>"# - )) - } else { - Html::new(format!( - r#"<div class="preview"><img src="{src}" alt="Preview" /></div>"# - )) - } - } - - fn image(&self, filename: &str, data: &[u8]) -> String { - if self.verbose { - eprintln!("image({filename}, {} bytes)", data.len()); - } - - let path = self.assets_dir.join(filename); - fs::create_dir_all(path.parent().expect("parent")).expect("create dir"); - fs::write(&path, data).expect("write image"); - eprintln!("Created {} byte image at {path:?}", data.len()); - - format!("{}assets/{filename}", self.base) - } - - fn link(&self, link: &str) -> Option<String> { - if self.verbose { - eprintln!("link({link})"); - } - None - } - - fn base(&self) -> &str { - self.base - } -} - -/// Generates the JSON representation of the documentation. This can be used to -/// generate the HTML yourself. Be warned: the JSON structure is not stable and -/// may change at any time. -#[derive(Parser, Debug)] -#[command(version, about, long_about = None)] -struct Args { - /// The generation process can produce additional assets. Namely images. - /// This option controls where to spit them out. The HTML generation will - /// assume that this output directory is served at `${base_url}/assets/*`. - /// The default is `assets`. For example, if the base URL is `/docs/` then - /// the gemerated HTML might look like `<img src="/docs/assets/foo.png">` - /// even though the `--assets-dir` was set to `/tmp/images` or something. - #[arg(long, default_value = "assets")] - assets_dir: PathBuf, - - /// Write the JSON output to this file. The default is `-` which is a - /// special value that means "write to standard output". If you want to - /// write to a file named `-` then use `./-`. - #[arg(long, default_value = "-")] - out_file: PathBuf, - - /// The base URL for the documentation. This can be an absolute URL like - /// `https://example.com/docs/` or a relative URL like `/docs/`. This is - /// used as the base URL for the generated page's `.route` properties as - /// well as cross-page links. The default is `/`. If a `/` trailing slash is - /// not present then it will be added. This option also affects the HTML - /// asset references. For example: `--base /docs/` will generate - /// `<img src="/docs/assets/foo.png">`. - #[arg(long, default_value = "/")] - base: String, - - /// Enable verbose logging. This will print out all the calls to the - /// resolver and the paths of the generated assets. - #[arg(long)] - verbose: bool, -} - -fn main() -> Result<(), Box<dyn std::error::Error>> { - let args = Args::parse(); - let mut base = args.base.clone(); - if !base.ends_with('/') { - base.push('/'); - } - - let resolver = CliResolver { - assets_dir: &args.assets_dir, - verbose: args.verbose, - base: &base, - }; - if args.verbose { - eprintln!("resolver: {resolver:?}"); - } - let pages = provide(&resolver); - - eprintln!("Be warned: the JSON structure is not stable and may change at any time."); - let json = serde_json::to_string_pretty(&pages)?; - - if args.out_file.to_string_lossy() == "-" { - println!("{json}"); - } else { - fs::write(&args.out_file, &*json)?; - } - - Ok(()) -} diff --git a/crates/typst-docs/src/model.rs b/crates/typst-docs/src/model.rs deleted file mode 100644 index 1564ef2f..00000000 --- a/crates/typst-docs/src/model.rs +++ /dev/null @@ -1,173 +0,0 @@ -use ecow::EcoString; -use heck::ToKebabCase; -use serde::Serialize; - -use crate::html::Html; - -/// Details about a documentation page and its children. -#[derive(Debug, Serialize)] -pub struct PageModel { - pub route: EcoString, - pub title: EcoString, - pub description: EcoString, - pub part: Option<&'static str>, - pub outline: Vec<OutlineItem>, - pub body: BodyModel, - pub children: Vec<Self>, -} - -impl PageModel { - pub fn with_route(self, route: &str) -> Self { - Self { route: route.into(), ..self } - } - - pub fn with_part(self, part: &'static str) -> Self { - Self { part: Some(part), ..self } - } -} - -/// An element in the "On This Page" outline. -#[derive(Debug, Clone, Serialize)] -pub struct OutlineItem { - pub id: EcoString, - pub name: EcoString, - pub children: Vec<Self>, -} - -impl OutlineItem { - /// Create an outline item from a name with auto-generated id. - pub fn from_name(name: &str) -> Self { - Self { - id: name.to_kebab_case().into(), - name: name.into(), - children: vec![], - } - } -} - -/// Details about the body of a documentation page. -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -#[serde(tag = "kind", content = "content")] -pub enum BodyModel { - Html(Html), - Category(CategoryModel), - Func(FuncModel), - Group(GroupModel), - Type(TypeModel), - Symbols(SymbolsModel), - Packages(Html), -} - -/// Details about a category. -#[derive(Debug, Serialize)] -pub struct CategoryModel { - pub name: &'static str, - pub title: &'static str, - pub details: Html, - pub items: Vec<CategoryItem>, - pub shorthands: Option<ShorthandsModel>, -} - -/// Details about a category item. -#[derive(Debug, Serialize)] -pub struct CategoryItem { - pub name: EcoString, - pub route: EcoString, - pub oneliner: EcoString, - pub code: bool, -} - -/// Details about a function. -#[derive(Debug, Serialize)] -pub struct FuncModel { - pub path: Vec<EcoString>, - pub name: EcoString, - pub title: &'static str, - pub keywords: &'static [&'static str], - pub oneliner: &'static str, - pub element: bool, - pub contextual: bool, - pub details: Html, - /// This example is only for nested function models. Others can have - /// their example directly in their details. - pub example: Option<Html>, - #[serde(rename = "self")] - pub self_: bool, - pub params: Vec<ParamModel>, - pub returns: Vec<&'static str>, - pub scope: Vec<FuncModel>, -} - -/// Details about a function parameter. -#[derive(Debug, Serialize)] -pub struct ParamModel { - pub name: &'static str, - pub details: Html, - pub example: Option<Html>, - pub types: Vec<&'static str>, - pub strings: Vec<StrParam>, - pub default: Option<Html>, - pub positional: bool, - pub named: bool, - pub required: bool, - pub variadic: bool, - pub settable: bool, -} - -/// A specific string that can be passed as an argument. -#[derive(Debug, Serialize)] -pub struct StrParam { - pub string: EcoString, - pub details: Html, -} - -/// Details about a group of functions. -#[derive(Debug, Serialize)] -pub struct GroupModel { - pub name: EcoString, - pub title: EcoString, - pub details: Html, - pub functions: Vec<FuncModel>, -} - -/// Details about a type. -#[derive(Debug, Serialize)] -pub struct TypeModel { - pub name: &'static str, - pub title: &'static str, - pub keywords: &'static [&'static str], - pub oneliner: &'static str, - pub details: Html, - pub constructor: Option<FuncModel>, - pub scope: Vec<FuncModel>, -} - -/// A collection of symbols. -#[derive(Debug, Serialize)] -pub struct SymbolsModel { - pub name: EcoString, - pub title: EcoString, - pub details: Html, - pub list: Vec<SymbolModel>, -} - -/// Details about a symbol. -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct SymbolModel { - pub name: EcoString, - pub codepoint: u32, - pub accent: bool, - pub unicode_name: Option<EcoString>, - pub alternates: Vec<EcoString>, - pub markup_shorthand: Option<&'static str>, - pub math_shorthand: Option<&'static str>, -} - -/// Shorthands listed on a category page. -#[derive(Debug, Serialize)] -pub struct ShorthandsModel { - pub markup: Vec<SymbolModel>, - pub math: Vec<SymbolModel>, -} |
