Documentation provider
This commit is contained in:
parent
ea8edfa821
commit
d4d702017c
138
Cargo.lock
generated
138
Cargo.lock
generated
@ -366,6 +366,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getopts"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
|
||||
dependencies = [
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.8"
|
||||
@ -387,6 +396,18 @@ dependencies = [
|
||||
"weezl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
|
||||
|
||||
[[package]]
|
||||
name = "hypher"
|
||||
version = "0.1.1"
|
||||
@ -447,6 +468,35 @@ dependencies = [
|
||||
"png",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "include_dir"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "18762faeff7122e89e0857b02f7ce6fcc0d101d5e9ad2ad7846cc01d61b7f19e"
|
||||
dependencies = [
|
||||
"include_dir_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "include_dir_macros"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b139284b5cf57ecfa712bcc66950bb635b31aff41c188e8a4cfc758eca374a3f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify"
|
||||
version = "0.9.6"
|
||||
@ -550,6 +600,12 @@ dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linked-hash-map"
|
||||
version = "0.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
|
||||
|
||||
[[package]]
|
||||
name = "lipsum"
|
||||
version = "0.8.2"
|
||||
@ -725,6 +781,18 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pulldown-cmark"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d9cc634bc78768157b5cbfe988ffcd1dcba95cd2b2f03a88316c08c6d00ed63"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"getopts",
|
||||
"memchr",
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.23"
|
||||
@ -920,6 +988,18 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_yaml"
|
||||
version = "0.8.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"ryu",
|
||||
"serde",
|
||||
"yaml-rust",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simplecss"
|
||||
version = "0.2.1"
|
||||
@ -1133,6 +1213,24 @@ dependencies = [
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typst-docs"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"comemo",
|
||||
"heck",
|
||||
"include_dir",
|
||||
"once_cell",
|
||||
"pulldown-cmark",
|
||||
"serde",
|
||||
"serde_yaml",
|
||||
"typst",
|
||||
"typst-library",
|
||||
"unicode_names2",
|
||||
"unscanny",
|
||||
"yaml-front-matter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typst-library"
|
||||
version = "0.1.0"
|
||||
@ -1184,6 +1282,15 @@ dependencies = [
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
|
||||
dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.8"
|
||||
@ -1243,6 +1350,12 @@ version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
|
||||
|
||||
[[package]]
|
||||
name = "unicode_names2"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "446c96c6dd42604779487f0a981060717156648c1706aa1f464677f03c6cc059"
|
||||
|
||||
[[package]]
|
||||
name = "unscanny"
|
||||
version = "0.1.0"
|
||||
@ -1269,6 +1382,12 @@ dependencies = [
|
||||
"svgtypes",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.3.2"
|
||||
@ -1445,3 +1564,22 @@ name = "xmlparser"
|
||||
version = "0.13.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d25c75bf9ea12c4040a97f829154768bbbce366287e2dc044af160cd79a13fd"
|
||||
|
||||
[[package]]
|
||||
name = "yaml-front-matter"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a94fb32d2b438e3fddf901fbfe9eb87b34d63853ca6c6da5d2ab7e27031e0bae"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_yaml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yaml-rust"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
|
||||
dependencies = [
|
||||
"linked-hash-map",
|
||||
]
|
||||
|
@ -5,7 +5,7 @@ authors = ["The Typst Project Developers"]
|
||||
edition = "2021"
|
||||
|
||||
[workspace]
|
||||
members = ["cli", "library", "macros", "tests"]
|
||||
members = ["cli", "docs", "library", "macros", "tests"]
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
|
24
docs/Cargo.toml
Normal file
24
docs/Cargo.toml
Normal file
@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "typst-docs"
|
||||
version = "0.1.0"
|
||||
authors = ["The Typst Project Developers"]
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
test = false
|
||||
doctest = false
|
||||
bench = false
|
||||
|
||||
[dependencies]
|
||||
typst = { path = ".." }
|
||||
typst-library = { path = "../library" }
|
||||
unscanny = "0.1"
|
||||
include_dir = "0.7"
|
||||
pulldown-cmark = "0.9"
|
||||
comemo = { git = "https://github.com/typst/comemo" }
|
||||
serde = "1"
|
||||
serde_yaml = "0.8"
|
||||
heck = "0.4"
|
||||
yaml-front-matter = "0.1"
|
||||
unicode_names2 = "0.6.0"
|
||||
once_cell = "1"
|
328
docs/src/html.rs
Normal file
328
docs/src/html.rs
Normal file
@ -0,0 +1,328 @@
|
||||
use comemo::Prehashed;
|
||||
use md::escape::escape_html;
|
||||
use pulldown_cmark as md;
|
||||
use typst::diag::FileResult;
|
||||
use typst::font::{Font, FontBook};
|
||||
use typst::geom::{Point, Size};
|
||||
use typst::syntax::{Source, SourceId};
|
||||
use typst::util::Buffer;
|
||||
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>,
|
||||
}
|
||||
|
||||
impl Html {
|
||||
/// Create HTML from a raw string.
|
||||
pub fn new(raw: String) -> Self {
|
||||
Self { md: String::new(), raw, description: None }
|
||||
}
|
||||
|
||||
/// Convert markdown to HTML.
|
||||
#[track_caller]
|
||||
pub fn markdown(resolver: &dyn Resolver, md: &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 mut handler = Handler::new(resolver);
|
||||
let iter = md::Parser::new_ext(text, options)
|
||||
.filter_map(|mut event| handler.handle(&mut event).then(|| 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 }
|
||||
}
|
||||
|
||||
/// 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 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>,
|
||||
}
|
||||
|
||||
impl<'a> Handler<'a> {
|
||||
fn new(resolver: &'a dyn Resolver) -> Self {
|
||||
Self { resolver, lang: None }
|
||||
}
|
||||
|
||||
fn handle(&mut self, event: &mut md::Event) -> bool {
|
||||
let lang = self.lang.take();
|
||||
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 needle = "src=\"";
|
||||
let offset = html.find(needle).unwrap() + needle.len();
|
||||
let len = html[offset..].find('"').unwrap();
|
||||
let range = offset..offset + len;
|
||||
let path = &html[range.clone()];
|
||||
let mut buf = html.to_string();
|
||||
buf.replace_range(range, &self.handle_image(path));
|
||||
*html = buf.into();
|
||||
}
|
||||
|
||||
// Rewrite links.
|
||||
md::Event::Start(md::Tag::Link(ty, dest, _)) => {
|
||||
assert!(
|
||||
matches!(ty, md::LinkType::Inline | md::LinkType::Reference),
|
||||
"unsupported link type: {ty:?}",
|
||||
);
|
||||
|
||||
let mut link = self
|
||||
.handle_link(dest)
|
||||
.unwrap_or_else(|| panic!("invalid link: {dest}"));
|
||||
|
||||
if !link.contains('#') && !link.ends_with('/') {
|
||||
link.push('/');
|
||||
}
|
||||
|
||||
*dest = link.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());
|
||||
return false;
|
||||
}
|
||||
md::Event::End(md::Tag::CodeBlock(md::CodeBlockKind::Fenced(_))) => {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Example with preview.
|
||||
md::Event::Text(text) => {
|
||||
let Some(lang) = lang.as_deref() else { return true };
|
||||
let html = code_block(self.resolver, lang, text);
|
||||
*event = md::Event::Html(html.raw.into());
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn handle_image(&self, path: &str) -> String {
|
||||
let data = IMAGES
|
||||
.get_file(path)
|
||||
.unwrap_or_else(|| panic!("missing image: {path}"))
|
||||
.contents();
|
||||
self.resolver.image(&path, data).into()
|
||||
}
|
||||
|
||||
fn handle_link(&self, link: &str) -> Option<String> {
|
||||
if link.starts_with(['#', 'h']) {
|
||||
return Some(link.into());
|
||||
} else if !link.starts_with('$') {
|
||||
return None;
|
||||
}
|
||||
|
||||
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/",
|
||||
"$community" => "/docs/community/",
|
||||
"$type" => "/docs/reference/types/",
|
||||
"$func" => "/docs/reference/",
|
||||
_ => 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('.');
|
||||
let name = parts.next()?;
|
||||
let param = parts.next();
|
||||
let value =
|
||||
LIBRARY.global.get(name).or_else(|_| LIBRARY.math.get(name)).ok()?;
|
||||
let Value::Func(func) = value else { return None };
|
||||
let info = func.info()?;
|
||||
route.push_str(info.category);
|
||||
route.push('/');
|
||||
route.push_str(name);
|
||||
if let Some(param) = param {
|
||||
route.push_str("#parameters--");
|
||||
route.push_str(param);
|
||||
}
|
||||
} else {
|
||||
route.push_str(rest);
|
||||
}
|
||||
|
||||
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;
|
||||
if let Some(args) = parts.next() {
|
||||
zoom = args
|
||||
.split(',')
|
||||
.take(4)
|
||||
.map(|s| Abs::pt(s.parse().unwrap()))
|
||||
.collect::<Vec<_>>()
|
||||
.try_into()
|
||||
.ok();
|
||||
}
|
||||
|
||||
if !matches!(lang, "example" | "typ") {
|
||||
let mut buf = String::from("<pre>");
|
||||
escape_html(&mut buf, &display).unwrap();
|
||||
buf.push_str("</pre>");
|
||||
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 source = Source::new(SourceId::from_u16(0), Path::new("main.typ"), compile);
|
||||
let world = DocWorld(source);
|
||||
let mut frame = match typst::compile(&world, &world.0) {
|
||||
Ok(doc) => doc.pages.into_iter().next().unwrap(),
|
||||
Err(err) => panic!("failed to compile {text}: {err:?}"),
|
||||
};
|
||||
|
||||
if let Some([x, y, w, h]) = zoom {
|
||||
frame.translate(Point::new(-x, -y));
|
||||
*frame.size_mut() = Size::new(w, h);
|
||||
}
|
||||
|
||||
resolver.example(highlighted, frame)
|
||||
}
|
||||
|
||||
/// 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 font(&self, id: usize) -> Option<Font> {
|
||||
Some(FONTS.1[id].clone())
|
||||
}
|
||||
|
||||
fn file(&self, path: &Path) -> FileResult<Buffer> {
|
||||
Ok(FILES
|
||||
.get_file(path)
|
||||
.unwrap_or_else(|| panic!("failed to load {path:?}"))
|
||||
.contents()
|
||||
.into())
|
||||
}
|
||||
|
||||
fn resolve(&self, _: &Path) -> FileResult<SourceId> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn source(&self, id: SourceId) -> &Source {
|
||||
assert_eq!(id.into_u16(), 0, "invalid source id");
|
||||
&self.0
|
||||
}
|
||||
}
|
744
docs/src/lib.rs
Normal file
744
docs/src/lib.rs
Normal file
@ -0,0 +1,744 @@
|
||||
//! Documentation provider for Typst.
|
||||
|
||||
mod html;
|
||||
|
||||
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::font::{Font, FontBook};
|
||||
use typst::geom::{Abs, Sides, Smart};
|
||||
use typst::model::{CastInfo, Func, FuncInfo, Library, Module, ParamInfo, Value};
|
||||
use typst_library::layout::PageNode;
|
||||
use unscanny::Scanner;
|
||||
|
||||
static SRC: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/src");
|
||||
static FILES: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../assets/files");
|
||||
static IMAGES: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../assets/images");
|
||||
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(PageNode::WIDTH, Smart::Custom(Abs::pt(240.0).into()));
|
||||
lib.styles.set(PageNode::HEIGHT, Smart::Auto);
|
||||
lib.styles
|
||||
.set(PageNode::MARGIN, Sides::splat(Some(Smart::Custom(Abs::pt(15.0).into()))));
|
||||
typst::model::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_page(resolver),
|
||||
reference_page(resolver),
|
||||
markdown_page(resolver, "/docs/", "general/changelog.md"),
|
||||
markdown_page(resolver, "/docs/", "general/community.md"),
|
||||
]
|
||||
}
|
||||
|
||||
/// Resolve consumer dependencies.
|
||||
pub trait Resolver {
|
||||
/// 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, frame: Frame) -> Html;
|
||||
}
|
||||
|
||||
/// 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 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 }
|
||||
}
|
||||
}
|
||||
|
||||
/// 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),
|
||||
}
|
||||
|
||||
/// Build the tutorial.
|
||||
fn tutorial_page(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_page(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, "basics").with_part("Content"),
|
||||
category_page(resolver, "text"),
|
||||
category_page(resolver, "math"),
|
||||
category_page(resolver, "layout"),
|
||||
category_page(resolver, "visualize"),
|
||||
category_page(resolver, "meta"),
|
||||
category_page(resolver, "symbols"),
|
||||
category_page(resolver, "foundations").with_part("Compute"),
|
||||
category_page(resolver, "calculate"),
|
||||
category_page(resolver, "construct"),
|
||||
category_page(resolver, "data-loading"),
|
||||
category_page(resolver, "utility"),
|
||||
];
|
||||
page
|
||||
}
|
||||
|
||||
/// 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,
|
||||
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"),
|
||||
_ => &LIBRARY.global,
|
||||
};
|
||||
|
||||
let grouped = match category {
|
||||
"math" => GROUPS.as_slice(),
|
||||
_ => &[],
|
||||
};
|
||||
|
||||
// Add functions.
|
||||
for (_, value) in focus.scope().iter() {
|
||||
let Value::Func(func) = value else { continue };
|
||||
let Some(info) = func.info() else { continue };
|
||||
if info.category != category {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip grouped functions.
|
||||
if grouped
|
||||
.iter()
|
||||
.flat_map(|merge| &merge.functions)
|
||||
.any(|f| f == info.name)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let subpage = function_page(resolver, &route, func, info);
|
||||
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![];
|
||||
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();
|
||||
functions.push(func_model(resolver, func, info));
|
||||
}
|
||||
|
||||
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.title.clone(),
|
||||
description: format!("Documentation for {} group of functions.", group.name),
|
||||
part: None,
|
||||
body: BodyModel::Funcs(FuncsModel {
|
||||
name: group.name.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();
|
||||
PageModel {
|
||||
route,
|
||||
title: name.clone(),
|
||||
description: format!("Documentation for functions related to {name} in Typst."),
|
||||
part: None,
|
||||
body: BodyModel::Category(CategoryModel {
|
||||
name,
|
||||
details: Html::markdown(resolver, details(category)),
|
||||
kind: match category {
|
||||
"symbols" => "Modules",
|
||||
_ => "Functions",
|
||||
},
|
||||
items,
|
||||
}),
|
||||
children,
|
||||
}
|
||||
}
|
||||
|
||||
/// Details about a function.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct FuncModel {
|
||||
pub name: &'static str,
|
||||
pub oneliner: &'static str,
|
||||
pub details: Html,
|
||||
pub showable: bool,
|
||||
pub params: Vec<ParamModel>,
|
||||
pub returns: Vec<&'static str>,
|
||||
}
|
||||
|
||||
/// Details about a group of functions.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct FuncsModel {
|
||||
pub name: 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,
|
||||
) -> PageModel {
|
||||
PageModel {
|
||||
route: format!("{parent}{}/", urlify(info.name)),
|
||||
title: info.display.to_string(),
|
||||
description: format!("Documentation for the `{}` function.", info.name),
|
||||
part: None,
|
||||
body: BodyModel::Func(func_model(resolver, func, info)),
|
||||
children: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Produce a function's model.
|
||||
fn func_model(resolver: &dyn Resolver, func: &Func, info: &FuncInfo) -> FuncModel {
|
||||
FuncModel {
|
||||
name: info.name.into(),
|
||||
oneliner: oneliner(info.docs),
|
||||
details: Html::markdown(resolver, info.docs),
|
||||
showable: func.select(None).is_ok() && info.category != "math",
|
||||
params: info.params.iter().map(|param| param_model(resolver, param)).collect(),
|
||||
returns: info.returns.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 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,
|
||||
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}{}/", 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,
|
||||
body: BodyModel::Type(model),
|
||||
children: vec![],
|
||||
});
|
||||
}
|
||||
|
||||
PageModel {
|
||||
route,
|
||||
title: "Types".into(),
|
||||
description: "Documentation for Typst's built-in types.".into(),
|
||||
part: None,
|
||||
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();
|
||||
|
||||
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### ")));
|
||||
}
|
||||
}
|
||||
|
||||
TypeModel {
|
||||
name: display.to_lowercase(),
|
||||
oneliner: oneliner(docs),
|
||||
details: Html::markdown(resolver, docs),
|
||||
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![],
|
||||
positional,
|
||||
named,
|
||||
required,
|
||||
variadic,
|
||||
settable: false,
|
||||
});
|
||||
}
|
||||
|
||||
MethodModel {
|
||||
name,
|
||||
details: Html::markdown(resolver, docs),
|
||||
params,
|
||||
returns,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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);
|
||||
|
||||
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::model::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,
|
||||
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,
|
||||
title: String,
|
||||
functions: Vec<String>,
|
||||
description: String,
|
||||
}
|
||||
|
||||
/// Extract a module from another module.
|
||||
#[track_caller]
|
||||
fn module<'a>(parent: &'a Module, name: &str) -> &'a Module {
|
||||
match parent.scope().get(name) {
|
||||
Some(Value::Module(module)) => module,
|
||||
_ => panic!("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.
|
||||
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().into()
|
||||
}
|
||||
|
||||
/// 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",
|
||||
"string",
|
||||
"regex",
|
||||
"label",
|
||||
"content",
|
||||
"array",
|
||||
"dictionary",
|
||||
"function",
|
||||
"arguments",
|
||||
"dir",
|
||||
"alignment",
|
||||
"2d alignment",
|
||||
"selector",
|
||||
"stroke",
|
||||
];
|
Loading…
x
Reference in New Issue
Block a user