Link to label

This commit is contained in:
Laurenz 2023-03-29 20:08:53 +02:00
parent 621922bb35
commit 72fb155403
8 changed files with 109 additions and 38 deletions

View File

@ -7,9 +7,12 @@ description: |
# Changelog # Changelog
## Unreleased ## Unreleased
- Added [`polygon`]($func/polygon) function - Added [`polygon`]($func/polygon) function
- Reduced maximum function call depth from 256 to 64 - The [`link`]($func/link) function now accepts [labels]($func/label)
- Fixed styling of text operators in math
- Fixed invalid parsing of language tag in raw block with a single backtick
- CLI now returns with non-zero status code if there is an error - CLI now returns with non-zero status code if there is an error
- CLI now watches the root directory instead of the current one - CLI now watches the root directory instead of the current one
- Reduced maximum function call depth from 256 to 64
## March 28, 2023 ## March 28, 2023
- **Breaking:** Enumerations now require a space after their marker, that is, - **Breaking:** Enumerations now require a space after their marker, that is,

View File

@ -614,7 +614,8 @@ fn format_display_string(
Formatting::Bold => content.strong(), Formatting::Bold => content.strong(),
Formatting::Italic => content.emph(), Formatting::Italic => content.emph(),
Formatting::Link(link) => { Formatting::Link(link) => {
LinkElem::new(Destination::Url(link.as_str().into()), content).pack() LinkElem::new(Destination::Url(link.as_str().into()).into(), content)
.pack()
} }
}; };
} }

View File

@ -13,6 +13,7 @@ use crate::text::{Hyphenate, TextElem};
/// #show link: underline /// #show link: underline
/// ///
/// https://example.com \ /// https://example.com \
///
/// #link("https://example.com") \ /// #link("https://example.com") \
/// #link("https://example.com")[ /// #link("https://example.com")[
/// See example.com /// See example.com
@ -25,7 +26,7 @@ use crate::text::{Hyphenate, TextElem};
/// ///
/// Display: Link /// Display: Link
/// Category: meta /// Category: meta
#[element(Show, Finalize)] #[element(Show)]
pub struct LinkElem { pub struct LinkElem {
/// The destination the link points to. /// The destination the link points to.
/// ///
@ -34,33 +35,42 @@ pub struct LinkElem {
/// omitted, the email address or phone number will be the link's body, /// omitted, the email address or phone number will be the link's body,
/// without the scheme. /// without the scheme.
/// ///
/// - To link to another part of the document, `dest` can take one of two /// - To link to another part of the document, `dest` can take one of three
/// forms: A [`location`]($func/locate) or a dictionary with a `page` key /// forms:
/// of type `integer` and `x` and `y` coordinates of type `length`. Pages /// - A [label]($func/label) attached to an element. If you also want
/// are counted from one, and the coordinates are relative to the page's /// automatic text for the link based on the element, consider using
/// top left corner. /// a [reference]($func/ref) instead.
///
/// - A [location]($func/locate) resulting from a [`locate`]($func/locate)
/// call or [`query`]($func/query).
///
/// - A dictionary with a `page` key of type [integer]($type/integer) and
/// `x` and `y` coordinates of type [length]($type/length). Pages are
/// counted from one, and the coordinates are relative to the page's top
/// left corner.
/// ///
/// ```example /// ```example
/// = Introduction <intro>
/// #link("mailto:hello@typst.app") \ /// #link("mailto:hello@typst.app") \
/// #link(<intro>)[Go to intro] \
/// #link((page: 1, x: 0pt, y: 0pt))[ /// #link((page: 1, x: 0pt, y: 0pt))[
/// Go to top /// Go to top
/// ] /// ]
/// ``` /// ```
#[required] #[required]
#[parse( #[parse(
let dest = args.expect::<Destination>("destination")?; let dest = args.expect::<LinkTarget>("destination")?;
dest.clone() dest.clone()
)] )]
pub dest: Destination, pub dest: LinkTarget,
/// How the link is represented. /// The content that should become a link.
/// ///
/// The content that should become a link. If `dest` is an URL string, the /// If `dest` is an URL string, the parameter can be omitted. In this case,
/// parameter can be omitted. In this case, the URL will be shown as the /// the URL will be shown as the link.
/// link.
#[required] #[required]
#[parse(match &dest { #[parse(match &dest {
Destination::Url(url) => match args.eat()? { LinkTarget::Dest(Destination::Url(url)) => match args.eat()? {
Some(body) => body, Some(body) => body,
None => body_from_url(url), None => body_from_url(url),
}, },
@ -73,21 +83,28 @@ impl LinkElem {
/// Create a link element from a URL with its bare text. /// Create a link element from a URL with its bare text.
pub fn from_url(url: EcoString) -> Self { pub fn from_url(url: EcoString) -> Self {
let body = body_from_url(&url); let body = body_from_url(&url);
Self::new(Destination::Url(url), body) Self::new(LinkTarget::Dest(Destination::Url(url)), body)
} }
} }
impl Show for LinkElem { impl Show for LinkElem {
fn show(&self, _: &mut Vt, _: StyleChain) -> SourceResult<Content> { fn show(&self, vt: &mut Vt, _: StyleChain) -> SourceResult<Content> {
Ok(self.body()) let body = self.body();
} let dest = match self.dest() {
} LinkTarget::Dest(dest) => dest,
LinkTarget::Label(label) => {
if !vt.introspector.init() {
return Ok(body);
}
impl Finalize for LinkElem { let elem = vt.introspector.query_label(&label).at(self.span())?;
fn finalize(&self, realized: Content, _: StyleChain) -> Content { Destination::Location(elem.location().unwrap())
realized }
.linked(self.dest()) };
.styled(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false))))
Ok(body
.linked(dest)
.styled(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false)))))
} }
} }
@ -99,3 +116,29 @@ fn body_from_url(url: &EcoString) -> Content {
let shorter = text.len() < url.len(); let shorter = text.len() < url.len();
TextElem::packed(if shorter { text.into() } else { url.clone() }) TextElem::packed(if shorter { text.into() } else { url.clone() })
} }
/// A target where a link can go.
#[derive(Debug, Clone)]
pub enum LinkTarget {
Dest(Destination),
Label(Label),
}
cast_from_value! {
LinkTarget,
v: Destination => Self::Dest(v),
v: Label => Self::Label(v),
}
cast_to_value! {
v: LinkTarget => match v {
LinkTarget::Dest(v) => v.into(),
LinkTarget::Label(v) => v.into(),
}
}
impl From<Destination> for LinkTarget {
fn from(dest: Destination) -> Self {
Self::Dest(dest)
}
}

