summaryrefslogtreecommitdiff
path: root/crates/typst-docs
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2024-02-28 11:37:52 +0100
committerGitHub <noreply@github.com>2024-02-28 10:37:52 +0000
commita518e2dd4d829b45b0887da28acb77d0568894ab (patch)
treee7387b27e299383a0bd1a26ad58f485630cb23b7 /crates/typst-docs
parente16d3f5a67a31154797b4d56cdc6ed142ee2a7cf (diff)
Move docs generation code (#3519)
Diffstat (limited to 'crates/typst-docs')
-rw-r--r--crates/typst-docs/Cargo.toml42
-rw-r--r--crates/typst-docs/src/contribs.rs86
-rw-r--r--crates/typst-docs/src/html.rs469
-rw-r--r--crates/typst-docs/src/lib.rs840
-rw-r--r--crates/typst-docs/src/link.rs108
-rw-r--r--crates/typst-docs/src/main.rs148
-rw-r--r--crates/typst-docs/src/model.rs173
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('\"', "&quot;").replace('&', "&amp;");
- 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 = &params[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>,
-}