summaryrefslogtreecommitdiff
path: root/docs/src/link.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/link.rs
parente16d3f5a67a31154797b4d56cdc6ed142ee2a7cf (diff)
Move docs generation code (#3519)
Diffstat (limited to 'docs/src/link.rs')
-rw-r--r--docs/src/link.rs108
1 files changed, 108 insertions, 0 deletions
diff --git a/docs/src/link.rs b/docs/src/link.rs
new file mode 100644
index 00000000..f4d803c3
--- /dev/null
+++ b/docs/src/link.rs
@@ -0,0 +1,108 @@
+use typst::diag::{bail, StrResult};
+use typst::foundations::Func;
+
+use crate::{get_module, GROUPS, LIBRARY};
+
+/// Resolve an intra-doc link.
+pub fn resolve(link: &str, base: &str) -> StrResult<String> {
+ if link.starts_with('#') || link.starts_with("http") {
+ return Ok(link.to_string());
+ }
+
+ let (head, tail) = split_link(link)?;
+ let mut route = match resolve_known(head, base) {
+ Some(route) => route,
+ None => resolve_definition(head, base)?,
+ };
+
+ if !tail.is_empty() {
+ route.push('/');
+ route.push_str(tail);
+ }
+
+ if !route.contains('#') && !route.ends_with('/') {
+ route.push('/');
+ }
+
+ Ok(route)
+}
+
+/// Split a link at the first slash.
+fn split_link(link: &str) -> StrResult<(&str, &str)> {
+ let first = link.split('/').next().unwrap_or(link);
+ let rest = link[first.len()..].trim_start_matches('/');
+ Ok((first, rest))
+}
+
+/// Resolve a `$` link head to a known destination.
+fn resolve_known(head: &str, base: &str) -> Option<String> {
+ Some(match head {
+ "$tutorial" => format!("{base}tutorial"),
+ "$reference" => format!("{base}reference"),
+ "$category" => format!("{base}reference"),
+ "$syntax" => format!("{base}reference/syntax"),
+ "$styling" => format!("{base}reference/styling"),
+ "$scripting" => format!("{base}reference/scripting"),
+ "$context" => format!("{base}reference/context"),
+ "$guides" => format!("{base}guides"),
+ "$packages" => format!("{base}packages"),
+ "$changelog" => format!("{base}changelog"),
+ "$community" => format!("{base}community"),
+ _ => return None,
+ })
+}
+
+/// Resolve a `$` link to a global definition.
+fn resolve_definition(head: &str, base: &str) -> StrResult<String> {
+ let mut parts = head.trim_start_matches('$').split('.').peekable();
+ let mut focus = &LIBRARY.global;
+ let mut category = None;
+
+ while let Some(name) = parts.peek() {
+ if category.is_none() {
+ category = focus.scope().get_category(name);
+ }
+ let Ok(module) = get_module(focus, name) else { break };
+ focus = module;
+ parts.next();
+ }
+
+ let Some(category) = category else { bail!("{head} has no category") };
+
+ let name = parts.next().ok_or("link is missing first part")?;
+ let value = focus.field(name)?;
+
+ // Handle grouped functions.
+ if let Some(group) = GROUPS.iter().find(|group| {
+ group.category == category.name() && group.filter.iter().any(|func| func == name)
+ }) {
+ let mut route = format!(
+ "{}reference/{}/{}/#functions-{}",
+ base, group.category, group.name, name
+ );
+ if let Some(param) = parts.next() {
+ route.push('-');
+ route.push_str(param);
+ }
+ return Ok(route);
+ }
+
+ let mut route = format!("{}reference/{}/{name}/", base, category.name());
+ if let Some(next) = parts.next() {
+ if value.field(next).is_ok() {
+ route.push_str("#definitions-");
+ route.push_str(next);
+ } else if value
+ .clone()
+ .cast::<Func>()
+ .map_or(false, |func| func.param(next).is_some())
+ {
+ route.push_str("#parameters-");
+ route.push_str(next);
+ } else {
+ bail!("field {next} not found");
+ }
+ }
+
+ Ok(route)
+}