summaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2023-07-02 19:59:52 +0200
committerLaurenz <laurmaedje@gmail.com>2023-07-02 20:07:43 +0200
commitebfdb1dafa430786db10dad2ef7d5467c1bdbed1 (patch)
tree2bbc24ddb4124c4bb14dec0e536129d4de37b056 /docs
parent3ab19185093d7709f824b95b979060ce125389d8 (diff)
Move everything into `crates/` directory
Diffstat (limited to 'docs')
-rw-r--r--docs/Cargo.toml27
-rw-r--r--docs/changelog.md (renamed from docs/src/general/changelog.md)0
-rw-r--r--docs/community.md (renamed from docs/src/general/community.md)0
-rw-r--r--docs/dev/architecture.md171
-rw-r--r--docs/guides/guide-for-latex-users.md (renamed from docs/src/guides/guide-for-latex-users.md)0
-rw-r--r--docs/guides/welcome.md (renamed from docs/src/guides/welcome.md)0
-rw-r--r--docs/overview.md (renamed from docs/src/general/overview.md)0
-rw-r--r--docs/reference/details.yml (renamed from docs/src/reference/details.yml)0
-rw-r--r--docs/reference/groups.yml (renamed from docs/src/reference/groups.yml)0
-rw-r--r--docs/reference/scripting.md (renamed from docs/src/reference/scripting.md)0
-rw-r--r--docs/reference/styling.md (renamed from docs/src/reference/styling.md)0
-rw-r--r--docs/reference/syntax.md (renamed from docs/src/reference/syntax.md)0
-rw-r--r--docs/reference/types.md (renamed from docs/src/reference/types.md)0
-rw-r--r--docs/reference/welcome.md (renamed from docs/src/reference/welcome.md)0
-rw-r--r--docs/src/contribs.rs86
-rw-r--r--docs/src/html.rs513
-rw-r--r--docs/src/lib.rs1002
-rw-r--r--docs/tutorial/1-writing.md (renamed from docs/src/tutorial/1-writing.md)0
-rw-r--r--docs/tutorial/2-formatting.md (renamed from docs/src/tutorial/2-formatting.md)0
-rw-r--r--docs/tutorial/3-advanced.md (renamed from docs/src/tutorial/3-advanced.md)0
-rw-r--r--docs/tutorial/4-template.md (renamed from docs/src/tutorial/4-template.md)0
-rw-r--r--docs/tutorial/welcome.md (renamed from docs/src/tutorial/welcome.md)0
22 files changed, 171 insertions, 1628 deletions
diff --git a/docs/Cargo.toml b/docs/Cargo.toml
deleted file mode 100644
index b6015d01..00000000
--- a/docs/Cargo.toml
+++ /dev/null
@@ -1,27 +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
-
-[dependencies]
-typst = { path = ".." }
-typst-library = { path = "../library" }
-comemo = "0.3"
-heck = "0.4"
-include_dir = "0.7"
-once_cell = "1"
-pulldown-cmark = "0.9"
-serde = { version = "1", features = ["derive"] }
-serde_yaml = "0.8"
-syntect = { version = "5", default-features = false, features = ["parsing", "html", "regex-fancy"] }
-typed-arena = "2"
-unicode_names2 = "0.6.0"
-unscanny = "0.1"
-yaml-front-matter = "0.1"
diff --git a/docs/src/general/changelog.md b/docs/changelog.md
index 95808fab..95808fab 100644
--- a/docs/src/general/changelog.md
+++ b/docs/changelog.md
diff --git a/docs/src/general/community.md b/docs/community.md
index 32d2d185..32d2d185 100644
--- a/docs/src/general/community.md
+++ b/docs/community.md
diff --git a/docs/dev/architecture.md b/docs/dev/architecture.md
new file mode 100644
index 00000000..af48401d
--- /dev/null
+++ b/docs/dev/architecture.md
@@ -0,0 +1,171 @@
+# Typst Compiler Architecture
+Wondering how to contribute or just curious how Typst works? This document
+covers the general architecture of Typst's compiler, so you get an understanding
+of what's where and how everything fits together.
+
+The source-to-PDF compilation process of a Typst file proceeds in four phases.
+
+1. **Parsing:** Turns a source string into a syntax tree.
+2. **Evaluation:** Turns a syntax tree and its dependencies into content.
+4. **Layout:** Layouts content into frames.
+5. **Export:** Turns frames into an output format like PDF or a raster graphic.
+
+The Typst compiler is _incremental:_ Recompiling a document that was compiled
+previously is much faster than compiling from scratch. Most of the hard work is
+done by [`comemo`], an incremental compilation framework we have written for
+Typst. However, the compiler is still carefully written with incrementality in
+mind. Below we discuss the four phases and how incrementality affects each of
+them.
+
+
+## Parsing
+The syntax tree and parser are located in `src/syntax`. Parsing is a pure
+function `&str -> SyntaxNode` without any further dependencies. The result is a
+concrete syntax tree reflecting the whole file structure, including whitespace
+and comments. Parsing cannot fail. If there are syntactic errors, the returned
+syntax tree contains error nodes instead. It's important that the parser deals
+well with broken code because it is also used for syntax highlighting and IDE
+functionality.
+
+**Typedness:**
+The syntax tree is untyped, any node can have any `SyntaxKind`. This makes it
+very easy to (a) attach spans to each node (see below), (b) traverse the tree
+when doing highlighting or IDE analyses (no extra complications like a visitor
+pattern). The `typst::syntax::ast` module provides a typed API on top of
+the raw tree. This API resembles a more classical AST and is used by the
+interpreter.
+
+**Spans:**
+After parsing, the syntax tree is numbered with _span numbers._ These numbers
+are unique identifiers for syntax nodes that are used to trace back errors in
+later compilation phases to a piece of syntax. The span numbers are ordered so
+that the node corresponding to a number can be found quickly.
+
+**Incremental:**
+Typst has an incremental parser that can reparse a segment of markup or a
+code/content block. After incremental parsing, span numbers are reassigned
+locally. This way, span numbers further away from an edit stay mostly stable.
+This is important because they are used pervasively throughout the compiler,
+also as input to memoized functions. The less they change, the better for
+incremental compilation.
+
+
+## Evaluation
+The evaluation phase lives in `src/eval`. It takes a parsed `Source` file and
+evaluates it to a `Module`. A module consists of the `Content` that was written
+in it and a `Scope` with the bindings that were defined within it.
+
+A source file may depend on other files (imported sources, images, data files),
+which need to be resolved. Since Typst is deployed in different environments
+(CLI, web app, etc.) these system dependencies are resolved through a general
+interface called a `World`. Apart from files, the world also provides
+configuration and fonts.
+
+**Interpreter:**
+Typst implements a tree-walking interpreter. To evaluate a piece of source, you
+first create a `Vm` with a scope stack. Then, the AST is recursively evaluated
+through trait impls of the form `fn eval(&self, vm: &mut Vm) -> Result<Value>`.
+An interesting detail is how closures are dealt with: When the interpreter sees
+a closure / function definition, it walks the body of the closure and finds all
+accesses to variables that aren't defined within the closure. It then clones the
+values of all these variables (it _captures_ them) and stores them alongside the
+closure's syntactical definition in a closure value. When the closure is called,
+a fresh `Vm` is created and its scope stack is initialized with the captured
+variables.
+
+**Incremental:**
+In this phase, incremental compilation happens at the granularity of the module
+and the closure. Typst memoizes the result of evaluating a source file across
+compilations. Furthermore, it memoizes the result of calling a closure with a
+certain set of parameters. This is possible because Typst ensures that all
+functions are pure. The result of a closure call can be recycled if the closure
+has the same syntax and captures, even if the closure values stems from a
+different module evaluation (i.e. if a module is reevaluated, previous calls to
+closures defined in the module can still be reused).
+
+
+## Layout
+The layout phase takes `Content` and produces one `Frame` per page for it. To
+layout `Content`, we first have to _realize_ it by applying all relevant show
+rules to the content. Since show rules may be defined as Typst closures,
+realization can trigger closure evaluation, which in turn produces content that
+is recursively realized. Realization is a shallow process: While collecting list
+items into a list that we want to layout, we don't realize the content within
+the list items just yet. This only happens lazily once the list items are
+layouted.
+
+When we a have realized the content into a layoutable element, we can then
+layout it into _regions,_ which describe the space into which the content shall
+be layouted. Within these, an element is free to layout itself as it sees fit,
+returning one `Frame` per region it wants to occupy.
+
+**Introspection:**
+How content layouts (and realizes) may depend on how _it itself_ is layouted
+(e.g., through page numbers in the table of contents, counters, state, etc.).
+Typst resolves these inherently cyclical dependencies through the _introspection
+loop:_ The layout phase runs in a loop until the results stabilize. Most
+introspections stabilize after one or two iterations. However, some may never
+stabilize, so we give up after five attempts.
+
+**Incremental:**
+Layout caching happens at the granularity of the element. This is important
+because overall layout is the most expensive compilation phase, so we want to
+reuse as much as possible.
+
+
+## Export
+Exporters live in `src/export`. They turn layouted frames into an output file
+format.
+
+- The PDF exporter takes layouted frames and turns them into a PDF file.
+- The built-in renderer takes a frame and turns it into a pixel buffer.
+- HTML export does not exist yet, but will in the future. However, this requires
+ some complex compiler work because the export will start with `Content`
+ instead of `Frames` (layout is the browser's job).
+
+
+## IDE
+The `src/ide` module implements IDE functionality for Typst. It builds heavily
+on the other modules (most importantly, `syntax` and `eval`).
+
+**Syntactic:**
+Basic IDE functionality is based on a file's syntax. However, the standard
+syntax node is a bit too limited for writing IDE tooling. It doesn't provide
+access to its parents or neighbours. This is a fine for an evaluation-like
+recursive traversal, but impractical for IDE use cases. For this reason, there
+is an additional abstraction on top of a syntax node called a `LinkedNode`,
+which is used pervasively across the `ide` module.
+
+**Semantic:**
+More advanced functionality like autocompletion requires semantic analysis of
+the source. To gain semantic information for things like hover tooltips, we
+directly use other parts of the compiler. For instance, to find out the type of
+a variable, we evaluate and realize the full document equipped with a `Tracer`
+that emits the variable's value whenever it is visited. From the set of
+resulting values, we can then compute the set of types a value takes on. Thanks
+to incremental compilation, we can recycle large parts of the compilation that
+we had to do anyway to typeset the document.
+
+**Incremental:**
+Syntactic IDE stuff is relatively cheap for now, so there are no special
+incrementality concerns. Semantic analysis with a tracer is relatively
+expensive. However, large parts of a traced analysis compilation can reuse
+memoized results from a previous normal compilation. Only the module evaluation
+of the active file and layout code that somewhere within evaluates source code
+in the active file needs to re-run. This is all handled automatically by
+`comemo` because the tracer is wrapped in a `comemo::TrackedMut` container.
+
+
+## Tests
+Typst has an extensive suite of integration tests. A test file consists of
+multiple tests that are separated by `---`. For each test file, we store a
+reference image defining what the compiler _should_ output. To manage the
+reference images, you can use the VS code extension in `tools/test-helper`.
+
+The integration tests cover parsing, evaluation, realization, layout and
+rendering. PDF output is sadly untested, but most bugs are in earlier phases of
+the compiler; the PDF output itself is relatively straight-forward. IDE
+functionality is also mostly untested. PDF and IDE testing should be added in
+the future.
+
+[`comemo`]: https://github.com/typst/comemo/
diff --git a/docs/src/guides/guide-for-latex-users.md b/docs/guides/guide-for-latex-users.md
index b51e8cf1..b51e8cf1 100644
--- a/docs/src/guides/guide-for-latex-users.md
+++ b/docs/guides/guide-for-latex-users.md
diff --git a/docs/src/guides/welcome.md b/docs/guides/welcome.md
index 43ecd181..43ecd181 100644
--- a/docs/src/guides/welcome.md
+++ b/docs/guides/welcome.md
diff --git a/docs/src/general/overview.md b/docs/overview.md
index e3fa3b3c..e3fa3b3c 100644
--- a/docs/src/general/overview.md
+++ b/docs/overview.md
diff --git a/docs/src/reference/details.yml b/docs/reference/details.yml
index 8e9af3a0..8e9af3a0 100644
--- a/docs/src/reference/details.yml
+++ b/docs/reference/details.yml
diff --git a/docs/src/reference/groups.yml b/docs/reference/groups.yml
index 3bc80339..3bc80339 100644
--- a/docs/src/reference/groups.yml
+++ b/docs/reference/groups.yml
diff --git a/docs/src/reference/scripting.md b/docs/reference/scripting.md
index c18bdc4b..c18bdc4b 100644
--- a/docs/src/reference/scripting.md
+++ b/docs/reference/scripting.md
diff --git a/docs/src/reference/styling.md b/docs/reference/styling.md
index 85095e34..85095e34 100644
--- a/docs/src/reference/styling.md
+++ b/docs/reference/styling.md
diff --git a/docs/src/reference/syntax.md b/docs/reference/syntax.md
index d0cd80d7..d0cd80d7 100644
--- a/docs/src/reference/syntax.md
+++ b/docs/reference/syntax.md
diff --git a/docs/src/reference/types.md b/docs/reference/types.md
index 3e08d670..3e08d670 100644
--- a/docs/src/reference/types.md
+++ b/docs/reference/types.md
diff --git a/docs/src/reference/welcome.md b/docs/reference/welcome.md
index fc526f52..fc526f52 100644
--- a/docs/src/reference/welcome.md
+++ b/docs/reference/welcome.md
diff --git a/docs/src/contribs.rs b/docs/src/contribs.rs
deleted file mode 100644
index 901b5f69..00000000
--- a/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;
-
-use super::{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, Deserialize)]
-pub struct Commit {
- author: Author,
-}
-
-/// A commit author.
-#[derive(Debug, Deserialize)]
-pub struct Author {
- login: String,
- avatar_url: String,
-}
diff --git a/docs/src/html.rs b/docs/src/html.rs
deleted file mode 100644
index 0d40f46b..00000000
--- a/docs/src/html.rs
+++ /dev/null
@@ -1,513 +0,0 @@
-use std::ops::Range;
-
-use comemo::Prehashed;
-use pulldown_cmark as md;
-use typed_arena::Arena;
-use typst::diag::FileResult;
-use typst::eval::Datetime;
-use typst::file::FileId;
-use typst::font::{Font, FontBook};
-use typst::geom::{Point, Size};
-use typst::syntax::Source;
-use typst::util::Bytes;
-use typst::World;
-use yaml_front_matter::YamlFrontMatter;
-
-use super::*;
-
-/// HTML documentation.
-#[derive(Serialize)]
-#[serde(transparent)]
-pub struct Html {
- raw: String,
- #[serde(skip)]
- md: String,
- #[serde(skip)]
- description: Option<String>,
- #[serde(skip)]
- outline: Vec<OutlineItem>,
-}
-
-impl Html {
- /// Create HTML from a raw string.
- pub fn new(raw: String) -> Self {
- Self {
- md: String::new(),
- raw,
- description: None,
- outline: vec![],
- }
- }
-
- /// Convert markdown to HTML.
- #[track_caller]
- pub fn markdown(resolver: &dyn Resolver, md: &str) -> Self {
- Self::markdown_with_id_base(resolver, md, "")
- }
-
- /// Convert markdown to HTML, preceding all fragment identifiers with the
- /// `id_base`.
- #[track_caller]
- pub fn markdown_with_id_base(
- resolver: &dyn Resolver,
- md: &str,
- id_base: &str,
- ) -> Self {
- let mut text = md;
- let mut description = None;
- let document = YamlFrontMatter::parse::<Metadata>(md);
- if let Ok(document) = &document {
- text = &document.content;
- description = Some(document.metadata.description.clone())
- }
-
- 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 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("</h1>"))
- }
-
- /// The outline of the HTML.
- pub fn outline(&self) -> Vec<OutlineItem> {
- self.outline.clone()
- }
-
- /// The description from the front matter.
- pub fn description(&self) -> Option<String> {
- self.description.clone()
- }
-}
-
-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: String,
-}
-
-struct Handler<'a> {
- resolver: &'a dyn Resolver,
- lang: Option<String>,
- code: String,
- outline: Vec<OutlineItem>,
- id_base: String,
- ids: &'a Arena<String>,
-}
-
-impl<'a> Handler<'a> {
- fn new(resolver: &'a dyn Resolver, id_base: String, ids: &'a Arena<String>) -> Self {
- Self {
- resolver,
- lang: None,
- code: String::new(),
- outline: vec![],
- id_base,
- 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, Some(id), _)) => {
- self.handle_heading(id, level);
- }
-
- // Also handle heading closings.
- md::Event::End(md::Tag::Heading(level, Some(_), _)) => {
- if *level > md::HeadingLevel::H1 && !self.id_base.is_empty() {
- nest_heading(level);
- }
- }
-
- // Rewrite contributor sections.
- md::Event::Html(html) if html.starts_with("<contributors") => {
- let from = html_attr(html, "from").unwrap();
- let to = html_attr(html, "to").unwrap();
- 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 = self
- .handle_link(dest)
- .unwrap_or_else(|| panic!("invalid link: {dest}"))
- .into();
- }
-
- // 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::ide::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 = String::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(file) = FILES.get_file(link) {
- self.resolver.image(link, file.contents())
- } else if let Some(url) = self.resolver.link(link) {
- url
- } else {
- panic!("missing image: {link}")
- }
- }
-
- fn handle_heading(&mut self, id: &mut &'a str, level: &mut md::HeadingLevel) {
- if *level == md::HeadingLevel::H1 {
- return;
- }
-
- // Special case for things like "v0.3.0".
- let name = if id.starts_with('v') && id.contains('.') {
- id.to_string()
- } else {
- id.to_title_case()
- };
-
- let mut children = &mut self.outline;
- let mut depth = *level as usize;
- while depth > 2 {
- if !children.is_empty() {
- children = &mut children.last_mut().unwrap().children;
- }
- depth -= 1;
- }
-
- // Put base before id.
- if !self.id_base.is_empty() {
- nest_heading(level);
- *id = self.ids.alloc(format!("{}-{id}", self.id_base)).as_str();
- }
-
- children.push(OutlineItem { id: id.to_string(), name, children: vec![] });
- }
-
- fn handle_link(&self, link: &str) -> Option<String> {
- if link.starts_with('#') || link.starts_with("http") {
- return Some(link.into());
- }
-
- 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);
- }
-
- if !route.contains('#') && !route.ends_with('/') {
- route.push('/');
- }
-
- Some(route)
- }
-}
-
-/// 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") {
- let set = &*typst_library::text::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_library::text::THEME,
- )
- .expect("failed to highlight code");
- return Html::new(buf);
- }
-
- let root = typst::syntax::parse(&display);
- let highlighted = Html::new(typst::ide::highlight_html(&root));
- if lang == "typ" {
- return Html::new(format!("<pre>{}</pre>", highlighted.as_str()));
- }
-
- let id = FileId::new(None, Path::new("/main.typ"));
- let source = Source::new(id, compile);
- let world = DocWorld(source);
- let mut frames = match typst::compile(&world) {
- Ok(doc) => doc.pages,
- 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 {
- frames[0].translate(Point::new(-x, -y));
- *frames[0].size_mut() = Size::new(w, h);
- }
-
- if single {
- frames.truncate(1);
- }
-
- resolver.example(highlighted, &frames)
-}
-
-/// 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) {
- *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,
- };
-}
-
-/// 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(FILES
- .get_file(id.path().strip_prefix("/").unwrap())
- .unwrap_or_else(|| panic!("failed to load {:?}", id.path().display()))
- .contents()
- .into())
- }
-
- 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/docs/src/lib.rs b/docs/src/lib.rs
deleted file mode 100644
index afceff22..00000000
--- a/docs/src/lib.rs
+++ /dev/null
@@ -1,1002 +0,0 @@
-//! Documentation provider for Typst.
-
-mod contribs;
-mod html;
-
-pub use contribs::{contributors, Author, Commit};
-pub use html::Html;
-
-use std::fmt::{self, Debug, Formatter};
-use std::path::Path;
-
-use comemo::Prehashed;
-use heck::ToTitleCase;
-use include_dir::{include_dir, Dir};
-use once_cell::sync::Lazy;
-use serde::de::DeserializeOwned;
-use serde::{Deserialize, Serialize};
-use serde_yaml as yaml;
-use typst::doc::Frame;
-use typst::eval::{CastInfo, Func, FuncInfo, Library, Module, ParamInfo, Value};
-use typst::font::{Font, FontBook};
-use typst::geom::{Abs, Smart};
-use typst_library::layout::{Margin, PageElem};
-use unscanny::Scanner;
-
-static SRC: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/src");
-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 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 LIBRARY: Lazy<Prehashed<Library>> = Lazy::new(|| {
- let mut lib = typst_library::build();
- 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(),
- )))));
- typst::eval::set_lang_items(lib.items.clone());
- Prehashed::new(lib)
-});
-
-/// Build documentation pages.
-pub fn provide(resolver: &dyn Resolver) -> Vec<PageModel> {
- vec![
- markdown_page(resolver, "/docs/", "general/overview.md").with_route("/docs/"),
- tutorial_pages(resolver),
- reference_pages(resolver),
- guides_pages(resolver),
- packages_page(resolver),
- markdown_page(resolver, "/docs/", "general/changelog.md"),
- markdown_page(resolver, "/docs/", "general/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, source: Html, frames: &[Frame]) -> Html;
-
- /// Determine the commits between two tags.
- 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 }
- }
-}
-
-/// 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 = SRC
- .get_dir("tutorial")
- .unwrap()
- .files()
- .filter(|file| file.path() != Path::new("tutorial/welcome.md"))
- .map(|file| markdown_page(resolver, "/docs/tutorial/", file.path()))
- .collect();
- page
-}
-
-/// Build the reference.
-fn reference_pages(resolver: &dyn Resolver) -> PageModel {
- let mut page = markdown_page(resolver, "/docs/", "reference/welcome.md");
- page.children = vec![
- markdown_page(resolver, "/docs/reference/", "reference/syntax.md")
- .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, "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 {
- let mut page = markdown_page(resolver, "/docs/", "guides/welcome.md");
- page.children =
- vec![markdown_page(resolver, "/docs/guides/", "guides/guide-for-latex-users.md")];
- page
-}
-
-/// Build the packages section.
-fn packages_page(resolver: &dyn Resolver) -> PageModel {
- PageModel {
- route: "/docs/packages/".into(),
- title: "Packages".into(),
- 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 = SRC.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),
- 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>,
-}
-
-/// 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 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 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;
- }
-
- // Skip grouped functions.
- if grouped
- .iter()
- .flat_map(|group| &group.functions)
- .any(|f| f == info.name)
- {
- 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);
- }
-
- // 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);
- }
-
- 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![],
- });
- }
-
- 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.
- if category == "symbols" {
- for module in ["sym", "emoji"] {
- let subpage = symbol_page(resolver, &route, module);
- items.push(CategoryItem {
- name: module.into(),
- route: subpage.route.clone(),
- oneliner: oneliner(details(module)).into(),
- code: true,
- });
- children.push(subpage);
- }
- }
-
- let name = category.to_title_case();
- let kind = match category {
- "symbols" => "Modules",
- _ => "Functions",
- };
-
- PageModel {
- route,
- title: name.clone(),
- description: format!("Documentation for functions related to {name} in Typst."),
- part: None,
- outline: category_outline(kind),
- body: BodyModel::Category(CategoryModel {
- name,
- details: Html::markdown(resolver, details(category)),
- kind,
- items,
- }),
- 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>,
-}
-
-/// Create a page for a function.
-fn function_page(
- resolver: &dyn Resolver,
- parent: &str,
- func: &Func,
- info: &FuncInfo,
- parents: &[&'static str],
-) -> PageModel {
- let model = func_model(resolver, func, info, parents, "");
- PageModel {
- route: format!("{parent}{}/", urlify(info.name)),
- title: info.display.to_string(),
- description: format!("Documentation for the `{}` function.", info.name),
- part: None,
- outline: func_outline(&model, "", true),
- body: BodyModel::Func(model),
- children: vec![],
- }
-}
-
-/// Produce a function's model.
-fn func_model(
- resolver: &dyn Resolver,
- func: &Func,
- info: &FuncInfo,
- parents: &[&'static str],
- id_base: &str,
-) -> 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 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 returns = vec![];
- casts(resolver, &mut returns, &mut vec![], &info.returns);
- returns.sort_by_key(|ty| type_index(ty));
- if returns == ["none"] {
- returns.clear();
- }
-
- FuncModel {
- path,
- display: info.display,
- keywords: info.keywords,
- oneliner: oneliner(docs),
- 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(),
- 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![],
- });
- }
-
- 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 mut types = vec![];
- let mut strings = vec![];
- casts(resolver, &mut types, &mut strings, &info.cast);
- 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)),
- types,
- strings,
- default: info.default.map(|default| {
- let node = typst::syntax::parse_code(&default().repr());
- Html::new(typst::ide::highlight_html(&node))
- }),
- positional: info.positional,
- named: info.named,
- required: info.required,
- variadic: info.variadic,
- settable: info.settable,
- }
-}
-
-/// 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.to_string(),
- details: Html::markdown(resolver, docs),
- }),
- CastInfo::Value(..) => {}
- CastInfo::Type(ty) => types.push(ty),
- CastInfo::Union(options) => {
- for option in options {
- casts(resolver, types, strings, option);
- }
- }
- }
-}
-
-/// A collection of symbols.
-#[derive(Debug, Serialize)]
-pub struct TypeModel {
- pub name: String,
- pub oneliner: &'static str,
- pub details: Html,
- pub methods: Vec<MethodModel>,
-}
-
-/// 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>,
-}
-
-/// 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![];
-
- 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),
- 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,
- }),
- children,
- }
-}
-
-/// Produce the types' models.
-fn type_models(resolver: &dyn Resolver) -> Vec<TypeModel> {
- let file = SRC.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');
- }
-
- 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),
- }
-}
-
-/// 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### ")));
- }
- }
-
- 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()
- )
- }
-
- if name == "returns" {
- returns = types;
- continue;
- }
-
- 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),
- }
- }
-
- s.expect(')');
-
- 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,
- });
- }
-
- MethodModel {
- name,
- details: Html::markdown(resolver, docs),
- params,
- returns,
- }
-}
-
-/// 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![],
- }];
-
- outline.extend(methods_outline(&model.methods));
- 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(),
- })
-}
-
-/// 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(),
- }
-}
-
-/// 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, Serialize)]
-#[serde(rename_all = "camelCase")]
-pub struct SymbolModel {
- pub name: String,
- pub 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();
-
- let mut list = vec![];
- for (name, value) in module.scope().iter() {
- let Value::Symbol(symbol) = value else { continue };
- let complete = |variant: &str| {
- if variant.is_empty() {
- name.into()
- } else {
- format!("{}.{}", name, variant)
- }
- };
-
- for (variant, c) in symbol.variants() {
- list.push(SymbolModel {
- name: complete(variant),
- shorthand: typst::syntax::ast::Shorthand::LIST
- .iter()
- .copied()
- .find(|&(_, x)| x == c)
- .map(|(s, _)| s),
- 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()),
- alternates: symbol
- .variants()
- .filter(|(other, _)| other != &variant)
- .map(|(other, _)| complete(other))
- .collect(),
- });
- }
- }
-
- 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![],
- }
-}
-
-/// 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> {
- match parent.scope().get(name) {
- Some(Value::Module(module)) => Ok(module),
- _ => Err(format!("module doesn't contain module `{name}`")),
- }
-}
-
-/// Load YAML from a path.
-#[track_caller]
-fn yaml<T: DeserializeOwned>(path: &str) -> T {
- let file = SRC.get_file(path).unwrap();
- yaml::from_slice(file.contents()).unwrap()
-}
-
-/// Load details for an identifying key.
-#[track_caller]
-fn details(key: &str) -> &str {
- DETAILS
- .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 {
- 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",
- "boolean",
- "integer",
- "float",
- "length",
- "angle",
- "ratio",
- "relative length",
- "fraction",
- "color",
- "datetime",
- "string",
- "regex",
- "label",
- "content",
- "array",
- "dictionary",
- "function",
- "arguments",
- "location",
- "dir",
- "alignment",
- "2d alignment",
- "selector",
- "stroke",
-];
-
-#[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, _: Html, _: &[Frame]) -> Html {
- Html::new(String::new())
- }
-
- fn image(&self, _: &str, _: &[u8]) -> String {
- String::new()
- }
-
- fn commits(&self, _: &str, _: &str) -> Vec<Commit> {
- vec![]
- }
- }
-}
diff --git a/docs/src/tutorial/1-writing.md b/docs/tutorial/1-writing.md
index a2a2ca65..a2a2ca65 100644
--- a/docs/src/tutorial/1-writing.md
+++ b/docs/tutorial/1-writing.md
diff --git a/docs/src/tutorial/2-formatting.md b/docs/tutorial/2-formatting.md
index c231b878..c231b878 100644
--- a/docs/src/tutorial/2-formatting.md
+++ b/docs/tutorial/2-formatting.md
diff --git a/docs/src/tutorial/3-advanced.md b/docs/tutorial/3-advanced.md
index 42a810fd..42a810fd 100644
--- a/docs/src/tutorial/3-advanced.md
+++ b/docs/tutorial/3-advanced.md
diff --git a/docs/src/tutorial/4-template.md b/docs/tutorial/4-template.md
index 320f82b2..320f82b2 100644
--- a/docs/src/tutorial/4-template.md
+++ b/docs/tutorial/4-template.md
diff --git a/docs/src/tutorial/welcome.md b/docs/tutorial/welcome.md
index 530336cb..530336cb 100644
--- a/docs/src/tutorial/welcome.md
+++ b/docs/tutorial/welcome.md