diff options
| author | Laurenz <laurmaedje@gmail.com> | 2023-07-02 19:59:52 +0200 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2023-07-02 20:07:43 +0200 |
| commit | ebfdb1dafa430786db10dad2ef7d5467c1bdbed1 (patch) | |
| tree | 2bbc24ddb4124c4bb14dec0e536129d4de37b056 /docs | |
| parent | 3ab19185093d7709f824b95b979060ce125389d8 (diff) | |
Move everything into `crates/` directory
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/Cargo.toml | 27 | ||||
| -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.md | 171 | ||||
| -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.rs | 86 | ||||
| -rw-r--r-- | docs/src/html.rs | 513 | ||||
| -rw-r--r-- | docs/src/lib.rs | 1002 | ||||
| -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('\"', """).replace('&', "&"); - let avatar = avatar.replace("?v=", "?s=64&v="); - let s = if contributions > 1 { "s" } else { "" }; - write!( - html, - r#"<li> - <a href="https://github.com/{login}" target="_blank"> - <img - width="64" - height="64" - src="{avatar}" - alt="GitHub avatar of {login}" - title="@{login} made {contributions} contribution{s}" - crossorigin="anonymous" - > - </a> - </li>"# - ) - .unwrap(); - } - - html += "</ul>"; - - Some(Html::new(html)) -} - -#[derive(Debug)] -struct Contributor { - login: String, - avatar: String, - contributions: usize, -} - -/// A commit on the `typst` repository. -#[derive(Debug, 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 |
