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
## 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,

View File

@ -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()
}
};
}

View File

@ -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 <intro>
/// #link("mailto:hello@typst.app") \
/// #link(<intro>)[Go to intro] \
/// #link((page: 1, x: 0pt, y: 0pt))[
/// Go to top
/// ]
/// ```
#[required]
#[parse(
let dest = args.expect::<Destination>("destination")?;
let dest = args.expect::<LinkTarget>("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<Content> {
Ok(self.body())
}
}
fn show(&self, vt: &mut Vt, _: StyleChain) -> SourceResult<Content> {
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<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.
///
/// 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::<dyn Locatable>() {
bail!(self.span(), "cannot reference {}", elem.func().name());
}

View File

@ -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<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.
pub fn pages(&self) -> NonZeroUsize {
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.
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;
}

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((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.]