summaryrefslogtreecommitdiff
path: root/docs/src/lib.rs
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2024-02-28 11:37:52 +0100
committerGitHub <noreply@github.com>2024-02-28 10:37:52 +0000
commita518e2dd4d829b45b0887da28acb77d0568894ab (patch)
treee7387b27e299383a0bd1a26ad58f485630cb23b7 /docs/src/lib.rs
parente16d3f5a67a31154797b4d56cdc6ed142ee2a7cf (diff)
Move docs generation code (#3519)
Diffstat (limited to 'docs/src/lib.rs')
-rw-r--r--docs/src/lib.rs803
1 files changed, 803 insertions, 0 deletions
diff --git a/docs/src/lib.rs b/docs/src/lib.rs
new file mode 100644
index 00000000..0bb9fc86
--- /dev/null
+++ b/docs/src/lib.rs
@@ -0,0 +1,803 @@
+//! 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 comemo::Prehashed;
+use ecow::{eco_format, EcoString};
+use heck::ToTitleCase;
+use once_cell::sync::Lazy;
+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;
+
+macro_rules! load {
+ ($path:literal) => {
+ include_str!(concat!("../", $path))
+ };
+}
+
+static GROUPS: Lazy<Vec<GroupData>> = Lazy::new(|| {
+ let mut groups: Vec<GroupData> =
+ yaml::from_str(load!("reference/groups.yml")).unwrap();
+ 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> {
+ let base = resolver.base();
+ vec![
+ md_page(resolver, base, load!("overview.md")).with_route(base),
+ tutorial_pages(resolver),
+ reference_pages(resolver),
+ guide_pages(resolver),
+ packages_page(resolver),
+ md_page(resolver, base, load!("changelog.md")),
+ md_page(resolver, base, load!("roadmap.md")),
+ md_page(resolver, base, load!("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 md_page(resolver: &dyn Resolver, parent: &str, md: &str) -> PageModel {
+ assert!(parent.starts_with('/') && parent.ends_with('/'));
+ 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 = md_page(resolver, resolver.base(), load!("tutorial/welcome.md"));
+ let base = format!("{}tutorial/", resolver.base());
+ page.children = vec![
+ md_page(resolver, &base, load!("tutorial/1-writing.md")),
+ md_page(resolver, &base, load!("tutorial/2-formatting.md")),
+ md_page(resolver, &base, load!("tutorial/3-advanced.md")),
+ md_page(resolver, &base, load!("tutorial/4-template.md")),
+ ];
+ page
+}
+
+/// Build the reference.
+fn reference_pages(resolver: &dyn Resolver) -> PageModel {
+ let mut page = md_page(resolver, resolver.base(), load!("reference/welcome.md"));
+ let base = format!("{}reference/", resolver.base());
+ page.children = vec![
+ md_page(resolver, &base, load!("reference/syntax.md")).with_part("Language"),
+ md_page(resolver, &base, load!("reference/styling.md")),
+ md_page(resolver, &base, load!("reference/scripting.md")),
+ md_page(resolver, &base, load!("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 = md_page(resolver, resolver.base(), load!("guides/welcome.md"));
+ let base = format!("{}guides/", resolver.base());
+ page.children = vec![
+ md_page(resolver, &base, load!("guides/guide-for-latex-users.md")),
+ md_page(resolver, &base, load!("guides/page-setup.md")),
+ ];
+ page
+}
+
+/// Build the packages section.
+fn packages_page(resolver: &dyn Resolver) -> PageModel {
+ 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,
+ load!("reference/packages.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}`"),
+ }
+}
+
+/// 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 {
+ "/"
+ }
+ }
+}