View File

@ -11,6 +11,9 @@ use crate::text::TextElem;
/// ///
/// Reference syntax can also be used to [cite]($func/cite) from a bibliography. /// Reference syntax can also be used to [cite]($func/cite) from a bibliography.
/// ///
/// If you just want to link to a labelled element and not get an automatic
/// textual reference, consider using the [`link`]($func/link) function instead.
///
/// # Example /// # Example
/// ```example /// ```example
/// #set heading(numbering: "1.") /// #set heading(numbering: "1.")
@ -93,24 +96,17 @@ impl Show for RefElem {
} }
let target = self.target(); let target = self.target();
let matches = vt.introspector.query(Selector::Label(self.target())); let elem = vt.introspector.query_label(&self.target());
if BibliographyElem::has(vt, &target.0) { if BibliographyElem::has(vt, &target.0) {
if !matches.is_empty() { if elem.is_ok() {
bail!(self.span(), "label occurs in the document and its bibliography"); bail!(self.span(), "label occurs in the document and its bibliography");
} }
return Ok(self.to_citation(styles).pack()); return Ok(self.to_citation(styles).pack());
} }
let [elem] = matches.as_slice() else { let elem = elem.at(self.span())?;
bail!(self.span(), if matches.is_empty() {
"label does not exist in the document"
} else {
"label occurs multiple times in the document"
});
};
if !elem.can::<dyn Locatable>() { if !elem.can::<dyn Locatable>() {
bail!(self.span(), "cannot reference {}", elem.func().name()); bail!(self.span(), "cannot reference {}", elem.func().name());
} }

View File

@ -3,9 +3,11 @@ use std::hash::Hash;
use std::num::NonZeroUsize; use std::num::NonZeroUsize;
use super::{Content, Selector}; use super::{Content, Selector};
use crate::diag::StrResult;
use crate::doc::{Frame, FrameItem, Meta, Position}; use crate::doc::{Frame, FrameItem, Meta, Position};
use crate::eval::cast_from_value; use crate::eval::cast_from_value;
use crate::geom::{Point, Transform}; use crate::geom::{Point, Transform};
use crate::model::Label;
use crate::util::NonZeroExt; use crate::util::NonZeroExt;
/// Stably identifies an element in the document across multiple layout passes. /// Stably identifies an element in the document across multiple layout passes.
@ -160,6 +162,18 @@ impl Introspector {
.collect() .collect()
} }
/// Query for a unique element with the label.
pub fn query_label(&self, label: &Label) -> StrResult<Content> {
let mut found = None;
for elem in self.all().filter(|elem| elem.label() == Some(label)) {
if found.is_some() {
return Err("label occurs multiple times in the document".into());
}
found = Some(elem.clone());
}
found.ok_or_else(|| "label does not exist in the document".into())
}
/// The total number pages. /// The total number pages.
pub fn pages(&self) -> NonZeroUsize { pub fn pages(&self) -> NonZeroUsize {
NonZeroUsize::new(self.pages).unwrap_or(NonZeroUsize::ONE) NonZeroUsize::new(self.pages).unwrap_or(NonZeroUsize::ONE)

View File

@ -176,9 +176,8 @@ pub trait Show {
/// Post-process an element after it was realized. /// Post-process an element after it was realized.
pub trait Finalize { pub trait Finalize {
/// Finalize the fully realized form of the element. Use this for effects that /// Finalize the fully realized form of the element. Use this for effects
/// should work even in the face of a user-defined show rule, for example /// that should work even in the face of a user-defined show rule.
/// the linking behaviour of a link element.
fn finalize(&self, realized: Content, styles: StyleChain) -> Content; fn finalize(&self, realized: Content, styles: StyleChain) -> Content;
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 51 KiB

View File

@ -44,3 +44,18 @@ My cool #box(move(dx: 0.7cm, dy: 0.7cm, rotate(10deg, scale(200%, mylink))))
--- ---
// Link to page one. // Link to page one.
#link((page: 1, x: 10pt, y: 20pt))[Back to the start] #link((page: 1, x: 10pt, y: 20pt))[Back to the start]
---
// Test link to label.
Text <hey>
#link(<hey>)[Go to text.]
---
// Error: 2-20 label does not exist in the document
#link(<hey>)[Nope.]
---
Text <hey>
Text <hey>
// Error: 2-20 label occurs multiple times in the document
#link(<hey>)[Nope.]