typst/docs/src/link.rs

252 lines
7.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use typst::diag::{StrResult, bail};
use typst::foundations::{Binding, Func, Type};
use crate::{GROUPS, LIBRARY, get_module};
/// 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"),
"$html" => format!("{base}reference/html"),
"$pdf" => format!("{base}reference/pdf"),
"$guides" => format!("{base}guides"),
"$changelog" => format!("{base}changelog"),
"$universe" => "https://typst.app/universe".into(),
_ => 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(name).and_then(Binding::category);
}
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 && group.filter.iter().any(|func| func == name)
}) {
let mut route = format!(
"{}reference/{}/{}/#functions-{}",
base,
group.category.name(),
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 let Ok(field) = value.field(next, ()) {
// For top-level definitions
route.push_str("/#definitions-");
route.push_str(next);
let mut focus = field;
// For subsequent parameters, definitions, or definitions parameters
for next in parts.by_ref() {
if let Ok(field) = focus.field(next, ()) {
// For definitions
route.push_str("-definitions-");
route.push_str(next);
focus = field.clone();
} else if focus
.clone()
.cast::<Func>()
.is_ok_and(|func| func.param(next).is_some())
{
// For parameters
route.push('-');
route.push_str(next);
}
}
} else if let Ok(ty) = value.clone().cast::<Type>()
&& let Ok(func) = ty.constructor()
&& func.param(next).is_some()
{
// For parameters of a constructor function
route.push_str("/#constructor-");
route.push_str(next);
} else if value
.clone()
.cast::<Func>()
.is_ok_and(|func| func.param(next).is_some())
{
// For parameters of a function (except for constructor functions)
route.push_str("/#parameters-");
route.push_str(next);
} else {
bail!("field {next} not found");
}
if let Some(next) = parts.next() {
bail!("found redundant field {next}");
}
}
Ok(route)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_function() {
assert_eq!(
resolve_definition("$figure", "/"),
Ok("/reference/model/figure".into())
);
assert_eq!(
resolve_definition("$figure.body", "/"),
Ok("/reference/model/figure/#parameters-body".into())
);
assert_eq!(
resolve_definition("$figure.caption", "/"),
Ok("/reference/model/figure/#definitions-caption".into())
);
assert_eq!(
resolve_definition("$figure.caption.position", "/"),
Ok("/reference/model/figure/#definitions-caption-position".into())
);
}
#[test]
fn test_function_definition() {
assert_eq!(
resolve_definition("$outline", "/"),
Ok("/reference/model/outline".into())
);
assert_eq!(
resolve_definition("$outline.title", "/"),
Ok("/reference/model/outline/#parameters-title".into())
);
assert_eq!(
resolve_definition("$outline.entry", "/"),
Ok("/reference/model/outline/#definitions-entry".into())
);
assert_eq!(
resolve_definition("$outline.entry.fill", "/"),
Ok("/reference/model/outline/#definitions-entry-fill".into())
);
}
#[test]
fn test_function_definition_definition() {
assert_eq!(
resolve_definition("$outline.entry.indented", "/"),
Ok("/reference/model/outline/#definitions-entry-definitions-indented".into())
);
assert_eq!(
resolve_definition("$outline.entry.indented.prefix", "/"),
Ok("/reference/model/outline/#definitions-entry-definitions-indented-prefix"
.into())
);
}
#[test]
fn test_type() {
assert_eq!(
resolve_definition("$array", "/"),
Ok("/reference/foundations/array".into())
);
assert_eq!(
resolve_definition("$array.at", "/"),
Ok("/reference/foundations/array/#definitions-at".into())
);
assert_eq!(
resolve_definition("$array.at.index", "/"),
Ok("/reference/foundations/array/#definitions-at-index".into())
);
}
#[test]
fn test_type_constructor() {
assert_eq!(
resolve_definition("$str.base", "/"),
Ok("/reference/foundations/str/#constructor-base".into())
);
assert_eq!(
resolve_definition("$tiling.relative", "/"),
Ok("/reference/visualize/tiling/#constructor-relative".into())
);
}
#[test]
fn test_group() {
assert_eq!(
resolve_definition("$calc.abs", "/"),
Ok("/reference/foundations/calc/#functions-abs".into())
);
assert_eq!(
resolve_definition("$calc.pow.exponent", "/"),
Ok("/reference/foundations/calc/#functions-pow-exponent".into())
);
}
#[test]
fn test_redundant_field() {
assert_eq!(
resolve_definition("$figure.body.anything", "/"),
Err("found redundant field anything".into())
);
}
}