Link to label
This commit is contained in:
parent
621922bb35
commit
72fb155403
@ -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,
|
||||
|
@ -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()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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 |
@ -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.]
|
||||
|
Loading…
x
Reference in New Issue
Block a user