diff --git a/docs/src/general/changelog.md b/docs/src/general/changelog.md index 958c3e4a..cbd6b039 100644 --- a/docs/src/general/changelog.md +++ b/docs/src/general/changelog.md @@ -7,9 +7,12 @@ description: | # Changelog ## Unreleased - 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 watches the root directory instead of the current one +- Reduced maximum function call depth from 256 to 64 ## March 28, 2023 - **Breaking:** Enumerations now require a space after their marker, that is, diff --git a/library/src/meta/bibliography.rs b/library/src/meta/bibliography.rs index 2a0a96b1..c9532915 100644 --- a/library/src/meta/bibliography.rs +++ b/library/src/meta/bibliography.rs @@ -614,7 +614,8 @@ fn format_display_string( Formatting::Bold => content.strong(), Formatting::Italic => content.emph(), Formatting::Link(link) => { - LinkElem::new(Destination::Url(link.as_str().into()), content).pack() + LinkElem::new(Destination::Url(link.as_str().into()).into(), content) + .pack() } }; } diff --git a/library/src/meta/link.rs b/library/src/meta/link.rs index d8cb779a..135ce0af 100644 --- a/library/src/meta/link.rs +++ b/library/src/meta/link.rs @@ -13,6 +13,7 @@ use crate::text::{Hyphenate, TextElem}; /// #show link: underline /// /// https://example.com \ +/// /// #link("https://example.com") \ /// #link("https://example.com")[ /// See example.com @@ -25,7 +26,7 @@ use crate::text::{Hyphenate, TextElem}; /// /// Display: Link /// Category: meta -#[element(Show, Finalize)] +#[element(Show)] pub struct LinkElem { /// 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, /// without the scheme. /// - /// - To link to another part of the document, `dest` can take one of two - /// forms: A [`location`]($func/locate) or a dictionary with a `page` key - /// of type `integer` and `x` and `y` coordinates of type `length`. Pages - /// are counted from one, and the coordinates are relative to the page's - /// top left corner. + /// - To link to another part of the document, `dest` can take one of three + /// forms: + /// - A [label]($func/label) attached to an element. If you also want + /// automatic text for the link based on the element, consider using + /// 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 + /// = Introduction /// #link("mailto:hello@typst.app") \ + /// #link()[Go to intro] \ /// #link((page: 1, x: 0pt, y: 0pt))[ /// Go to top /// ] /// ``` #[required] #[parse( - let dest = args.expect::("destination")?; + let dest = args.expect::("destination")?; 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 - /// parameter can be omitted. In this case, the URL will be shown as the - /// link. + /// If `dest` is an URL string, the parameter can be omitted. In this case, + /// the URL will be shown as the link. #[required] #[parse(match &dest { - Destination::Url(url) => match args.eat()? { + LinkTarget::Dest(Destination::Url(url)) => match args.eat()? { Some(body) => body, None => body_from_url(url), }, @@ -73,21 +83,28 @@ impl LinkElem { /// Create a link element from a URL with its bare text. pub fn from_url(url: EcoString) -> Self { let body = body_from_url(&url); - Self::new(Destination::Url(url), body) + Self::new(LinkTarget::Dest(Destination::Url(url)), body) } } impl Show for LinkElem { - fn show(&self, _: &mut Vt, _: StyleChain) -> SourceResult { - Ok(self.body()) - } -} + fn show(&self, vt: &mut Vt, _: StyleChain) -> SourceResult { + 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 { - fn finalize(&self, realized: Content, _: StyleChain) -> Content { - realized - .linked(self.dest()) - .styled(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false)))) + let elem = vt.introspector.query_label(&label).at(self.span())?; + Destination::Location(elem.location().unwrap()) + } + }; + + 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(); 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 for LinkTarget { + fn from(dest: Destination) -> Self { + Self::Dest(dest) + } +} diff --git a/library/src/meta/reference.rs b/library/src/meta/reference.rs index 14246436..0b317db0 100644 --- a/library/src/meta/reference.rs +++ b/library/src/meta/reference.rs @@ -11,6 +11,9 @@ use crate::text::TextElem; /// /// 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 /// #set heading(numbering: "1.") @@ -93,24 +96,17 @@ impl Show for RefElem { } 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 !matches.is_empty() { + if elem.is_ok() { bail!(self.span(), "label occurs in the document and its bibliography"); } return Ok(self.to_citation(styles).pack()); } - let [elem] = matches.as_slice() else { - bail!(self.span(), if matches.is_empty() { - "label does not exist in the document" - } else { - "label occurs multiple times in the document" - }); - }; - + let elem = elem.at(self.span())?; if !elem.can::() { bail!(self.span(), "cannot reference {}", elem.func().name()); } diff --git a/src/model/introspect.rs b/src/model/introspect.rs index f0ff1169..31786d5b 100644 --- a/src/model/introspect.rs +++ b/src/model/introspect.rs @@ -3,9 +3,11 @@ use std::hash::Hash; use std::num::NonZeroUsize; use super::{Content, Selector}; +use crate::diag::StrResult; use crate::doc::{Frame, FrameItem, Meta, Position}; use crate::eval::cast_from_value; use crate::geom::{Point, Transform}; +use crate::model::Label; use crate::util::NonZeroExt; /// Stably identifies an element in the document across multiple layout passes. @@ -160,6 +162,18 @@ impl Introspector { .collect() } + /// Query for a unique element with the label. + pub fn query_label(&self, label: &Label) -> StrResult { + 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. pub fn pages(&self) -> NonZeroUsize { NonZeroUsize::new(self.pages).unwrap_or(NonZeroUsize::ONE) diff --git a/src/model/realize.rs b/src/model/realize.rs index 10e1b0e2..e96e0dc1 100644 --- a/src/model/realize.rs +++ b/src/model/realize.rs @@ -176,9 +176,8 @@ pub trait Show { /// Post-process an element after it was realized. pub trait Finalize { - /// Finalize the fully realized form of the element. Use this for effects that - /// should work even in the face of a user-defined show rule, for example - /// the linking behaviour of a link element. + /// Finalize the fully realized form of the element. Use this for effects + /// that should work even in the face of a user-defined show rule. fn finalize(&self, realized: Content, styles: StyleChain) -> Content; } diff --git a/tests/ref/meta/link.png b/tests/ref/meta/link.png index d80acc6f..4e182e9b 100644 Binary files a/tests/ref/meta/link.png and b/tests/ref/meta/link.png differ diff --git a/tests/typ/meta/link.typ b/tests/typ/meta/link.typ index 3ceb261d..de4c91c9 100644 --- a/tests/typ/meta/link.typ +++ b/tests/typ/meta/link.typ @@ -44,3 +44,18 @@ My cool #box(move(dx: 0.7cm, dy: 0.7cm, rotate(10deg, scale(200%, mylink)))) --- // Link to page one. #link((page: 1, x: 10pt, y: 20pt))[Back to the start] + +--- +// Test link to label. +Text +#link()[Go to text.] + +--- +// Error: 2-20 label does not exist in the document +#link()[Nope.] + +--- +Text +Text +// Error: 2-20 label occurs multiple times in the document +#link()[Nope.]