summaryrefslogtreecommitdiff
path: root/crates/typst-docs/src/lib.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/typst-docs/src/lib.rs')
-rw-r--r--crates/typst-docs/src/lib.rs840
1 files changed, 0 insertions, 840 deletions
diff --git a/crates/typst-docs/src/lib.rs b/crates/typst-docs/src/lib.rs
deleted file mode 100644
index 5439ad1b..00000000
--- a/crates/typst-docs/src/lib.rs
+++ /dev/null
@@ -1,840 +0,0 @@
-//! Documentation provider for Typst.
-
-mod contribs;
-mod html;
-mod link;
-mod model;
-
-pub use self::contribs::*;
-pub use self::html::*;
-pub use self::model::*;
-
-use std::path::Path;
-
-use comemo::Prehashed;
-use ecow::{eco_format, EcoString};
-use heck::ToTitleCase;
-use include_dir::{include_dir, Dir};
-use once_cell::sync::Lazy;
-use serde::de::DeserializeOwned;
-use serde::Deserialize;
-use serde_yaml as yaml;
-use typst::diag::{bail, StrResult};
-use typst::foundations::Bytes;
-use typst::foundations::{
- CastInfo, Category, Func, Module, ParamInfo, Repr, Scope, Smart, Type, Value,
- FOUNDATIONS,
-};
-use typst::introspection::INTROSPECTION;
-use typst::layout::{Abs, Margin, PageElem, LAYOUT};
-use typst::loading::DATA_LOADING;
-use typst::math::MATH;
-use typst::model::Document;
-use typst::model::MODEL;
-use typst::symbols::SYMBOLS;
-use typst::text::{Font, FontBook, TEXT};
-use typst::visualize::VISUALIZE;
-use typst::Library;
-
-static DOCS_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../../docs");
-
-static GROUPS: Lazy<Vec<GroupData>> = Lazy::new(|| {
- let mut groups: Vec<GroupData> = yaml("reference/groups.yml");
- for group in &mut groups {
- if group.filter.is_empty() {
- group.filter = group
- .module()
- .scope()
- .iter()
- .filter(|(_, v)| matches!(v, Value::Func(_)))
- .map(|(k, _)| k.clone())
- .collect();
- }
- }
- groups
-});
-
-static LIBRARY: Lazy<Prehashed<Library>> = Lazy::new(|| {
- let mut lib = Library::default();
- lib.styles
- .set(PageElem::set_width(Smart::Custom(Abs::pt(240.0).into())));
- lib.styles.set(PageElem::set_height(Smart::Auto));
- lib.styles.set(PageElem::set_margin(Margin::splat(Some(Smart::Custom(
- Abs::pt(15.0).into(),
- )))));
- Prehashed::new(lib)
-});
-
-static FONTS: Lazy<(Prehashed<FontBook>, Vec<Font>)> = Lazy::new(|| {
- let fonts: Vec<_> = typst_assets::fonts()
- .chain(typst_dev_assets::fonts())
- .flat_map(|data| Font::iter(Bytes::from_static(data)))
- .collect();
- let book = FontBook::from_fonts(&fonts);
- (Prehashed::new(book), fonts)
-});
-
-/// Build documentation pages.
-pub fn provide(resolver: &dyn Resolver) -> Vec<PageModel> {
- vec![
- markdown_page(resolver, resolver.base(), "overview.md")
- .with_route(resolver.base()),
- tutorial_pages(resolver),
- reference_pages(resolver),
- guide_pages(resolver),
- packages_page(resolver),
- markdown_page(resolver, resolver.base(), "changelog.md"),
- markdown_page(resolver, resolver.base(), "roadmap.md"),
- markdown_page(resolver, resolver.base(), "community.md"),
- ]
-}
-
-/// Resolve consumer dependencies.
-pub trait Resolver {
- /// Try to resolve a link that the system cannot resolve itself.
- fn link(&self, link: &str) -> Option<String>;
-
- /// Produce an URL for an image file.
- fn image(&self, filename: &str, data: &[u8]) -> String;
-
- /// Produce HTML for an example.
- fn example(&self, hash: u128, source: Option<Html>, document: &Document) -> Html;
-
- /// Determine the commits between two tags.
- fn commits(&self, from: &str, to: &str) -> Vec<Commit>;
-
- /// Get the base URL for the routes and links. This must end with a slash.
- fn base(&self) -> &str;
-}
-
-/// Create a page from a markdown file.
-#[track_caller]
-fn markdown_page(
- resolver: &dyn Resolver,
- parent: &str,
- path: impl AsRef<Path>,
-) -> PageModel {
- assert!(parent.starts_with('/') && parent.ends_with('/'));
- let md = DOCS_DIR.get_file(path).unwrap().contents_utf8().unwrap();
- let html = Html::markdown(resolver, md, Some(0));
- let title: EcoString = html.title().expect("chapter lacks a title").into();
- PageModel {
- route: eco_format!("{parent}{}/", urlify(&title)),
- title,
- description: html.description().unwrap(),
- part: None,
- outline: html.outline(),
- body: BodyModel::Html(html),
- children: vec![],
- }
-}
-
-/// Build the tutorial.
-fn tutorial_pages(resolver: &dyn Resolver) -> PageModel {
- let mut page = markdown_page(resolver, resolver.base(), "tutorial/welcome.md");
- page.children = DOCS_DIR
- .get_dir("tutorial")
- .unwrap()
- .files()
- .filter(|file| file.path() != Path::new("tutorial/welcome.md"))
- .map(|file| {
- markdown_page(resolver, &format!("{}tutorial/", resolver.base()), file.path())
- })
- .collect();
- page
-}
-
-/// Build the reference.
-fn reference_pages(resolver: &dyn Resolver) -> PageModel {
- let mut page = markdown_page(resolver, resolver.base(), "reference/welcome.md");
- page.children = vec![
- markdown_page(
- resolver,
- &format!("{}reference/", resolver.base()),
- "reference/syntax.md",
- )
- .with_part("Language"),
- markdown_page(
- resolver,
- &format!("{}reference/", resolver.base()),
- "reference/styling.md",
- ),
- markdown_page(
- resolver,
- &format!("{}reference/", resolver.base()),
- "reference/scripting.md",
- ),
- markdown_page(
- resolver,
- &format!("{}reference/", resolver.base()),
- "reference/context.md",
- ),
- category_page(resolver, FOUNDATIONS).with_part("Library"),
- category_page(resolver, MODEL),
- category_page(resolver, TEXT),
- category_page(resolver, MATH),
- category_page(resolver, SYMBOLS),
- category_page(resolver, LAYOUT),
- category_page(resolver, VISUALIZE),
- category_page(resolver, INTROSPECTION),
- category_page(resolver, DATA_LOADING),
- ];
- page
-}
-
-/// Build the guides section.
-fn guide_pages(resolver: &dyn Resolver) -> PageModel {
- let mut page = markdown_page(resolver, resolver.base(), "guides/welcome.md");
- page.children = vec![
- markdown_page(
- resolver,
- &format!("{}guides/", resolver.base()),
- "guides/guide-for-latex-users.md",
- ),
- markdown_page(
- resolver,
- &format!("{}guides/", resolver.base()),
- "guides/page-setup.md",
- ),
- ];
- page
-}
-
-/// Build the packages section.
-fn packages_page(resolver: &dyn Resolver) -> PageModel {
- let md = DOCS_DIR
- .get_file("reference/packages.md")
- .unwrap()
- .contents_utf8()
- .unwrap();
- PageModel {
- route: eco_format!("{}packages/", resolver.base()),
- title: "Packages".into(),
- description: "Packages for Typst.".into(),
- part: None,
- outline: vec![],
- body: BodyModel::Packages(Html::markdown(resolver, md, Some(1))),
- children: vec![],
- }
-}
-
-/// Create a page for a category.
-#[track_caller]
-fn category_page(resolver: &dyn Resolver, category: Category) -> PageModel {
- let route = eco_format!("{}reference/{}/", resolver.base(), category.name());
- let mut children = vec![];
- let mut items = vec![];
- let mut shorthands = None;
- let mut markup = vec![];
- let mut math = vec![];
-
- let (module, path): (&Module, &[&str]) = if category == MATH {
- (&LIBRARY.math, &["math"])
- } else {
- (&LIBRARY.global, &[])
- };
-
- // Add groups.
- for group in GROUPS.iter().filter(|g| g.category == category.name()).cloned() {
- if matches!(group.name.as_str(), "sym" | "emoji") {
- let subpage = symbols_page(resolver, &route, &group);
- let BodyModel::Symbols(model) = &subpage.body else { continue };
- let list = &model.list;
- markup.extend(
- list.iter()
- .filter(|symbol| symbol.markup_shorthand.is_some())
- .cloned(),
- );
- math.extend(
- list.iter().filter(|symbol| symbol.math_shorthand.is_some()).cloned(),
- );
-
- items.push(CategoryItem {
- name: group.name.clone(),
- route: subpage.route.clone(),
- oneliner: oneliner(category.docs()).into(),
- code: true,
- });
- children.push(subpage);
- continue;
- }
-
- let (child, item) = group_page(resolver, &route, &group);
- children.push(child);
- items.push(item);
- }
-
- // Add symbol pages. These are ordered manually.
- if category == SYMBOLS {
- shorthands = Some(ShorthandsModel { markup, math });
- }
-
- // Add functions.
- let scope = module.scope();
- for (name, value) in scope.iter() {
- if scope.get_category(name) != Some(category) {
- continue;
- }
-
- if category == MATH {
- // Skip grouped functions.
- if GROUPS.iter().flat_map(|group| &group.filter).any(|f| f == name) {
- continue;
- }
-
- // Already documented in the text category.
- if name == "text" {
- continue;
- }
- }
-
- match value {
- Value::Func(func) => {
- let name = func.name().unwrap();
-
- let subpage = func_page(resolver, &route, func, path);
- items.push(CategoryItem {
- name: name.into(),
- route: subpage.route.clone(),
- oneliner: oneliner(func.docs().unwrap_or_default()).into(),
- code: true,
- });
- children.push(subpage);
- }
- Value::Type(ty) => {
- let subpage = type_page(resolver, &route, ty);
- items.push(CategoryItem {
- name: ty.short_name().into(),
- route: subpage.route.clone(),
- oneliner: oneliner(ty.docs()).into(),
- code: true,
- });
- children.push(subpage);
- }
- _ => {}
- }
- }
-
- if category != SYMBOLS {
- children.sort_by_cached_key(|child| child.title.clone());
- items.sort_by_cached_key(|item| item.name.clone());
- }
-
- let name = category.title();
- let details = Html::markdown(resolver, category.docs(), Some(1));
- let mut outline = vec![OutlineItem::from_name("Summary")];
- outline.extend(details.outline());
- outline.push(OutlineItem::from_name("Definitions"));
- if shorthands.is_some() {
- outline.push(OutlineItem::from_name("Shorthands"));
- }
-
- PageModel {
- route,
- title: name.into(),
- description: eco_format!(
- "Documentation for functions related to {name} in Typst."
- ),
- part: None,
- outline,
- body: BodyModel::Category(CategoryModel {
- name: category.name(),
- title: category.title(),
- details,
- items,
- shorthands,
- }),
- children,
- }
-}
-
-/// Create a page for a function.
-fn func_page(
- resolver: &dyn Resolver,
- parent: &str,
- func: &Func,
- path: &[&str],
-) -> PageModel {
- let model = func_model(resolver, func, path, false);
- let name = func.name().unwrap();
- PageModel {
- route: eco_format!("{parent}{}/", urlify(name)),
- title: func.title().unwrap().into(),
- description: eco_format!("Documentation for the `{name}` function."),
- part: None,
- outline: func_outline(&model, ""),
- body: BodyModel::Func(model),
- children: vec![],
- }
-}
-
-/// Produce a function's model.
-fn func_model(
- resolver: &dyn Resolver,
- func: &Func,
- path: &[&str],
- nested: bool,
-) -> FuncModel {
- let name = func.name().unwrap();
- let scope = func.scope().unwrap();
- let docs = func.docs().unwrap();
-
- let mut self_ = false;
- let mut params = func.params().unwrap();
- if params.first().map_or(false, |first| first.name == "self") {
- self_ = true;
- params = &params[1..];
- }
-
- let mut returns = vec![];
- casts(resolver, &mut returns, &mut vec![], func.returns().unwrap());
- returns.sort_by_key(|ty| type_index(ty));
- if returns == ["none"] {
- returns.clear();
- }
-
- let nesting = if nested { None } else { Some(1) };
- let (details, example) =
- if nested { split_details_and_example(docs) } else { (docs, None) };
-
- FuncModel {
- path: path.iter().copied().map(Into::into).collect(),
- name: name.into(),
- title: func.title().unwrap(),
- keywords: func.keywords(),
- oneliner: oneliner(details),
- element: func.element().is_some(),
- contextual: func.contextual().unwrap_or(false),
- details: Html::markdown(resolver, details, nesting),
- example: example.map(|md| Html::markdown(resolver, md, None)),
- self_,
- params: params.iter().map(|param| param_model(resolver, param)).collect(),
- returns,
- scope: scope_models(resolver, name, scope),
- }
-}
-
-/// Produce a parameter's model.
-fn param_model(resolver: &dyn Resolver, info: &ParamInfo) -> ParamModel {
- let (details, example) = split_details_and_example(info.docs);
-
- let mut types = vec![];
- let mut strings = vec![];
- casts(resolver, &mut types, &mut strings, &info.input);
- if !strings.is_empty() && !types.contains(&"str") {
- types.push("str");
- }
- types.sort_by_key(|ty| type_index(ty));
-
- ParamModel {
- name: info.name,
- details: Html::markdown(resolver, details, None),
- example: example.map(|md| Html::markdown(resolver, md, None)),
- types,
- strings,
- default: info.default.map(|default| {
- let node = typst::syntax::parse_code(&default().repr());
- Html::new(typst::syntax::highlight_html(&node))
- }),
- positional: info.positional,
- named: info.named,
- required: info.required,
- variadic: info.variadic,
- settable: info.settable,
- }
-}
-
-/// Split up documentation into details and an example.
-fn split_details_and_example(docs: &str) -> (&str, Option<&str>) {
- let mut details = docs;
- let mut example = None;
- if let Some(mut i) = docs.find("```") {
- while docs[..i].ends_with('`') {
- i -= 1;
- }
- details = &docs[..i];
- example = Some(&docs[i..]);
- }
- (details, example)
-}
-
-/// Process cast information into types and strings.
-fn casts(
- resolver: &dyn Resolver,
- types: &mut Vec<&'static str>,
- strings: &mut Vec<StrParam>,
- info: &CastInfo,
-) {
- match info {
- CastInfo::Any => types.push("any"),
- CastInfo::Value(Value::Str(string), docs) => strings.push(StrParam {
- string: string.clone().into(),
- details: Html::markdown(resolver, docs, None),
- }),
- CastInfo::Value(..) => {}
- CastInfo::Type(ty) => types.push(ty.short_name()),
- CastInfo::Union(options) => {
- for option in options {
- casts(resolver, types, strings, option);
- }
- }
- }
-}
-
-/// Produce models for a function's scope.
-fn scope_models(resolver: &dyn Resolver, name: &str, scope: &Scope) -> Vec<FuncModel> {
- scope
- .iter()
- .filter_map(|(_, value)| {
- let Value::Func(func) = value else { return None };
- Some(func_model(resolver, func, &[name], true))
- })
- .collect()
-}
-
-/// Produce an outline for a function page.
-fn func_outline(model: &FuncModel, id_base: &str) -> Vec<OutlineItem> {
- let mut outline = vec![];
-
- if id_base.is_empty() {
- outline.push(OutlineItem::from_name("Summary"));
- outline.extend(model.details.outline());
-
- if !model.params.is_empty() {
- outline.push(OutlineItem {
- id: "parameters".into(),
- name: "Parameters".into(),
- children: model
- .params
- .iter()
- .map(|param| OutlineItem {
- id: eco_format!("parameters-{}", urlify(param.name)),
- name: param.name.into(),
- children: vec![],
- })
- .collect(),
- });
- }
-
- outline.extend(scope_outline(&model.scope));
- } else {
- outline.extend(model.params.iter().map(|param| OutlineItem {
- id: eco_format!("{id_base}-{}", urlify(param.name)),
- name: param.name.into(),
- children: vec![],
- }));
- }
-
- outline
-}
-
-/// Produce an outline for a function scope.
-fn scope_outline(scope: &[FuncModel]) -> Option<OutlineItem> {
- if scope.is_empty() {
- return None;
- }
-
- Some(OutlineItem {
- id: "definitions".into(),
- name: "Definitions".into(),
- children: scope
- .iter()
- .map(|func| {
- let id = urlify(&eco_format!("definitions-{}", func.name));
- let children = func_outline(func, &id);
- OutlineItem { id, name: func.title.into(), children }
- })
- .collect(),
- })
-}
-
-/// Create a page for a group of functions.
-fn group_page(
- resolver: &dyn Resolver,
- parent: &str,
- group: &GroupData,
-) -> (PageModel, CategoryItem) {
- let mut functions = vec![];
- let mut outline = vec![OutlineItem::from_name("Summary")];
-
- let path: Vec<_> = group.path.iter().map(|s| s.as_str()).collect();
- let details = Html::markdown(resolver, &group.details, Some(1));
- outline.extend(details.outline());
-
- let mut outline_items = vec![];
- for name in &group.filter {
- let value = group.module().scope().get(name).unwrap();
- let Value::Func(func) = value else { panic!("not a function") };
- let func = func_model(resolver, func, &path, true);
- let id_base = urlify(&eco_format!("functions-{}", func.name));
- let children = func_outline(&func, &id_base);
- outline_items.push(OutlineItem {
- id: id_base,
- name: func.title.into(),
- children,
- });
- functions.push(func);
- }
-
- outline.push(OutlineItem {
- id: "functions".into(),
- name: "Functions".into(),
- children: outline_items,
- });
-
- let model = PageModel {
- route: eco_format!("{parent}{}", group.name),
- title: group.title.clone(),
- description: eco_format!("Documentation for the {} functions.", group.name),
- part: None,
- outline,
- body: BodyModel::Group(GroupModel {
- name: group.name.clone(),
- title: group.title.clone(),
- details,
- functions,
- }),
- children: vec![],
- };
-
- let item = CategoryItem {
- name: group.name.clone(),
- route: model.route.clone(),
- oneliner: oneliner(&group.details).into(),
- code: false,
- };
-
- (model, item)
-}
-
-/// Create a page for a type.
-fn type_page(resolver: &dyn Resolver, parent: &str, ty: &Type) -> PageModel {
- let model = type_model(resolver, ty);
- PageModel {
- route: eco_format!("{parent}{}/", urlify(ty.short_name())),
- title: ty.title().into(),
- description: eco_format!("Documentation for the {} type.", ty.title()),
- part: None,
- outline: type_outline(&model),
- body: BodyModel::Type(model),
- children: vec![],
- }
-}
-
-/// Produce a type's model.
-fn type_model(resolver: &dyn Resolver, ty: &Type) -> TypeModel {
- TypeModel {
- name: ty.short_name(),
- title: ty.title(),
- keywords: ty.keywords(),
- oneliner: oneliner(ty.docs()),
- details: Html::markdown(resolver, ty.docs(), Some(1)),
- constructor: ty
- .constructor()
- .ok()
- .map(|func| func_model(resolver, &func, &[], true)),
- scope: scope_models(resolver, ty.short_name(), ty.scope()),
- }
-}
-
-/// Produce an outline for a type page.
-fn type_outline(model: &TypeModel) -> Vec<OutlineItem> {
- let mut outline = vec![OutlineItem::from_name("Summary")];
- outline.extend(model.details.outline());
-
- if let Some(func) = &model.constructor {
- outline.push(OutlineItem {
- id: "constructor".into(),
- name: "Constructor".into(),
- children: func_outline(func, "constructor"),
- });
- }
-
- outline.extend(scope_outline(&model.scope));
- outline
-}
-
-/// Create a page for symbols.
-fn symbols_page(resolver: &dyn Resolver, parent: &str, group: &GroupData) -> PageModel {
- let model = symbols_model(resolver, group);
- PageModel {
- route: eco_format!("{parent}{}/", group.name),
- title: group.title.clone(),
- description: eco_format!("Documentation for the `{}` module.", group.name),
- part: None,
- outline: vec![],
- body: BodyModel::Symbols(model),
- children: vec![],
- }
-}
-
-/// Produce a symbol list's model.
-fn symbols_model(resolver: &dyn Resolver, group: &GroupData) -> SymbolsModel {
- let mut list = vec![];
- for (name, value) in group.module().scope().iter() {
- let Value::Symbol(symbol) = value else { continue };
- let complete = |variant: &str| {
- if variant.is_empty() {
- name.clone()
- } else {
- eco_format!("{}.{}", name, variant)
- }
- };
-
- for (variant, c) in symbol.variants() {
- let shorthand = |list: &[(&'static str, char)]| {
- list.iter().copied().find(|&(_, x)| x == c).map(|(s, _)| s)
- };
-
- list.push(SymbolModel {
- name: complete(variant),
- markup_shorthand: shorthand(typst::syntax::ast::Shorthand::MARKUP_LIST),
- math_shorthand: shorthand(typst::syntax::ast::Shorthand::MATH_LIST),
- codepoint: c as u32,
- accent: typst::symbols::Symbol::combining_accent(c).is_some(),
- unicode_name: unicode_names2::name(c)
- .map(|s| s.to_string().to_title_case().into()),
- alternates: symbol
- .variants()
- .filter(|(other, _)| other != &variant)
- .map(|(other, _)| complete(other))
- .collect(),
- });
- }
- }
-
- SymbolsModel {
- name: group.name.clone(),
- title: group.title.clone(),
- details: Html::markdown(resolver, &group.details, Some(1)),
- list,
- }
-}
-
-/// Extract a module from another module.
-#[track_caller]
-fn get_module<'a>(parent: &'a Module, name: &str) -> StrResult<&'a Module> {
- match parent.scope().get(name) {
- Some(Value::Module(module)) => Ok(module),
- _ => bail!("module doesn't contain module `{name}`"),
- }
-}
-
-/// Load YAML from a path.
-#[track_caller]
-fn yaml<T: DeserializeOwned>(path: &str) -> T {
- let file = DOCS_DIR.get_file(path).unwrap();
- yaml::from_slice(file.contents()).unwrap()
-}
-
-/// Turn a title into an URL fragment.
-pub fn urlify(title: &str) -> EcoString {
- title
- .chars()
- .map(|c| c.to_ascii_lowercase())
- .map(|c| match c {
- 'a'..='z' | '0'..='9' => c,
- _ => '-',
- })
- .collect()
-}
-
-/// Extract the first line of documentation.
-fn oneliner(docs: &str) -> &str {
- docs.lines().next().unwrap_or_default()
-}
-
-/// The order of types in the documentation.
-fn type_index(ty: &str) -> usize {
- TYPE_ORDER.iter().position(|&v| v == ty).unwrap_or(usize::MAX)
-}
-
-const TYPE_ORDER: &[&str] = &[
- "any",
- "none",
- "auto",
- "bool",
- "int",
- "float",
- "length",
- "angle",
- "ratio",
- "relative",
- "fraction",
- "color",
- "gradient",
- "datetime",
- "duration",
- "str",
- "bytes",
- "regex",
- "label",
- "content",
- "array",
- "dict",
- "func",
- "args",
- "selector",
- "location",
- "direction",
- "alignment",
- "alignment2d",
- "stroke",
-];
-
-/// Data about a collection of functions.
-#[derive(Debug, Clone, Deserialize)]
-struct GroupData {
- name: EcoString,
- title: EcoString,
- category: EcoString,
- #[serde(default)]
- path: Vec<EcoString>,
- #[serde(default)]
- filter: Vec<EcoString>,
- details: EcoString,
-}
-
-impl GroupData {
- fn module(&self) -> &'static Module {
- let mut focus = &LIBRARY.global;
- for path in &self.path {
- focus = get_module(focus, path).unwrap();
- }
- focus
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_docs() {
- provide(&TestResolver);
- }
-
- struct TestResolver;
-
- impl Resolver for TestResolver {
- fn link(&self, _: &str) -> Option<String> {
- None
- }
-
- fn example(&self, _: u128, _: Option<Html>, _: &Document) -> Html {
- Html::new(String::new())
- }
-
- fn image(&self, _: &str, _: &[u8]) -> String {
- String::new()
- }
-
- fn commits(&self, _: &str, _: &str) -> Vec<Commit> {
- vec![]
- }
-
- fn base(&self) -> &str {
- "/"
- }
- }
-}