From b45f574703f674c962e8678b4af0aabe081216a1 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Wed, 22 Jan 2025 13:58:57 +0100 Subject: [PATCH 1/2] Move no-hyphenation style in link from show to show-set rule (#5731) --- crates/typst-library/src/model/link.rs | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/crates/typst-library/src/model/link.rs b/crates/typst-library/src/model/link.rs index 4558cb394..5df6bead4 100644 --- a/crates/typst-library/src/model/link.rs +++ b/crates/typst-library/src/model/link.rs @@ -6,8 +6,8 @@ use smallvec::SmallVec; use crate::diag::{bail, warning, At, SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, Content, Label, NativeElement, Packed, Repr, Show, Smart, StyleChain, - TargetElem, + cast, elem, Content, Label, NativeElement, Packed, Repr, Show, ShowSet, Smart, + StyleChain, Styles, TargetElem, }; use crate::html::{attr, tag, HtmlElem}; use crate::introspection::Location; @@ -16,7 +16,7 @@ use crate::text::{Hyphenate, TextElem}; /// Links to a URL or a location in the document. /// -/// By default, links are not styled any different from normal text. However, +/// By default, links do not look any different from normal text. However, /// you can easily apply a style of your choice with a show rule. /// /// # Example @@ -31,6 +31,11 @@ use crate::text::{Hyphenate, TextElem}; /// ] /// ``` /// +/// # Hyphenation +/// If you enable hyphenation or justification, by default, it will not apply to +/// links to prevent unwanted hyphenation in URLs. You can opt out of this +/// default via `{show link: set text(hyphenate: true)}`. +/// /// # Syntax /// This function also has dedicated syntax: Text that starts with `http://` or /// `https://` is automatically turned into a link. @@ -119,20 +124,26 @@ impl Show for Packed { body } } else { - let linked = match &self.dest { + match &self.dest { LinkTarget::Dest(dest) => body.linked(dest.clone()), LinkTarget::Label(label) => { let elem = engine.introspector.query_label(*label).at(self.span())?; let dest = Destination::Location(elem.location().unwrap()); body.clone().linked(dest) } - }; - - linked.styled(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false)))) + } }) } } +impl ShowSet for Packed { + fn show_set(&self, _: StyleChain) -> Styles { + let mut out = Styles::new(); + out.set(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false)))); + out + } +} + fn body_from_url(url: &Url) -> Content { let text = ["mailto:", "tel:"] .into_iter() From 6fcc4322845482c1810c26ee7f6fc8f6fed20d7d Mon Sep 17 00:00:00 2001 From: Laurenz Date: Wed, 22 Jan 2025 14:24:14 +0100 Subject: [PATCH 2/2] Don't link items if container is already linked (#5732) --- crates/typst-layout/src/flow/collect.rs | 32 ++--- crates/typst-layout/src/inline/collect.rs | 20 ++-- crates/typst-layout/src/inline/line.rs | 25 ++-- crates/typst-layout/src/inline/mod.rs | 2 +- crates/typst-layout/src/inline/shaping.rs | 2 + crates/typst-layout/src/lib.rs | 1 + crates/typst-layout/src/math/fragment.rs | 15 +-- crates/typst-layout/src/math/stretch.rs | 3 +- crates/typst-layout/src/modifiers.rs | 110 ++++++++++++++++++ .../typst-library/src/foundations/content.rs | 3 +- crates/typst-library/src/layout/frame.rs | 52 +-------- crates/typst-library/src/model/link.rs | 5 +- tests/ref/issue-758-link-repeat.png | Bin 0 -> 1836 bytes tests/ref/link-empty-block.png | Bin 0 -> 96 bytes tests/ref/link-on-block.png | Bin 2422 -> 2355 bytes tests/suite/model/link.typ | 11 ++ 16 files changed, 184 insertions(+), 97 deletions(-) create mode 100644 crates/typst-layout/src/modifiers.rs create mode 100644 tests/ref/issue-758-link-repeat.png create mode 100644 tests/ref/link-empty-block.png diff --git a/crates/typst-layout/src/flow/collect.rs b/crates/typst-layout/src/flow/collect.rs index 12cfa152e..76d7b7433 100644 --- a/crates/typst-layout/src/flow/collect.rs +++ b/crates/typst-layout/src/flow/collect.rs @@ -22,6 +22,7 @@ use typst_library::text::TextElem; use typst_library::World; use super::{layout_multi_block, layout_single_block}; +use crate::modifiers::layout_and_modify; /// Collects all elements of the flow into prepared children. These are much /// simpler to handle than the raw elements. @@ -377,8 +378,9 @@ fn layout_single_impl( route: Route::extend(route), }; - layout_single_block(elem, &mut engine, locator, styles, region) - .map(|frame| frame.post_processed(styles)) + layout_and_modify(styles, |styles| { + layout_single_block(elem, &mut engine, locator, styles, region) + }) } /// A child that encapsulates a prepared breakable block. @@ -473,11 +475,8 @@ fn layout_multi_impl( route: Route::extend(route), }; - layout_multi_block(elem, &mut engine, locator, styles, regions).map(|mut fragment| { - for frame in &mut fragment { - frame.post_process(styles); - } - fragment + layout_and_modify(styles, |styles| { + layout_multi_block(elem, &mut engine, locator, styles, regions) }) } @@ -579,20 +578,23 @@ impl PlacedChild<'_> { self.cell.get_or_init(base, |base| { let align = self.alignment.unwrap_or_else(|| Alignment::CENTER); let aligned = AlignElem::set_alignment(align).wrap(); + let styles = self.styles.chain(&aligned); - let mut frame = crate::layout_frame( - engine, - &self.elem.body, - self.locator.relayout(), - self.styles.chain(&aligned), - Region::new(base, Axes::splat(false)), - )?; + let mut frame = layout_and_modify(styles, |styles| { + crate::layout_frame( + engine, + &self.elem.body, + self.locator.relayout(), + styles, + Region::new(base, Axes::splat(false)), + ) + })?; if self.float { frame.set_parent(self.elem.location().unwrap()); } - Ok(frame.post_processed(self.styles)) + Ok(frame) }) } diff --git a/crates/typst-layout/src/inline/collect.rs b/crates/typst-layout/src/inline/collect.rs index fcf7508e9..6023f5c63 100644 --- a/crates/typst-layout/src/inline/collect.rs +++ b/crates/typst-layout/src/inline/collect.rs @@ -13,6 +13,7 @@ use typst_syntax::Span; use typst_utils::Numeric; use super::*; +use crate::modifiers::{layout_and_modify, FrameModifiers, FrameModify}; // The characters by which spacing, inline content and pins are replaced in the // paragraph's full text. @@ -36,7 +37,7 @@ pub enum Item<'a> { /// Fractional spacing between other items. Fractional(Fr, Option<(&'a Packed, Locator<'a>, StyleChain<'a>)>), /// Layouted inline-level content. - Frame(Frame, StyleChain<'a>), + Frame(Frame), /// A tag. Tag(&'a Tag), /// An item that is invisible and needs to be skipped, e.g. a Unicode @@ -67,7 +68,7 @@ impl<'a> Item<'a> { match self { Self::Text(shaped) => shaped.text, Self::Absolute(_, _) | Self::Fractional(_, _) => SPACING_REPLACE, - Self::Frame(_, _) => OBJ_REPLACE, + Self::Frame(_) => OBJ_REPLACE, Self::Tag(_) => "", Self::Skip(s) => s, } @@ -83,7 +84,7 @@ impl<'a> Item<'a> { match self { Self::Text(shaped) => shaped.width, Self::Absolute(v, _) => *v, - Self::Frame(frame, _) => frame.width(), + Self::Frame(frame) => frame.width(), Self::Fractional(_, _) | Self::Tag(_) => Abs::zero(), Self::Skip(_) => Abs::zero(), } @@ -210,8 +211,10 @@ pub fn collect<'a>( InlineItem::Space(space, weak) => { collector.push_item(Item::Absolute(space, weak)); } - InlineItem::Frame(frame) => { - collector.push_item(Item::Frame(frame, styles)); + InlineItem::Frame(mut frame) => { + frame.modify(&FrameModifiers::get_in(styles)); + apply_baseline_shift(&mut frame, styles); + collector.push_item(Item::Frame(frame)); } } } @@ -222,8 +225,11 @@ pub fn collect<'a>( if let Sizing::Fr(v) = elem.width(styles) { collector.push_item(Item::Fractional(v, Some((elem, loc, styles)))); } else { - let frame = layout_box(elem, engine, loc, styles, region)?; - collector.push_item(Item::Frame(frame, styles)); + let mut frame = layout_and_modify(styles, |styles| { + layout_box(elem, engine, loc, styles, region) + })?; + apply_baseline_shift(&mut frame, styles); + collector.push_item(Item::Frame(frame)); } } else if let Some(elem) = child.to_packed::() { collector.push_item(Item::Tag(&elem.tag)); diff --git a/crates/typst-layout/src/inline/line.rs b/crates/typst-layout/src/inline/line.rs index ef7e26c3c..fba4bef80 100644 --- a/crates/typst-layout/src/inline/line.rs +++ b/crates/typst-layout/src/inline/line.rs @@ -10,6 +10,7 @@ use typst_library::text::{Lang, TextElem}; use typst_utils::Numeric; use super::*; +use crate::modifiers::layout_and_modify; const SHY: char = '\u{ad}'; const HYPHEN: char = '-'; @@ -93,7 +94,7 @@ impl Line<'_> { pub fn has_negative_width_items(&self) -> bool { self.items.iter().any(|item| match item { Item::Absolute(amount, _) => *amount < Abs::zero(), - Item::Frame(frame, _) => frame.width() < Abs::zero(), + Item::Frame(frame) => frame.width() < Abs::zero(), _ => false, }) } @@ -409,6 +410,11 @@ fn should_repeat_hyphen(pred_line: &Line, text: &str) -> bool { } } +/// Apply the current baseline shift to a frame. +pub fn apply_baseline_shift(frame: &mut Frame, styles: StyleChain) { + frame.translate(Point::with_y(TextElem::baseline_in(styles))); +} + /// Commit to a line and build its frame. #[allow(clippy::too_many_arguments)] pub fn commit( @@ -509,10 +515,11 @@ pub fn commit( let amount = v.share(fr, remaining); if let Some((elem, loc, styles)) = elem { let region = Size::new(amount, full); - let mut frame = - layout_box(elem, engine, loc.relayout(), *styles, region)?; - frame.translate(Point::with_y(TextElem::baseline_in(*styles))); - push(&mut offset, frame.post_processed(*styles)); + let mut frame = layout_and_modify(*styles, |styles| { + layout_box(elem, engine, loc.relayout(), styles, region) + })?; + apply_baseline_shift(&mut frame, *styles); + push(&mut offset, frame); } else { offset += amount; } @@ -524,12 +531,10 @@ pub fn commit( justification_ratio, extra_justification, ); - push(&mut offset, frame.post_processed(shaped.styles)); + push(&mut offset, frame); } - Item::Frame(frame, styles) => { - let mut frame = frame.clone(); - frame.translate(Point::with_y(TextElem::baseline_in(*styles))); - push(&mut offset, frame.post_processed(*styles)); + Item::Frame(frame) => { + push(&mut offset, frame.clone()); } Item::Tag(tag) => { let mut frame = Frame::soft(Size::zero()); diff --git a/crates/typst-layout/src/inline/mod.rs b/crates/typst-layout/src/inline/mod.rs index 658e30846..bedc54d63 100644 --- a/crates/typst-layout/src/inline/mod.rs +++ b/crates/typst-layout/src/inline/mod.rs @@ -23,7 +23,7 @@ use typst_library::World; use self::collect::{collect, Item, Segment, SpanMapper}; use self::deco::decorate; use self::finalize::finalize; -use self::line::{commit, line, Line}; +use self::line::{apply_baseline_shift, commit, line, Line}; use self::linebreak::{linebreak, Breakpoint}; use self::prepare::{prepare, Preparation}; use self::shaping::{ diff --git a/crates/typst-layout/src/inline/shaping.rs b/crates/typst-layout/src/inline/shaping.rs index d6b7632b6..2ed95f14f 100644 --- a/crates/typst-layout/src/inline/shaping.rs +++ b/crates/typst-layout/src/inline/shaping.rs @@ -20,6 +20,7 @@ use unicode_bidi::{BidiInfo, Level as BidiLevel}; use unicode_script::{Script, UnicodeScript}; use super::{decorate, Item, Range, SpanMapper}; +use crate::modifiers::{FrameModifiers, FrameModify}; /// The result of shaping text. /// @@ -326,6 +327,7 @@ impl<'a> ShapedText<'a> { offset += width; } + frame.modify(&FrameModifiers::get_in(self.styles)); frame } diff --git a/crates/typst-layout/src/lib.rs b/crates/typst-layout/src/lib.rs index 2e8c1129b..56d7afe11 100644 --- a/crates/typst-layout/src/lib.rs +++ b/crates/typst-layout/src/lib.rs @@ -6,6 +6,7 @@ mod image; mod inline; mod lists; mod math; +mod modifiers; mod pad; mod pages; mod repeat; diff --git a/crates/typst-layout/src/math/fragment.rs b/crates/typst-layout/src/math/fragment.rs index a0453c14f..81b726bad 100644 --- a/crates/typst-layout/src/math/fragment.rs +++ b/crates/typst-layout/src/math/fragment.rs @@ -1,23 +1,22 @@ use std::fmt::{self, Debug, Formatter}; use rustybuzz::Feature; -use smallvec::SmallVec; use ttf_parser::gsub::{AlternateSubstitution, SingleSubstitution, SubstitutionSubtable}; use ttf_parser::opentype_layout::LayoutTable; use ttf_parser::{GlyphId, Rect}; use typst_library::foundations::StyleChain; use typst_library::introspection::Tag; use typst_library::layout::{ - Abs, Axis, Corner, Em, Frame, FrameItem, HideElem, Point, Size, VAlignment, + Abs, Axis, Corner, Em, Frame, FrameItem, Point, Size, VAlignment, }; use typst_library::math::{EquationElem, MathSize}; -use typst_library::model::{Destination, LinkElem}; use typst_library::text::{Font, Glyph, Lang, Region, TextElem, TextItem}; use typst_library::visualize::Paint; use typst_syntax::Span; use unicode_math_class::MathClass; use super::{stretch_glyph, MathContext, Scaled}; +use crate::modifiers::{FrameModifiers, FrameModify}; #[derive(Debug, Clone)] pub enum MathFragment { @@ -245,8 +244,7 @@ pub struct GlyphFragment { pub class: MathClass, pub math_size: MathSize, pub span: Span, - pub dests: SmallVec<[Destination; 1]>, - pub hidden: bool, + pub modifiers: FrameModifiers, pub limits: Limits, pub extended_shape: bool, } @@ -302,8 +300,7 @@ impl GlyphFragment { accent_attach: Abs::zero(), class, span, - dests: LinkElem::dests_in(styles), - hidden: HideElem::hidden_in(styles), + modifiers: FrameModifiers::get_in(styles), extended_shape: false, }; fragment.set_id(ctx, id); @@ -390,7 +387,7 @@ impl GlyphFragment { let mut frame = Frame::soft(size); frame.set_baseline(self.ascent); frame.push(Point::with_y(self.ascent + self.shift), FrameItem::Text(item)); - frame.post_process_raw(self.dests, self.hidden); + frame.modify(&self.modifiers); frame } @@ -516,7 +513,7 @@ impl FrameFragment { let base_ascent = frame.ascent(); let accent_attach = frame.width() / 2.0; Self { - frame: frame.post_processed(styles), + frame: frame.modified(&FrameModifiers::get_in(styles)), font_size: TextElem::size_in(styles), class: EquationElem::class_in(styles).unwrap_or(MathClass::Normal), math_size: EquationElem::size_in(styles), diff --git a/crates/typst-layout/src/math/stretch.rs b/crates/typst-layout/src/math/stretch.rs index 6379bdb2e..dafa8cbe8 100644 --- a/crates/typst-layout/src/math/stretch.rs +++ b/crates/typst-layout/src/math/stretch.rs @@ -10,6 +10,7 @@ use super::{ delimiter_alignment, GlyphFragment, MathContext, MathFragment, Scaled, VariantFragment, }; +use crate::modifiers::FrameModify; /// Maximum number of times extenders can be repeated. const MAX_REPEATS: usize = 1024; @@ -265,7 +266,7 @@ fn assemble( let mut frame = Frame::soft(size); let mut offset = Abs::zero(); frame.set_baseline(baseline); - frame.post_process_raw(base.dests, base.hidden); + frame.modify(&base.modifiers); for (fragment, advance) in selected { let pos = match axis { diff --git a/crates/typst-layout/src/modifiers.rs b/crates/typst-layout/src/modifiers.rs new file mode 100644 index 000000000..ac5f40b04 --- /dev/null +++ b/crates/typst-layout/src/modifiers.rs @@ -0,0 +1,110 @@ +use typst_library::foundations::StyleChain; +use typst_library::layout::{Fragment, Frame, FrameItem, HideElem, Point}; +use typst_library::model::{Destination, LinkElem}; + +/// Frame-level modifications resulting from styles that do not impose any +/// layout structure. +/// +/// These are always applied at the highest level of style uniformity. +/// Consequently, they must be applied by all layouters that manually manage +/// styles of their children (because they can produce children with varying +/// styles). This currently includes flow, inline, and math layout. +/// +/// Other layouters don't manually need to handle it because their parents that +/// result from realization will take care of it and the styles can only apply +/// to them as a whole, not part of it (since they don't manage styles). +/// +/// Currently existing frame modifiers are: +/// - `HideElem::hidden` +/// - `LinkElem::dests` +#[derive(Debug, Clone)] +pub struct FrameModifiers { + /// A destination to link to. + dest: Option, + /// Whether the contents of the frame should be hidden. + hidden: bool, +} + +impl FrameModifiers { + /// Retrieve all modifications that should be applied per-frame. + pub fn get_in(styles: StyleChain) -> Self { + Self { + dest: LinkElem::current_in(styles), + hidden: HideElem::hidden_in(styles), + } + } +} + +/// Applies [`FrameModifiers`]. +pub trait FrameModify { + /// Apply the modifiers in-place. + fn modify(&mut self, modifiers: &FrameModifiers); + + /// Apply the modifiers, and return the modified result. + fn modified(mut self, modifiers: &FrameModifiers) -> Self + where + Self: Sized, + { + self.modify(modifiers); + self + } +} + +impl FrameModify for Frame { + fn modify(&mut self, modifiers: &FrameModifiers) { + if let Some(dest) = &modifiers.dest { + let size = self.size(); + self.push(Point::zero(), FrameItem::Link(dest.clone(), size)); + } + + if modifiers.hidden { + self.hide(); + } + } +} + +impl FrameModify for Fragment { + fn modify(&mut self, modifiers: &FrameModifiers) { + for frame in self.iter_mut() { + frame.modify(modifiers); + } + } +} + +impl FrameModify for Result +where + T: FrameModify, +{ + fn modify(&mut self, props: &FrameModifiers) { + if let Ok(inner) = self { + inner.modify(props); + } + } +} + +/// Performs layout and modification in one step. +/// +/// This just runs `layout(styles).modified(&FrameModifiers::get_in(styles))`, +/// but with the additional step that redundant modifiers (which are already +/// applied here) are removed from the `styles` passed to `layout`. This is used +/// for the layout of containers like `block`. +pub fn layout_and_modify(styles: StyleChain, layout: F) -> R +where + F: FnOnce(StyleChain) -> R, + R: FrameModify, +{ + let modifiers = FrameModifiers::get_in(styles); + + // Disable the current link internally since it's already applied at this + // level of layout. This means we don't generate redundant nested links, + // which may bloat the output considerably. + let reset; + let outer = styles; + let mut styles = styles; + if modifiers.dest.is_some() { + reset = LinkElem::set_current(None).wrap(); + styles = outer.chain(&reset); + } + + layout(styles).modified(&modifiers) +} diff --git a/crates/typst-library/src/foundations/content.rs b/crates/typst-library/src/foundations/content.rs index ab2f68ac2..76cd6a222 100644 --- a/crates/typst-library/src/foundations/content.rs +++ b/crates/typst-library/src/foundations/content.rs @@ -9,7 +9,6 @@ use std::sync::Arc; use comemo::Tracked; use ecow::{eco_format, EcoString}; use serde::{Serialize, Serializer}; -use smallvec::smallvec; use typst_syntax::Span; use typst_utils::{fat, singleton, LazyHash, SmallBitSet}; @@ -500,7 +499,7 @@ impl Content { /// Link the content somewhere. pub fn linked(self, dest: Destination) -> Self { - self.styled(LinkElem::set_dests(smallvec![dest])) + self.styled(LinkElem::set_current(Some(dest))) } /// Set alignments for this content. diff --git a/crates/typst-library/src/layout/frame.rs b/crates/typst-library/src/layout/frame.rs index e57eb27e8..a26a7d0ef 100644 --- a/crates/typst-library/src/layout/frame.rs +++ b/crates/typst-library/src/layout/frame.rs @@ -4,16 +4,13 @@ use std::fmt::{self, Debug, Formatter}; use std::num::NonZeroUsize; use std::sync::Arc; -use smallvec::SmallVec; use typst_syntax::Span; use typst_utils::{LazyHash, Numeric}; -use crate::foundations::{cast, dict, Dict, Label, StyleChain, Value}; +use crate::foundations::{cast, dict, Dict, Label, Value}; use crate::introspection::{Location, Tag}; -use crate::layout::{ - Abs, Axes, FixedAlignment, HideElem, Length, Point, Size, Transform, -}; -use crate::model::{Destination, LinkElem}; +use crate::layout::{Abs, Axes, FixedAlignment, Length, Point, Size, Transform}; +use crate::model::Destination; use crate::text::TextItem; use crate::visualize::{Color, Curve, FixedStroke, Geometry, Image, Paint, Shape}; @@ -304,49 +301,6 @@ impl Frame { } } - /// Apply late-stage properties from the style chain to this frame. This - /// includes: - /// - `HideElem::hidden` - /// - `LinkElem::dests` - /// - /// This must be called on all frames produced by elements - /// that manually handle styles (because their children can have varying - /// styles). This currently includes flow, par, and equation. - /// - /// Other elements don't manually need to handle it because their parents - /// that result from realization will take care of it and the styles can - /// only apply to them as a whole, not part of it (because they don't manage - /// styles). - pub fn post_processed(mut self, styles: StyleChain) -> Self { - self.post_process(styles); - self - } - - /// Post process in place. - pub fn post_process(&mut self, styles: StyleChain) { - if !self.is_empty() { - self.post_process_raw( - LinkElem::dests_in(styles), - HideElem::hidden_in(styles), - ); - } - } - - /// Apply raw late-stage properties from the raw data. - pub fn post_process_raw(&mut self, dests: SmallVec<[Destination; 1]>, hide: bool) { - if !self.is_empty() { - let size = self.size; - self.push_multiple( - dests - .into_iter() - .map(|dest| (Point::zero(), FrameItem::Link(dest, size))), - ); - if hide { - self.hide(); - } - } - } - /// Hide all content in the frame, but keep metadata. pub fn hide(&mut self) { Arc::make_mut(&mut self.items).retain_mut(|(_, item)| match item { diff --git a/crates/typst-library/src/model/link.rs b/crates/typst-library/src/model/link.rs index 5df6bead4..24b746b7e 100644 --- a/crates/typst-library/src/model/link.rs +++ b/crates/typst-library/src/model/link.rs @@ -1,7 +1,6 @@ use std::ops::Deref; use ecow::{eco_format, EcoString}; -use smallvec::SmallVec; use crate::diag::{bail, warning, At, SourceResult, StrResult}; use crate::engine::Engine; @@ -90,10 +89,10 @@ pub struct LinkElem { })] pub body: Content, - /// This style is set on the content contained in the `link` element. + /// A destination style that should be applied to elements. #[internal] #[ghost] - pub dests: SmallVec<[Destination; 1]>, + pub current: Option, } impl LinkElem { diff --git a/tests/ref/issue-758-link-repeat.png b/tests/ref/issue-758-link-repeat.png new file mode 100644 index 0000000000000000000000000000000000000000..aaec20d23cb31d58004b500b11d5ea28635ae274 GIT binary patch literal 1836 zcmV+{2h;e8P)XxL|x*>o^{nk*BEk@pKlbgx8XU?6O`=6OJ_s)O1{iUQ+q?>|3 z0j+>mqJUOFE1;DqpcT+cIt%EEii#&so{;`e&HDK93?hEZhFPV#RaG3%a`leO-)Vd0Apig^u2rcl6G7yEG(u@ zooa1uJ!Hs`?%lgPI5-R&Hf-|b$=W_-4IMfZWzwWcqLy!6{{0<#_3A~+pgwZs$fZk{ z)~s2Rk&z*DLqkKcpd34P?AoVrOUf;K75nYuBz=vBJm4N8A45 z#S0V$X;@g;fddD|j2V-VkdU37ee>o`hN_2$$IF*5+ZV`FL9O!a*)w6edi5&lfB^$e zo;=y!|IndB8#iuTwQAL#J$o7(8x0od9zA-D9zEK$RX%j@JkEL^yd4v>$Cj`_Y-7A#mGEOBvh)22e;jB@ZrN*Dh&;2CU{9n2}*5kZCza*3Msm+3~1D5MBsdhp=EnVFfQB6D+d6r`%EN*Lqg<4Kc~lWA~n zZfB+ux=T2l~ z?c2AltgJ?i7{PZQYi3AD2oUVrwd?%(^Syib-nVbx_3PI`!N$gB>C&Y{6&BA?qefAt zMw9oOy9Z-`{rdI5cjd|zUVs9}jvagX^5sE;1}$2&h*zcjxpU_-c)@Gkx^?~f_2UzW zz%pmfoN?pEiRR_xEj^?Zrw_xBI40n<`%Ek)YQZj^z-v0Qk^()f{;Z_TefT&y-4j0o#^Q3 zl`B`CJ9lpX{{1Xkgj3#*`D!=MRu7WuheiDvDxj4ppcT*xXa#`+S^=#@0j+>mKr87A zK$nzOs;h(7e*zkYMqOWt%2LuPqJZvtK(o~y-YRe2yb%^Qhy@#>M?5_}+3NjVE=LI{ zkX?4M^^VT=w+{1g`t)gd19qi1Z{Ez7x8Vala^%R}yLV5SG6l|wJ#Q>CX3QukDA4~D z{?Hbm%VjJ5@ZrOdWa!WWaJw)v+79poFb0q~?Bzp_u$^!CKtmuwh`@j>S+bXKy}&x(FdsrSq}e8 zM;a#35*ip%SW$TvK*B+U<@VjXcMM@U+3{y!a)bk27TWTjuD-Y8FVbRwzjOdkn02=7QpmYp%8*TRJ60RGh{ZSo|89@j*haZ zg9i_?5^&0gW>6G!4kLA8ynp}xKN_ipsUlr~44$rjF6*2UngIE&9=ZSk literal 0 HcmV?d00001 diff --git a/tests/ref/link-on-block.png b/tests/ref/link-on-block.png index 8fb7f6c6690680bda5cb700dd4718ab3e0ea043d..eeeb264b9b6696d744a72483832d0a6a0615f748 100644 GIT binary patch delta 2345 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 engsoGZ2tj9SW#bQ3K2K}0000 Text // Error: 2-20 label `` occurs multiple times in the document #link()[Nope.] + +--- link-empty-block --- +#link("", block(height: 10pt, width: 100%)) + +--- issue-758-link-repeat --- +#let url = "https://typst.org/" +#let body = [Hello #box(width: 1fr, repeat[.])] + +Inline: #link(url, body) + +#link(url, block(inset: 4pt, [Block: ] + body))