summaryrefslogtreecommitdiff
path: root/crates/typst-docs/src
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2023-09-11 15:53:20 +0200
committerLaurenz <laurmaedje@gmail.com>2023-09-11 15:53:20 +0200
commit3a979e88da7be6240821ba8acfdd376b7f4e0f09 (patch)
treeb41a095a11d710f0fc5547e5bd0627deafa02af2 /crates/typst-docs/src
parent305524d005df075d53575552ee090fb53192a3fe (diff)
Docs generation for types
Diffstat (limited to 'crates/typst-docs/src')
-rw-r--r--crates/typst-docs/src/html.rs238
-rw-r--r--crates/typst-docs/src/lib.rs1010
-rw-r--r--crates/typst-docs/src/link.rs102
-rw-r--r--crates/typst-docs/src/model.rs170
4 files changed, 749 insertions, 771 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()
diff --git a/crates/typst-docs/src/lib.rs b/crates/typst-docs/src/lib.rs
index 96ae460c..3018c244 100644
--- a/crates/typst-docs/src/lib.rs
+++ b/crates/typst-docs/src/lib.rs
@@ -2,49 +2,39 @@
mod contribs;
mod html;
+mod link;
+mod model;
pub use contribs::{contributors, Author, Commit};
pub use html::Html;
+pub use model::*;
-use std::fmt::{self, Debug, Formatter};
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, Serialize};
+use serde::Deserialize;
use serde_yaml as yaml;
+use typst::diag::{bail, StrResult};
use typst::doc::Frame;
-use typst::eval::{CastInfo, Func, FuncInfo, Library, Module, ParamInfo, Value};
+use typst::eval::{CastInfo, Func, Library, Module, ParamInfo, Scope, Type, Value};
use typst::font::{Font, FontBook};
use typst::geom::{Abs, Smart};
use typst_library::layout::{Margin, PageElem};
-use unscanny::Scanner;
-static DOCS: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../../docs");
-static FILES: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../../assets/files");
-static DETAILS: Lazy<yaml::Mapping> = Lazy::new(|| yaml("reference/details.yml"));
-static GROUPS: Lazy<Vec<GroupData>> = Lazy::new(|| yaml("reference/groups.yml"));
+static DOCS_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../../docs");
+static FILE_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../../assets/files");
+static FONT_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../../assets/fonts");
-static FONTS: Lazy<(Prehashed<FontBook>, Vec<Font>)> = Lazy::new(|| {
- static DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../../assets/fonts");
- let fonts: Vec<_> = DIR
- .files()
- .flat_map(|file| Font::iter(file.contents().into()))
- .collect();
- let book = FontBook::from_fonts(&fonts);
- (Prehashed::new(book), fonts)
-});
+static CATEGORIES: Lazy<yaml::Mapping> = Lazy::new(|| yaml("reference/categories.yml"));
+static GROUPS: Lazy<Vec<GroupData>> = Lazy::new(|| yaml("reference/groups.yml"));
static LIBRARY: Lazy<Prehashed<Library>> = Lazy::new(|| {
let mut lib = typst_library::build();
- // Hack for documenting the `mix` function in the color module.
- // Will be superseded by proper associated functions.
- lib.global
- .scope_mut()
- .define("mix", typst_library::compute::mix_func());
lib.styles
.set(PageElem::set_width(Smart::Custom(Abs::pt(240.0).into())));
lib.styles.set(PageElem::set_height(Smart::Auto));
@@ -55,13 +45,22 @@ static LIBRARY: Lazy<Prehashed<Library>> = Lazy::new(|| {
Prehashed::new(lib)
});
+static FONTS: Lazy<(Prehashed<FontBook>, Vec<Font>)> = Lazy::new(|| {
+ let fonts: Vec<_> = FONT_DIR
+ .files()
+ .flat_map(|file| Font::iter(file.contents().into()))
+ .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, "/docs/", "overview.md").with_route("/docs/"),
tutorial_pages(resolver),
reference_pages(resolver),
- guides_pages(resolver),
+ guide_pages(resolver),
packages_page(resolver),
markdown_page(resolver, "/docs/", "changelog.md"),
markdown_page(resolver, "/docs/", "roadmap.md"),
@@ -84,54 +83,32 @@ pub trait Resolver {
fn commits(&self, from: &str, to: &str) -> Vec<Commit>;
}
-/// Details about a documentation page and its children.
-#[derive(Debug, Serialize)]
-pub struct PageModel {
- pub route: String,
- pub title: String,
- pub description: String,
- pub part: Option<&'static str>,
- pub outline: Vec<OutlineItem>,
- pub body: BodyModel,
- pub children: Vec<Self>,
-}
-
-impl PageModel {
- fn with_route(self, route: &str) -> Self {
- Self { route: route.into(), ..self }
- }
-
- fn with_part(self, part: &'static str) -> Self {
- Self { part: Some(part), ..self }
+/// 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![],
}
}
-/// An element in the "On This Page" outline.
-#[derive(Debug, Clone, Serialize)]
-pub struct OutlineItem {
- id: String,
- name: String,
- children: Vec<Self>,
-}
-
-/// 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),
- Funcs(FuncsModel),
- Type(TypeModel),
- Symbols(SymbolsModel),
- Packages(Html),
-}
-
/// Build the tutorial.
fn tutorial_pages(resolver: &dyn Resolver) -> PageModel {
let mut page = markdown_page(resolver, "/docs/", "tutorial/welcome.md");
- page.children = DOCS
+ page.children = DOCS_DIR
.get_dir("tutorial")
.unwrap()
.files()
@@ -149,23 +126,20 @@ fn reference_pages(resolver: &dyn Resolver) -> PageModel {
.with_part("Language"),
markdown_page(resolver, "/docs/reference/", "reference/styling.md"),
markdown_page(resolver, "/docs/reference/", "reference/scripting.md"),
- types_page(resolver, "/docs/reference/"),
- category_page(resolver, "text").with_part("Content"),
+ category_page(resolver, "foundations").with_part("Library"),
+ category_page(resolver, "text"),
category_page(resolver, "math"),
category_page(resolver, "layout"),
category_page(resolver, "visualize"),
category_page(resolver, "meta"),
category_page(resolver, "symbols"),
- category_page(resolver, "foundations").with_part("Compute"),
- category_page(resolver, "calculate"),
- category_page(resolver, "construct"),
category_page(resolver, "data-loading"),
];
page
}
/// Build the guides section.
-fn guides_pages(resolver: &dyn Resolver) -> PageModel {
+fn guide_pages(resolver: &dyn Resolver) -> PageModel {
let mut page = markdown_page(resolver, "/docs/", "guides/welcome.md");
page.children = vec![
markdown_page(resolver, "/docs/guides/", "guides/guide-for-latex-users.md"),
@@ -182,257 +156,162 @@ fn packages_page(resolver: &dyn Resolver) -> PageModel {
description: "Packages for Typst.".into(),
part: None,
outline: vec![],
- body: BodyModel::Packages(Html::markdown(resolver, details("packages"))),
- children: vec![],
- }
-}
-
-/// 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.get_file(path).unwrap().contents_utf8().unwrap();
- let html = Html::markdown(resolver, md);
- let title = html.title().expect("chapter lacks a title").to_string();
- PageModel {
- route: format!("{parent}{}/", urlify(&title)),
- title,
- description: html.description().unwrap(),
- part: None,
- outline: html.outline(),
- body: BodyModel::Html(html),
+ body: BodyModel::Packages(Html::markdown(
+ resolver,
+ category_details("packages"),
+ Some(1),
+ )),
children: vec![],
}
}
-/// Details about a category.
-#[derive(Debug, Serialize)]
-pub struct CategoryModel {
- pub name: String,
- pub details: Html,
- pub kind: &'static str,
- pub items: Vec<CategoryItem>,
- pub markup_shorthands: Option<Vec<SymbolModel>>,
- pub math_shorthands: Option<Vec<SymbolModel>>,
-}
-
-/// Details about a category item.
-#[derive(Debug, Serialize)]
-pub struct CategoryItem {
- pub name: String,
- pub route: String,
- pub oneliner: String,
- pub code: bool,
-}
-
/// Create a page for a category.
#[track_caller]
fn category_page(resolver: &dyn Resolver, category: &str) -> PageModel {
- let route = format!("/docs/reference/{category}/");
+ let route = eco_format!("/docs/reference/{category}/");
let mut children = vec![];
let mut items = vec![];
- let focus = match category {
- "math" => &LIBRARY.math,
- "calculate" => module(&LIBRARY.global, "calc").unwrap(),
- _ => &LIBRARY.global,
- };
-
- let parents: &[&str] = match category {
- "math" => &[],
- "calculate" => &["calc"],
- _ => &[],
+ let (module, path): (&Module, &[&str]) = match category {
+ "math" => (&LIBRARY.math, &["math"]),
+ _ => (&LIBRARY.global, &[]),
};
- let grouped = match category {
- "math" => GROUPS.as_slice(),
- _ => &[],
- };
-
- // Add functions.
- for (_, value) in focus.scope().iter() {
- let Value::Func(func) = value else { continue };
- let Some(info) = func.info() else { continue };
- if info.category != category {
- continue;
+ // Add groups.
+ for mut group in GROUPS.iter().filter(|g| g.category == category).cloned() {
+ let mut focus = module;
+ if group.name == "calc" {
+ focus = get_module(focus, "calc").unwrap();
+ group.functions = focus
+ .scope()
+ .iter()
+ .filter(|(_, v)| matches!(v, Value::Func(_)))
+ .map(|(k, _)| k.clone())
+ .collect();
}
+ let (child, item) = group_page(resolver, &route, &group, focus.scope());
+ children.push(child);
+ items.push(item);
+ }
- // Skip grouped functions.
- if grouped
- .iter()
- .flat_map(|group| &group.functions)
- .any(|f| f == info.name)
- {
+ // Add functions.
+ let scope = module.scope();
+ for (name, value) in scope.iter() {
+ if scope.get_category(name) != Some(category) {
continue;
}
- let subpage = function_page(resolver, &route, func, info, parents);
- items.push(CategoryItem {
- name: info.name.into(),
- route: subpage.route.clone(),
- oneliner: oneliner(info.docs).into(),
- code: true,
- });
- children.push(subpage);
- }
+ if category == "math" {
+ // Skip grouped functions.
+ if GROUPS.iter().flat_map(|group| &group.functions).any(|f| f == name) {
+ continue;
+ }
- // Add grouped functions.
- for group in grouped {
- let mut functions = vec![];
- let mut outline = vec![OutlineItem {
- id: "summary".into(),
- name: "Summary".into(),
- children: vec![],
- }];
-
- for name in &group.functions {
- let value = focus.get(name).unwrap();
- let Value::Func(func) = value else { panic!("not a function") };
- let info = func.info().unwrap();
- let func = func_model(resolver, func, info, &[], info.name);
- let id = urlify(&func.path.join("-"));
- let children = func_outline(&func, &id, false);
- outline.push(OutlineItem { id, name: func.display.into(), children });
- functions.push(func);
+ // Already documented in the text category.
+ if name == "text" {
+ continue;
+ }
}
- let route = format!("{}{}/", route, group.name);
- items.push(CategoryItem {
- name: group.name.clone(),
- route: route.clone(),
- oneliner: oneliner(&group.description).into(),
- code: false,
- });
-
- children.push(PageModel {
- route,
- title: group.display.clone(),
- description: format!("Documentation for {} group of functions.", group.name),
- part: None,
- outline,
- body: BodyModel::Funcs(FuncsModel {
- name: group.name.clone(),
- display: group.display.clone(),
- details: Html::markdown(resolver, &group.description),
- functions,
- }),
- children: vec![],
- });
+ 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);
+ }
+ _ => {}
+ }
}
children.sort_by_cached_key(|child| child.title.clone());
items.sort_by_cached_key(|item| item.name.clone());
// Add symbol pages. These are ordered manually.
- let mut markup_shorthands = vec![];
- let mut math_shorthands = vec![];
+ let mut shorthands = None;
if category == "symbols" {
+ let mut markup = vec![];
+ let mut math = vec![];
for module in ["sym", "emoji"] {
- let subpage = symbol_page(resolver, &route, module);
+ let subpage = symbols_page(resolver, &route, module);
let BodyModel::Symbols(model) = &subpage.body else { continue };
let list = &model.list;
- markup_shorthands.extend(
+ markup.extend(
list.iter()
.filter(|symbol| symbol.markup_shorthand.is_some())
.cloned(),
);
- math_shorthands.extend(
+ math.extend(
list.iter().filter(|symbol| symbol.math_shorthand.is_some()).cloned(),
);
items.push(CategoryItem {
name: module.into(),
route: subpage.route.clone(),
- oneliner: oneliner(details(module)).into(),
+ oneliner: oneliner(category_details(module)).into(),
code: true,
});
children.push(subpage);
}
+ shorthands = Some(ShorthandsModel { markup, math });
}
- let name = category.to_title_case();
- let kind = match category {
- "symbols" => "Modules",
- _ => "Functions",
- };
+ let name: EcoString = category.to_title_case().into();
PageModel {
route,
title: name.clone(),
- description: format!("Documentation for functions related to {name} in Typst."),
+ description: eco_format!(
+ "Documentation for functions related to {name} in Typst."
+ ),
part: None,
- outline: category_outline(kind),
+ outline: category_outline(),
body: BodyModel::Category(CategoryModel {
name,
- details: Html::markdown(resolver, details(category)),
- kind,
+ details: Html::markdown(resolver, category_details(category), Some(1)),
items,
- markup_shorthands: Some(markup_shorthands),
- math_shorthands: Some(math_shorthands),
+ shorthands,
}),
children,
}
}
/// Produce an outline for a category page.
-fn category_outline(kind: &str) -> Vec<OutlineItem> {
- vec![
- OutlineItem {
- id: "summary".into(),
- name: "Summary".into(),
- children: vec![],
- },
- OutlineItem {
- id: urlify(kind),
- name: kind.into(),
- children: vec![],
- },
- ]
-}
-
-/// Details about a function.
-#[derive(Debug, Serialize)]
-pub struct FuncModel {
- pub path: Vec<&'static str>,
- pub display: &'static str,
- pub keywords: Option<&'static str>,
- pub oneliner: &'static str,
- pub element: bool,
- pub details: Html,
- pub params: Vec<ParamModel>,
- pub returns: Vec<&'static str>,
- pub methods: Vec<MethodModel>,
- pub scope: Vec<Self>,
-}
-
-/// Details about a group of functions.
-#[derive(Debug, Serialize)]
-pub struct FuncsModel {
- pub name: String,
- pub display: String,
- pub details: Html,
- pub functions: Vec<FuncModel>,
+fn category_outline() -> Vec<OutlineItem> {
+ vec![OutlineItem::from_name("Summary"), OutlineItem::from_name("Definitions")]
}
/// Create a page for a function.
-fn function_page(
+fn func_page(
resolver: &dyn Resolver,
parent: &str,
func: &Func,
- info: &FuncInfo,
- parents: &[&'static str],
+ path: &[&str],
) -> PageModel {
- let model = func_model(resolver, func, info, parents, "");
+ let model = func_model(resolver, func, path, false);
+ let name = func.name().unwrap();
PageModel {
- route: format!("{parent}{}/", urlify(info.name)),
- title: info.display.to_string(),
- description: format!("Documentation for the `{}` function.", info.name),
+ 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, "", true),
+ outline: func_outline(&model, ""),
body: BodyModel::Func(model),
children: vec![],
}
@@ -442,143 +321,63 @@ fn function_page(
fn func_model(
resolver: &dyn Resolver,
func: &Func,
- info: &FuncInfo,
- parents: &[&'static str],
- id_base: &str,
+ path: &[&str],
+ nested: bool,
) -> FuncModel {
- let mut s = unscanny::Scanner::new(info.docs);
- let docs = s.eat_until("\n## Methods").trim();
-
- let mut path = parents.to_vec();
- let mut name = info.name;
- for parent in parents.iter().rev() {
- name = name
- .strip_prefix(parent)
- .or(name.strip_prefix(parent.strip_suffix('s').unwrap_or(parent)))
- .unwrap_or(name)
- .trim_matches('-');
- }
- path.push(name);
+ let name = func.name().unwrap();
+ let scope = func.scope().unwrap();
+ let docs = func.docs().unwrap();
- let scope = info
- .scope
- .iter()
- .filter_map(|(_, value)| {
- let Value::Func(func) = value else { return None };
- let info = func.info().unwrap();
- Some(func_model(resolver, func, info, &path, id_base))
- })
- .collect();
+ let mut self_ = false;
+ let mut params = func.params().unwrap();
+ if params.first().map_or(false, |first| first.name == "self") {
+ self_ = true;
+ params = &params[1..];
+ }
let mut returns = vec![];
- casts(resolver, &mut returns, &mut vec![], &info.returns);
+ 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,
- display: info.display,
- keywords: info.keywords,
- oneliner: oneliner(docs),
+ 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(),
- details: Html::markdown_with_id_base(resolver, docs, id_base),
- params: info.params.iter().map(|param| param_model(resolver, param)).collect(),
+ 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,
- methods: method_models(resolver, info.docs),
- scope,
- }
-}
-
-/// Produce an outline for a function page.
-fn func_outline(model: &FuncModel, base: &str, summary: bool) -> Vec<OutlineItem> {
- let mut outline = vec![];
-
- if summary {
- outline.push(OutlineItem {
- id: "summary".into(),
- name: "Summary".into(),
- children: vec![],
- });
+ scope: scope_models(resolver, name, scope),
}
-
- outline.extend(model.details.outline());
-
- if !model.params.is_empty() {
- let join = if base.is_empty() { "" } else { "-" };
- outline.push(OutlineItem {
- id: format!("{base}{join}parameters"),
- name: "Parameters".into(),
- children: model
- .params
- .iter()
- .map(|param| OutlineItem {
- id: format!("{base}{join}parameters-{}", urlify(param.name)),
- name: param.name.into(),
- children: vec![],
- })
- .collect(),
- });
- }
-
- for func in &model.scope {
- let id = urlify(&func.path.join("-"));
- let children = func_outline(func, &id, false);
- outline.push(OutlineItem { id, name: func.display.into(), children })
- }
-
- outline.extend(methods_outline(&model.methods));
- outline
-}
-
-/// 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: String,
- pub details: Html,
}
/// 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.cast);
+ casts(resolver, &mut types, &mut strings, &info.input);
if !strings.is_empty() && !types.contains(&"string") {
types.push("string");
}
types.sort_by_key(|ty| type_index(ty));
- let mut details = info.docs;
- let mut example = None;
- if let Some(mut i) = info.docs.find("```example") {
- while info.docs[..i].ends_with('`') {
- i -= 1;
- }
- details = &info.docs[..i];
- example = Some(&info.docs[i..]);
- }
-
ParamModel {
name: info.name,
- details: Html::markdown(resolver, details),
- example: example.map(|md| Html::markdown(resolver, md)),
+ details: Html::markdown(resolver, details, None),
+ example: example.map(|md| Html::markdown(resolver, md, None)),
types,
strings,
default: info.default.map(|default| {
@@ -593,6 +392,20 @@ fn param_model(resolver: &dyn Resolver, info: &ParamInfo) -> ParamModel {
}
}
+/// 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,
@@ -603,11 +416,11 @@ fn casts(
match info {
CastInfo::Any => types.push("any"),
CastInfo::Value(Value::Str(string), docs) => strings.push(StrParam {
- string: string.to_string(),
- details: Html::markdown(resolver, docs),
+ string: string.clone().into(),
+ details: Html::markdown(resolver, docs, None),
}),
CastInfo::Value(..) => {}
- CastInfo::Type(ty) => types.push(ty),
+ CastInfo::Type(ty) => types.push(ty.short_name()),
CastInfo::Union(options) => {
for option in options {
casts(resolver, types, strings, option);
@@ -616,258 +429,216 @@ fn casts(
}
}
-/// A collection of symbols.
-#[derive(Debug, Serialize)]
-pub struct TypeModel {
- pub name: String,
- pub oneliner: &'static str,
- pub details: Html,
- pub methods: Vec<MethodModel>,
+/// 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()
}
-/// Details about a built-in method on a type.
-#[derive(Debug, Serialize)]
-pub struct MethodModel {
- pub name: &'static str,
- pub details: Html,
- pub params: Vec<ParamModel>,
- pub returns: Vec<&'static str>,
-}
+/// Produce an outline for a function page.
+fn func_outline(model: &FuncModel, id_base: &str) -> Vec<OutlineItem> {
+ let mut outline = vec![];
-/// Create a page for the types.
-fn types_page(resolver: &dyn Resolver, parent: &str) -> PageModel {
- let route = format!("{parent}types/");
- let mut children = vec![];
- let mut items = 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(),
+ });
+ }
- for model in type_models(resolver) {
- let route = format!("{route}{}/", urlify(&model.name));
- items.push(CategoryItem {
- name: model.name.clone(),
- route: route.clone(),
- oneliner: model.oneliner.into(),
- code: true,
- });
- children.push(PageModel {
- route,
- title: model.name.to_title_case(),
- description: format!("Documentation for the `{}` type.", model.name),
- part: None,
- outline: type_outline(&model),
- body: BodyModel::Type(model),
+ 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![],
- });
+ }));
}
- PageModel {
- route,
- title: "Types".into(),
- description: "Documentation for Typst's built-in types.".into(),
- part: None,
- outline: category_outline("Types"),
- body: BodyModel::Category(CategoryModel {
- name: "Types".into(),
- details: Html::markdown(resolver, details("types")),
- kind: "Types",
- items,
- markup_shorthands: None,
- math_shorthands: None,
- }),
- children,
- }
+ outline
}
-/// Produce the types' models.
-fn type_models(resolver: &dyn Resolver) -> Vec<TypeModel> {
- let file = DOCS.get_file("reference/types.md").unwrap();
- let text = file.contents_utf8().unwrap();
-
- let mut s = unscanny::Scanner::new(text);
- let mut types = vec![];
-
- while s.eat_if("# ") {
- let part = s.eat_until("\n# ");
- types.push(type_model(resolver, part));
- s.eat_if('\n');
+/// Produce an outline for a function scope.
+fn scope_outline(scope: &[FuncModel]) -> Option<OutlineItem> {
+ if scope.is_empty() {
+ return None;
}
- types
-}
-
-/// Produce a type's model.
-fn type_model(resolver: &dyn Resolver, part: &'static str) -> TypeModel {
- let mut s = unscanny::Scanner::new(part);
- let display = s.eat_until('\n').trim();
- let docs = s.eat_until("\n## Methods").trim();
- TypeModel {
- name: display.to_lowercase(),
- oneliner: oneliner(docs),
- details: Html::markdown(resolver, docs),
- methods: method_models(resolver, part),
- }
+ 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(),
+ })
}
-/// Produce multiple methods' models.
-fn method_models(resolver: &dyn Resolver, docs: &'static str) -> Vec<MethodModel> {
- let mut s = unscanny::Scanner::new(docs);
- s.eat_until("\n## Methods");
- s.eat_whitespace();
-
- let mut methods = vec![];
- if s.eat_if("## Methods") {
- s.eat_until("\n### ");
- while s.eat_if("\n### ") {
- methods.push(method_model(resolver, s.eat_until("\n### ")));
- }
+/// Create a page for a group of functions.
+fn group_page(
+ resolver: &dyn Resolver,
+ parent: &str,
+ group: &GroupData,
+ scope: &Scope,
+) -> (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.description, Some(1));
+ outline.extend(details.outline());
+
+ let mut outline_items = vec![];
+ for name in &group.functions {
+ let value = 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);
}
- methods
-}
-
-/// Produce a method's model.
-fn method_model(resolver: &dyn Resolver, part: &'static str) -> MethodModel {
- let mut s = unscanny::Scanner::new(part);
- let mut params = vec![];
- let mut returns = vec![];
-
- let name = s.eat_until('(').trim();
- s.expect("()");
- let docs = s.eat_until("\n- ").trim();
-
- while s.eat_if("\n- ") {
- let name = s.eat_until(':');
- s.expect(": ");
- let types: Vec<_> =
- s.eat_until(['(', '\n']).split(" or ").map(str::trim).collect();
- if !types.iter().all(|ty| type_index(ty) != usize::MAX) {
- panic!(
- "unknown type in method {} parameter {}",
- name,
- types.iter().find(|ty| type_index(ty) == usize::MAX).unwrap()
- )
- }
+ outline.push(OutlineItem {
+ id: "functions".into(),
+ name: "Functions".into(),
+ children: outline_items,
+ });
- if name == "returns" {
- returns = types;
- continue;
- }
+ let model = PageModel {
+ route: eco_format!("{parent}{}", group.name),
+ title: group.display.clone(),
+ description: eco_format!("Documentation for the {} functions.", group.name),
+ part: None,
+ outline,
+ body: BodyModel::Group(GroupModel {
+ name: group.name.clone(),
+ title: group.display.clone(),
+ details,
+ functions,
+ }),
+ children: vec![],
+ };
- s.expect('(');
-
- let mut named = false;
- let mut positional = false;
- let mut required = false;
- let mut variadic = false;
- for part in s.eat_until(')').split(',').map(str::trim) {
- match part {
- "named" => named = true,
- "positional" => positional = true,
- "required" => required = true,
- "variadic" => variadic = true,
- _ => panic!("unknown parameter flag {:?}", part),
- }
- }
+ let item = CategoryItem {
+ name: group.name.clone(),
+ route: model.route.clone(),
+ oneliner: oneliner(&group.description).into(),
+ code: false,
+ };
- s.expect(')');
+ (model, item)
+}
- params.push(ParamModel {
- name,
- details: Html::markdown(resolver, s.eat_until("\n- ").trim()),
- example: None,
- types,
- strings: vec![],
- default: None,
- positional,
- named,
- required,
- variadic,
- settable: false,
- });
+/// 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![],
}
+}
- MethodModel {
- name,
- details: Html::markdown(resolver, docs),
- params,
- returns,
+/// 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 {
- id: "summary".into(),
- name: "Summary".into(),
- children: vec![],
- }];
+ 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(methods_outline(&model.methods));
+ outline.extend(scope_outline(&model.scope));
outline
}
-/// Produce an outline for a type's method.
-fn methods_outline(methods: &[MethodModel]) -> Option<OutlineItem> {
- (!methods.is_empty()).then(|| OutlineItem {
- id: "methods".into(),
- name: "Methods".into(),
- children: methods.iter().map(method_outline).collect(),
- })
-}
+/// Create a page for symbols.
+fn symbols_page(resolver: &dyn Resolver, parent: &str, name: &str) -> PageModel {
+ let module = get_module(&LIBRARY.global, name).unwrap();
+ let title = match name {
+ "sym" => "General",
+ "emoji" => "Emoji",
+ _ => unreachable!(),
+ };
-/// Produce an outline for a type's method.
-fn method_outline(model: &MethodModel) -> OutlineItem {
- OutlineItem {
- id: format!("methods-{}", urlify(model.name)),
- name: model.name.into(),
- children: model
- .params
- .iter()
- .map(|param| OutlineItem {
- id: format!(
- "methods-{}-parameters-{}",
- urlify(model.name),
- urlify(param.name)
- ),
- name: param.name.into(),
- children: vec![],
- })
- .collect(),
+ let model = symbols_model(resolver, name, title, module.scope());
+ PageModel {
+ route: eco_format!("{parent}{name}/"),
+ title: title.into(),
+ description: eco_format!("Documentation for the `{name}` module."),
+ part: None,
+ outline: vec![],
+ body: BodyModel::Symbols(model),
+ children: vec![],
}
}
-/// A collection of symbols.
-#[derive(Debug, Serialize)]
-pub struct SymbolsModel {
- pub name: &'static str,
- pub details: Html,
- pub list: Vec<SymbolModel>,
-}
-
-/// Details about a symbol.
-#[derive(Debug, Clone, Serialize)]
-#[serde(rename_all = "camelCase")]
-pub struct SymbolModel {
- pub name: String,
- pub markup_shorthand: Option<&'static str>,
- pub math_shorthand: Option<&'static str>,
- pub codepoint: u32,
- pub accent: bool,
- pub unicode_name: Option<String>,
- pub alternates: Vec<String>,
-}
-
-/// Create a page for symbols.
-fn symbol_page(resolver: &dyn Resolver, parent: &str, name: &str) -> PageModel {
- let module = module(&LIBRARY.global, name).unwrap();
-
+/// Produce a symbol list's model.
+fn symbols_model(
+ resolver: &dyn Resolver,
+ name: &str,
+ title: &'static str,
+ scope: &Scope,
+) -> SymbolsModel {
let mut list = vec![];
- for (name, value) in module.scope().iter() {
+ for (name, value) in scope.iter() {
let Value::Symbol(symbol) = value else { continue };
let complete = |variant: &str| {
if variant.is_empty() {
- name.into()
+ name.clone()
} else {
- format!("{}.{}", name, variant)
+ eco_format!("{}.{}", name, variant)
}
};
@@ -883,7 +654,7 @@ fn symbol_page(resolver: &dyn Resolver, parent: &str, name: &str) -> PageModel {
codepoint: c as u32,
accent: typst::eval::Symbol::combining_accent(c).is_some(),
unicode_name: unicode_names2::name(c)
- .map(|s| s.to_string().to_title_case()),
+ .map(|s| s.to_string().to_title_case().into()),
alternates: symbol
.variants()
.filter(|(other, _)| other != &variant)
@@ -893,63 +664,40 @@ fn symbol_page(resolver: &dyn Resolver, parent: &str, name: &str) -> PageModel {
}
}
- let title = match name {
- "sym" => "General",
- "emoji" => "Emoji",
- _ => unreachable!(),
- };
-
- PageModel {
- route: format!("{parent}{name}/"),
- title: title.into(),
- description: format!("Documentation for the `{name}` module."),
- part: None,
- outline: vec![],
- body: BodyModel::Symbols(SymbolsModel {
- name: title,
- details: Html::markdown(resolver, details(name)),
- list,
- }),
- children: vec![],
+ SymbolsModel {
+ name: title,
+ details: Html::markdown(resolver, category_details(name), Some(1)),
+ list,
}
}
-/// Data about a collection of functions.
-#[derive(Debug, Deserialize)]
-struct GroupData {
- name: String,
- display: String,
- functions: Vec<String>,
- description: String,
-}
-
/// Extract a module from another module.
#[track_caller]
-fn module<'a>(parent: &'a Module, name: &str) -> Result<&'a Module, String> {
+fn get_module<'a>(parent: &'a Module, name: &str) -> StrResult<&'a Module> {
match parent.scope().get(name) {
Some(Value::Module(module)) => Ok(module),
- _ => Err(format!("module doesn't contain module `{name}`")),
+ _ => bail!("module doesn't contain module `{name}`"),
}
}
/// Load YAML from a path.
#[track_caller]
fn yaml<T: DeserializeOwned>(path: &str) -> T {
- let file = DOCS.get_file(path).unwrap();
+ let file = DOCS_DIR.get_file(path).unwrap();
yaml::from_slice(file.contents()).unwrap()
}
/// Load details for an identifying key.
#[track_caller]
-fn details(key: &str) -> &str {
- DETAILS
+fn category_details(key: &str) -> &str {
+ CATEGORIES
.get(&yaml::Value::String(key.into()))
.and_then(|value| value.as_str())
.unwrap_or_else(|| panic!("missing details for {key}"))
}
/// Turn a title into an URL fragment.
-pub fn urlify(title: &str) -> String {
+pub fn urlify(title: &str) -> EcoString {
title
.chars()
.map(|c| c.to_ascii_lowercase())
@@ -974,33 +722,47 @@ const TYPE_ORDER: &[&str] = &[
"any",
"none",
"auto",
- "boolean",
- "integer",
+ "bool",
+ "int",
"float",
"length",
"angle",
"ratio",
- "relative length",
+ "relative",
"fraction",
"color",
"datetime",
- "string",
+ "duration",
+ "str",
"bytes",
"regex",
"label",
"content",
"array",
- "dictionary",
- "function",
- "arguments",
+ "dict",
+ "func",
+ "args",
+ "selector",
"location",
- "dir",
+ "direction",
"alignment",
- "2d alignment",
- "selector",
+ "alignment2d",
"stroke",
];
+/// Data about a collection of functions.
+#[derive(Debug, Clone, Deserialize)]
+struct GroupData {
+ name: EcoString,
+ category: EcoString,
+ display: EcoString,
+ #[serde(default)]
+ path: Vec<EcoString>,
+ #[serde(default)]
+ functions: Vec<EcoString>,
+ description: EcoString,
+}
+
#[cfg(test)]
mod tests {
use super::*;
diff --git a/crates/typst-docs/src/link.rs b/crates/typst-docs/src/link.rs
new file mode 100644
index 00000000..5d7a5485
--- /dev/null
+++ b/crates/typst-docs/src/link.rs
@@ -0,0 +1,102 @@
+use typst::diag::{bail, StrResult};
+use typst::eval::Func;
+
+use super::{get_module, GROUPS, LIBRARY};
+
+/// Resolve an intra-doc link.
+pub fn resolve(link: &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) {
+ Some(route) => route.into(),
+ None => resolve_definition(head)?,
+ };
+
+ 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) -> Option<&'static str> {
+ Some(match head {
+ "$tutorial" => "/docs/tutorial/",
+ "$reference" => "/docs/reference/",
+ "$category" => "/docs/reference/",
+ "$syntax" => "/docs/reference/syntax/",
+ "$styling" => "/docs/reference/styling/",
+ "$scripting" => "/docs/reference/scripting/",
+ "$guides" => "/docs/guides/",
+ "$packages" => "/docs/packages/",
+ "$changelog" => "/docs/changelog/",
+ "$community" => "/docs/community/",
+ _ => return None,
+ })
+}
+
+/// Resolve a `$` link to a global definition.
+fn resolve_definition(head: &str) -> StrResult<String> {
+ let mut parts = head.trim_start_matches('$').split('.').peekable();
+ let mut focus = &LIBRARY.global;
+ while let Some(m) = parts.peek().and_then(|&name| get_module(focus, name).ok()) {
+ focus = m;
+ parts.next();
+ }
+
+ let name = parts.next().ok_or("link is missing first part")?;
+ let value = focus.field(name)?;
+ let Some(category) = focus.scope().get_category(name) else {
+ bail!("{name} has no category");
+ };
+
+ // Handle grouped functions.
+ if let Some(group) = GROUPS
+ .iter()
+ .filter(|_| category == "math")
+ .find(|group| group.functions.iter().any(|func| func == name))
+ {
+ let mut route =
+ format!("/docs/reference/math/{}/#functions-{}", group.name, name);
+ if let Some(param) = parts.next() {
+ route.push('-');
+ route.push_str(param);
+ }
+ return Ok(route);
+ }
+
+ let mut route = format!("/docs/reference/{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/model.rs b/crates/typst-docs/src/model.rs
new file mode 100644
index 00000000..46d979a2
--- /dev/null
+++ b/crates/typst-docs/src/model.rs
@@ -0,0 +1,170 @@
+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: EcoString,
+ 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 details: Html,
+ /// This example is only for nested function models. Others can have
+ /// their example directly in their detals.
+ 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: &'static str,
+ 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>,
+}