From a52987a8c26bc13e3db4cc300b8cd9d81eb8a18d Mon Sep 17 00:00:00 2001 From: Laurenz Date: Wed, 22 May 2024 11:26:03 +0200 Subject: [PATCH] Refactor frame metadata into tags (#4212) --- crates/typst-ide/src/jump.rs | 3 +- crates/typst-pdf/src/page.rs | 8 +- crates/typst-render/src/lib.rs | 8 +- crates/typst-svg/src/lib.rs | 8 +- crates/typst/src/foundations/content.rs | 10 +- crates/typst/src/introspection/counter.rs | 4 +- .../typst/src/introspection/introspector.rs | 12 +-- crates/typst/src/introspection/locator.rs | 4 +- crates/typst/src/introspection/mod.rs | 89 ++++++---------- crates/typst/src/layout/flow.rs | 99 +++++++++++------- crates/typst/src/layout/frame.rs | 85 +++++++++------ crates/typst/src/layout/hide.rs | 10 +- crates/typst/src/layout/inline/mod.rs | 50 +++++---- crates/typst/src/math/fragment.rs | 14 +-- crates/typst/src/math/mod.rs | 15 ++- crates/typst/src/math/stretch.rs | 2 +- crates/typst/src/model/link.rs | 6 ++ crates/typst/src/realize/mod.rs | 8 +- crates/typst/src/realize/process.rs | 35 +++---- tests/ref/eval-mode.png | Bin 881 -> 850 bytes tests/ref/issue-785-cite-locate.png | Bin 15456 -> 15194 bytes tests/ref/link-on-block.png | Bin 2423 -> 2422 bytes tests/ref/locate-position-trailing-tag.png | Bin 0 -> 74 bytes tests/ref/quote-cite-format-note.png | Bin 2943 -> 2941 bytes tests/src/run.rs | 5 +- tests/suite/introspection/locate.typ | 7 ++ 26 files changed, 257 insertions(+), 225 deletions(-) create mode 100644 tests/ref/locate-position-trailing-tag.png diff --git a/crates/typst-ide/src/jump.rs b/crates/typst-ide/src/jump.rs index 7c3c7569b..45186d5ba 100644 --- a/crates/typst-ide/src/jump.rs +++ b/crates/typst-ide/src/jump.rs @@ -1,7 +1,6 @@ use std::num::NonZeroUsize; use ecow::EcoString; -use typst::introspection::Meta; use typst::layout::{Frame, FrameItem, Point, Position, Size}; use typst::model::{Destination, Document}; use typst::syntax::{FileId, LinkedNode, Side, Source, Span, SyntaxKind}; @@ -37,7 +36,7 @@ pub fn jump_from_click( ) -> Option { // Try to find a link first. for (pos, item) in frame.items() { - if let FrameItem::Meta(Meta::Link(dest), size) = item { + if let FrameItem::Link(dest, size) = item { if is_in_rect(*pos, *size, click) { return Some(match dest { Destination::Url(url) => Jump::Url(url.clone()), diff --git a/crates/typst-pdf/src/page.rs b/crates/typst-pdf/src/page.rs index 557daa804..42c87f8e0 100644 --- a/crates/typst-pdf/src/page.rs +++ b/crates/typst-pdf/src/page.rs @@ -12,7 +12,6 @@ use pdf_writer::types::{ }; use pdf_writer::writers::{PageLabel, Resources}; use pdf_writer::{Content, Filter, Finish, Name, Rect, Ref, Str, TextStr}; -use typst::introspection::Meta; use typst::layout::{ Abs, Em, Frame, FrameItem, GroupItem, Page, Point, Ratio, Size, Transform, }; @@ -749,11 +748,8 @@ pub(crate) fn write_frame(ctx: &mut PageContext, frame: &Frame) { FrameItem::Text(text) => write_text(ctx, pos, text), FrameItem::Shape(shape, _) => write_shape(ctx, pos, shape), FrameItem::Image(image, size, _) => write_image(ctx, x, y, image, *size), - FrameItem::Meta(meta, size) => match meta { - Meta::Link(dest) => write_link(ctx, pos, dest, *size), - Meta::Elem(_) => {} - Meta::Hide => {} - }, + FrameItem::Link(dest, size) => write_link(ctx, pos, dest, *size), + FrameItem::Tag(_) => {} } } } diff --git a/crates/typst-render/src/lib.rs b/crates/typst-render/src/lib.rs index 08c80050f..305dcd1fc 100644 --- a/crates/typst-render/src/lib.rs +++ b/crates/typst-render/src/lib.rs @@ -6,7 +6,6 @@ mod shape; mod text; use tiny_skia as sk; -use typst::introspection::Meta; use typst::layout::{ Abs, Axes, Frame, FrameItem, FrameKind, GroupItem, Point, Size, Transform, }; @@ -162,11 +161,8 @@ fn render_frame(canvas: &mut sk::Pixmap, state: State, frame: &Frame) { FrameItem::Image(image, size, _) => { image::render_image(canvas, state.pre_translate(*pos), image, *size); } - FrameItem::Meta(meta, _) => match meta { - Meta::Link(_) => {} - Meta::Elem(_) => {} - Meta::Hide => {} - }, + FrameItem::Link(_, _) => {} + FrameItem::Tag(_) => {} } } } diff --git a/crates/typst-svg/src/lib.rs b/crates/typst-svg/src/lib.rs index 798a354c2..01ed3faee 100644 --- a/crates/typst-svg/src/lib.rs +++ b/crates/typst-svg/src/lib.rs @@ -184,8 +184,9 @@ impl SVGRenderer { } for (pos, item) in frame.items() { - // File size optimization - if matches!(item, FrameItem::Meta(_, _)) { + // File size optimization. + // TODO: SVGs could contain links, couldn't they? + if matches!(item, FrameItem::Link(_, _) | FrameItem::Tag(_)) { continue; } @@ -206,7 +207,8 @@ impl SVGRenderer { self.render_shape(state.pre_translate(*pos), shape) } FrameItem::Image(image, size, _) => self.render_image(image, size), - FrameItem::Meta(_, _) => unreachable!(), + FrameItem::Link(_, _) => unreachable!(), + FrameItem::Tag(_) => unreachable!(), }; self.xml.end_element(); diff --git a/crates/typst/src/foundations/content.rs b/crates/typst/src/foundations/content.rs index f11c74c09..6f8e46b48 100644 --- a/crates/typst/src/foundations/content.rs +++ b/crates/typst/src/foundations/content.rs @@ -18,9 +18,9 @@ use crate::foundations::{ NativeElement, Recipe, RecipeIndex, Repr, Selector, Str, Style, StyleChain, Styles, Value, }; -use crate::introspection::{Location, Meta, MetaElem}; +use crate::introspection::{Location, TagElem}; use crate::layout::{AlignElem, Alignment, Axes, Length, MoveElem, PadElem, Rel, Sides}; -use crate::model::{Destination, EmphElem, StrongElem}; +use crate::model::{Destination, EmphElem, LinkElem, StrongElem}; use crate::realize::{Behave, Behaviour}; use crate::syntax::Span; use crate::text::UnderlineElem; @@ -472,16 +472,16 @@ impl Content { /// Link the content somewhere. pub fn linked(self, dest: Destination) -> Self { - self.styled(MetaElem::set_data(smallvec![Meta::Link(dest)])) + self.styled(LinkElem::set_dests(smallvec![dest])) } /// Make the content linkable by `.linked(Destination::Location(loc))`. /// /// Should be used in combination with [`Location::variant`]. pub fn backlinked(self, loc: Location) -> Self { - let mut backlink = Content::empty(); + let mut backlink = Content::empty().spanned(self.span()); backlink.set_location(loc); - self.styled(MetaElem::set_data(smallvec![Meta::Elem(backlink)])) + TagElem::packed(backlink) + self } /// Set alignments for this content. diff --git a/crates/typst/src/introspection/counter.rs b/crates/typst/src/introspection/counter.rs index 93f954a2e..82f8bbfc0 100644 --- a/crates/typst/src/introspection/counter.rs +++ b/crates/typst/src/introspection/counter.rs @@ -13,7 +13,7 @@ use crate::foundations::{ Element, Func, IntoValue, Label, LocatableSelector, NativeElement, Packed, Repr, Selector, Show, Smart, Str, StyleChain, Value, }; -use crate::introspection::{Introspector, Locatable, Location, Locator, Meta}; +use crate::introspection::{Introspector, Locatable, Location, Locator}; use crate::layout::{Frame, FrameItem, PageElem}; use crate::math::EquationElem; use crate::model::{FigureElem, HeadingElem, Numbering, NumberingPattern}; @@ -820,7 +820,7 @@ impl ManualPageCounter { for (_, item) in page.items() { match item { FrameItem::Group(group) => self.visit(engine, &group.frame)?, - FrameItem::Meta(Meta::Elem(elem), _) => { + FrameItem::Tag(elem) => { let Some(elem) = elem.to_packed::() else { continue; }; diff --git a/crates/typst/src/introspection/introspector.rs b/crates/typst/src/introspection/introspector.rs index 93dec21c7..b6c32a47b 100644 --- a/crates/typst/src/introspection/introspector.rs +++ b/crates/typst/src/introspection/introspector.rs @@ -10,7 +10,7 @@ use smallvec::SmallVec; use crate::diag::{bail, StrResult}; use crate::foundations::{Content, Label, Repr, Selector}; -use crate::introspection::{Location, Meta}; +use crate::introspection::Location; use crate::layout::{Frame, FrameItem, Page, Point, Position, Transform}; use crate::model::Numbering; use crate::utils::NonZeroExt; @@ -61,18 +61,18 @@ impl Introspector { .pre_concat(group.transform); self.extract(&group.frame, page, ts); } - FrameItem::Meta(Meta::Elem(content), _) - if !self.elems.contains_key(&content.location().unwrap()) => + FrameItem::Tag(elem) + if !self.elems.contains_key(&elem.location().unwrap()) => { let pos = pos.transform(ts); let ret = self.elems.insert( - content.location().unwrap(), - (content.clone(), Position { page, point: pos }), + elem.location().unwrap(), + (elem.clone(), Position { page, point: pos }), ); assert!(ret.is_none(), "duplicate locations"); // Build the label cache. - if let Some(label) = content.label() { + if let Some(label) = elem.label() { self.labels.entry(label).or_default().push(self.elems.len() - 1); } } diff --git a/crates/typst/src/introspection/locator.rs b/crates/typst/src/introspection/locator.rs index 7346b6fbe..9c02f6db1 100644 --- a/crates/typst/src/introspection/locator.rs +++ b/crates/typst/src/introspection/locator.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use comemo::{Track, Tracked, Validate}; -use crate::introspection::{Location, Meta}; +use crate::introspection::Location; use crate::layout::{Frame, FrameItem}; /// Provides locations for elements in the document. @@ -77,7 +77,7 @@ impl<'a> Locator<'a> { for (_, item) in frame.items() { match item { FrameItem::Group(group) => self.visit_frame(&group.frame), - FrameItem::Meta(Meta::Elem(elem), _) => { + FrameItem::Tag(elem) => { let hashes = self.hashes.get_mut(); let loc = elem.location().unwrap(); let entry = hashes.entry(loc.hash).or_default(); diff --git a/crates/typst/src/introspection/mod.rs b/crates/typst/src/introspection/mod.rs index 4ccff0c1e..9bf61d35a 100644 --- a/crates/typst/src/introspection/mod.rs +++ b/crates/typst/src/introspection/mod.rs @@ -23,15 +23,12 @@ pub use self::metadata::*; pub use self::query_::*; pub use self::state::*; -use std::fmt::{self, Debug, Formatter}; - -use ecow::{eco_format, EcoString}; -use smallvec::SmallVec; - +use crate::diag::{bail, SourceResult}; +use crate::engine::Engine; +use crate::foundations::NativeElement; use crate::foundations::{ - category, elem, ty, Category, Content, Packed, Repr, Scope, Unlabellable, + category, elem, Args, Category, Construct, Content, Packed, Scope, Unlabellable, }; -use crate::model::Destination; use crate::realize::{Behave, Behaviour}; /// Interactions between document parts. @@ -59,59 +56,39 @@ pub fn define(global: &mut Scope) { global.define_func::(); } -/// Hosts metadata and ensures metadata is produced even for empty elements. -#[elem(Behave, Unlabellable)] -pub struct MetaElem { - /// Metadata that should be attached to all elements affected by this style - /// property. - /// - /// This must be accessed and applied to 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 metadata can - /// only apply to them as a whole, not part of it (because they don't manage - /// styles). - #[fold] - pub data: SmallVec<[Meta; 1]>, +/// Holds a locatable element that was realized. +/// +/// The `TagElem` is handled by all layouters. The held element becomes +/// available for introspection in the next compiler iteration. +#[elem(Behave, Unlabellable, Construct)] +pub struct TagElem { + /// The introspectible element. + #[required] + #[internal] + pub elem: Content, } -impl Unlabellable for Packed {} +impl TagElem { + /// Create a packed tag element. + pub fn packed(elem: Content) -> Content { + let span = elem.span(); + let mut content = Self::new(elem).pack().spanned(span); + // We can skip preparation for the `TagElem`. + content.mark_prepared(); + content + } +} -impl Behave for Packed { +impl Construct for TagElem { + fn construct(_: &mut Engine, args: &mut Args) -> SourceResult { + bail!(args.span, "cannot be constructed manually") + } +} + +impl Unlabellable for Packed {} + +impl Behave for Packed { fn behaviour(&self) -> Behaviour { Behaviour::Invisible } } - -/// Meta information that isn't visible or renderable. -#[ty] -#[derive(Clone, PartialEq, Hash)] -pub enum Meta { - /// An internal or external link to a destination. - Link(Destination), - /// An identifiable element that produces something within the area this - /// metadata is attached to. - Elem(Content), - /// Indicates that content should be hidden. This variant doesn't appear - /// in the final frames as it is removed alongside the content that should - /// be hidden. - Hide, -} - -impl Debug for Meta { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self { - Self::Link(dest) => write!(f, "Link({dest:?})"), - Self::Elem(content) => write!(f, "Elem({:?})", content.func()), - Self::Hide => f.pad("Hide"), - } - } -} - -impl Repr for Meta { - fn repr(&self) -> EcoString { - eco_format!("{self:?}") - } -} diff --git a/crates/typst/src/layout/flow.rs b/crates/typst/src/layout/flow.rs index 6f3b8f162..c550ac28a 100644 --- a/crates/typst/src/layout/flow.rs +++ b/crates/typst/src/layout/flow.rs @@ -11,7 +11,7 @@ use crate::engine::Engine; use crate::foundations::{ elem, Content, NativeElement, Packed, Resolve, Smart, StyleChain, StyledElem, }; -use crate::introspection::{Meta, MetaElem}; +use crate::introspection::TagElem; use crate::layout::{ Abs, AlignElem, Axes, BlockElem, ColbreakElem, ColumnsElem, FixedAlignment, Fr, Fragment, Frame, FrameItem, LayoutMultiple, LayoutSingle, PlaceElem, Point, Regions, @@ -55,8 +55,8 @@ impl LayoutMultiple for Packed { styles = outer.chain(&styled.styles); } - if child.is::() { - layouter.layout_meta(styles); + if let Some(elem) = child.to_packed::() { + layouter.layout_tag(elem); } else if let Some(elem) = child.to_packed::() { layouter.layout_spacing(engine, elem, styles)?; } else if let Some(placed) = child.to_packed::() { @@ -107,6 +107,8 @@ struct FlowLayouter<'a> { last_was_par: bool, /// Spacing and layouted blocks for the current region. items: Vec, + /// A queue of tags that will be attached to the next frame. + pending_tags: Vec, /// A queue of floating elements. pending_floats: Vec, /// Whether we have any footnotes in the current region. @@ -166,7 +168,9 @@ impl FlowItem { Self::Placed { float: false, .. } => true, Self::Frame { frame, .. } => { frame.size().is_zero() - && frame.items().all(|(_, item)| matches!(item, FrameItem::Meta(..))) + && frame.items().all(|(_, item)| { + matches!(item, FrameItem::Link(_, _) | FrameItem::Tag(_)) + }) } _ => false, } @@ -190,6 +194,7 @@ impl<'a> FlowLayouter<'a> { initial: regions.size, last_was_par: false, items: vec![], + pending_tags: vec![], pending_floats: vec![], has_footnotes: false, footnote_config: FootnoteConfig { @@ -202,15 +207,8 @@ impl<'a> FlowLayouter<'a> { } /// Place explicit metadata into the flow. - fn layout_meta(&mut self, styles: StyleChain) { - let mut frame = Frame::soft(Size::zero()); - frame.meta(styles, true); - self.items.push(FlowItem::Frame { - frame, - align: Axes::splat(FixedAlignment::Start), - sticky: true, - movable: false, - }); + fn layout_tag(&mut self, tag: &Packed) { + self.pending_tags.push(tag.elem.clone()); } /// Layout vertical spacing. @@ -232,6 +230,27 @@ impl<'a> FlowLayouter<'a> { ) } + /// Layout a placed element. + fn layout_placed( + &mut self, + engine: &mut Engine, + placed: &Packed, + styles: StyleChain, + ) -> SourceResult<()> { + let float = placed.float(styles); + let clearance = placed.clearance(styles); + let alignment = placed.alignment(styles); + let delta = Axes::new(placed.dx(styles), placed.dy(styles)).resolve(styles); + let x_align = alignment.map_or(FixedAlignment::Center, |align| { + align.x().unwrap_or_default().resolve(styles) + }); + let y_align = alignment.map(|align| align.y().map(|y| y.resolve(styles))); + let mut frame = placed.layout(engine, styles, self.regions.base())?.into_frame(); + frame.post_process(styles); + let item = FlowItem::Placed { frame, x_align, y_align, delta, float, clearance }; + self.layout_item(engine, item) + } + /// Layout a paragraph. fn layout_par( &mut self, @@ -279,11 +298,12 @@ impl<'a> FlowLayouter<'a> { } } - for (i, frame) in lines.into_iter().enumerate() { + for (i, mut frame) in lines.into_iter().enumerate() { if i > 0 { self.layout_item(engine, FlowItem::Absolute(leading, true))?; } + self.drain_tag(&mut frame); self.layout_item( engine, FlowItem::Frame { frame, align, sticky: false, movable: true }, @@ -305,7 +325,8 @@ impl<'a> FlowLayouter<'a> { let sticky = BlockElem::sticky_in(styles); let pod = Regions::one(self.regions.base(), Axes::splat(false)); let mut frame = layoutable.layout(engine, styles, pod)?; - frame.meta(styles, false); + self.drain_tag(&mut frame); + frame.post_process(styles); self.layout_item( engine, FlowItem::Frame { frame, align, sticky, movable: true }, @@ -314,27 +335,6 @@ impl<'a> FlowLayouter<'a> { Ok(()) } - /// Layout a placed element. - fn layout_placed( - &mut self, - engine: &mut Engine, - placed: &Packed, - styles: StyleChain, - ) -> SourceResult<()> { - let float = placed.float(styles); - let clearance = placed.clearance(styles); - let alignment = placed.alignment(styles); - let delta = Axes::new(placed.dx(styles), placed.dy(styles)).resolve(styles); - let x_align = alignment.map_or(FixedAlignment::Center, |align| { - align.x().unwrap_or_default().resolve(styles) - }); - let y_align = alignment.map(|align| align.y().map(|y| y.resolve(styles))); - let mut frame = placed.layout(engine, styles, self.regions.base())?.into_frame(); - frame.meta(styles, false); - let item = FlowItem::Placed { frame, x_align, y_align, delta, float, clearance }; - self.layout_item(engine, item) - } - /// Layout into multiple regions. fn layout_multiple( &mut self, @@ -380,7 +380,8 @@ impl<'a> FlowLayouter<'a> { self.finish_region(engine, false)?; } - frame.meta(styles, false); + self.drain_tag(&mut frame); + frame.post_process(styles); self.layout_item( engine, FlowItem::Frame { frame, align, sticky, movable: false }, @@ -396,6 +397,17 @@ impl<'a> FlowLayouter<'a> { Ok(()) } + /// Attach currently pending metadata to the frame. + fn drain_tag(&mut self, frame: &mut Frame) { + if !self.pending_tags.is_empty() && !frame.is_empty() { + frame.prepend_multiple( + self.pending_tags + .drain(..) + .map(|elem| (Point::zero(), FrameItem::Tag(elem))), + ); + } + } + /// Layout a finished frame. fn layout_item( &mut self, @@ -628,6 +640,13 @@ impl<'a> FlowLayouter<'a> { } } + if force && !self.pending_tags.is_empty() { + let pos = Point::with_y(offset); + output.push_multiple( + self.pending_tags.drain(..).map(|elem| (pos, FrameItem::Tag(elem))), + ); + } + // Advance to the next region. self.finished.push(output); self.regions.next(); @@ -782,10 +801,10 @@ fn find_footnotes(notes: &mut Vec>, frame: &Frame) { for (_, item) in frame.items() { match item { FrameItem::Group(group) => find_footnotes(notes, &group.frame), - FrameItem::Meta(Meta::Elem(content), _) - if !notes.iter().any(|note| note.location() == content.location()) => + FrameItem::Tag(elem) + if !notes.iter().any(|note| note.location() == elem.location()) => { - let Some(footnote) = content.to_packed::() else { + let Some(footnote) = elem.to_packed::() else { continue; }; notes.push(footnote.clone()); diff --git a/crates/typst/src/layout/frame.rs b/crates/typst/src/layout/frame.rs index be1a7cb34..be207dc31 100644 --- a/crates/typst/src/layout/frame.rs +++ b/crates/typst/src/layout/frame.rs @@ -4,11 +4,14 @@ use std::fmt::{self, Debug, Formatter}; use std::num::NonZeroUsize; use std::sync::Arc; -use crate::foundations::{cast, dict, Dict, StyleChain, Value}; -use crate::introspection::{Meta, MetaElem}; +use smallvec::SmallVec; + +use crate::foundations::{cast, dict, Content, Dict, StyleChain, Value}; use crate::layout::{ - Abs, Axes, Corners, FixedAlignment, Length, Point, Rel, Sides, Size, Transform, + Abs, Axes, Corners, FixedAlignment, HideElem, Length, Point, Rel, Sides, Size, + Transform, }; +use crate::model::{Destination, LinkElem}; use crate::syntax::Span; use crate::text::TextItem; use crate::utils::{LazyHash, Numeric}; @@ -150,6 +153,17 @@ impl Frame { Arc::make_mut(&mut self.items).push((pos, item)); } + /// Add multiple items at a position in the foreground. + /// + /// The first item in the iterator will be the one that is most in the + /// background. + pub fn push_multiple(&mut self, items: I) + where + I: IntoIterator, + { + Arc::make_mut(&mut self.items).extend(items); + } + /// Add a frame at a position in the foreground. /// /// Automatically decides whether to inline the frame or to include it as a @@ -162,11 +176,6 @@ impl Frame { } } - /// Add zero-sized metadata at the origin. - pub fn push_positionless_meta(&mut self, meta: Meta) { - self.push(Point::zero(), FrameItem::Meta(meta, Size::zero())); - } - /// Insert an item at the given layer in the frame. /// /// This panics if the layer is greater than the number of layers present. @@ -284,31 +293,40 @@ impl Frame { } } - /// Attach the metadata from this style chain to the frame. + /// Apply late-stage properties from the style chain to this frame. This + /// includes: + /// - `HideElem::hidden` + /// - `LinkElem::dests` /// - /// If `force` is true, then the metadata is attached even when - /// the frame is empty. - // TODO: when would you want to pass true to `force` as opposed to false? - pub fn meta(&mut self, styles: StyleChain, force: bool) { - if force || !self.is_empty() { - self.meta_iter(MetaElem::data_in(styles)); + /// 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_process(&mut self, styles: StyleChain) { + if !self.is_empty() { + self.post_process_raw( + LinkElem::dests_in(styles), + HideElem::hidden_in(styles), + ); } } - /// Attach metadata from an iterator. - pub fn meta_iter(&mut self, iter: impl IntoIterator) { - let mut hide = false; - let size = self.size; - self.prepend_multiple(iter.into_iter().filter_map(|meta| { - if matches!(meta, Meta::Hide) { - hide = true; - None - } else { - Some((Point::zero(), FrameItem::Meta(meta, size))) + /// 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(); } - })); - if hide { - self.hide(); } } @@ -319,7 +337,7 @@ impl Frame { group.frame.hide(); !group.frame.is_empty() } - FrameItem::Meta(Meta::Elem(_), _) => true, + FrameItem::Tag(_) => true, _ => false, }); } @@ -488,8 +506,10 @@ pub enum FrameItem { Shape(Shape, Span), /// An image and its size. Image(Image, Size, Span), - /// Meta information and the region it applies to. - Meta(Meta, Size), + /// An internal or external link to a destination. + Link(Destination, Size), + /// An introspectable element that produced something within this frame. + Tag(Content), } impl Debug for FrameItem { @@ -499,7 +519,8 @@ impl Debug for FrameItem { Self::Text(text) => write!(f, "{text:?}"), Self::Shape(shape, _) => write!(f, "{shape:?}"), Self::Image(image, _, _) => write!(f, "{image:?}"), - Self::Meta(meta, _) => write!(f, "{meta:?}"), + Self::Link(dest, _) => write!(f, "Link({dest:?})"), + Self::Tag(elem) => write!(f, "Tag({elem:?})"), } } } diff --git a/crates/typst/src/layout/hide.rs b/crates/typst/src/layout/hide.rs index 7a8618c1e..1b8b9bd57 100644 --- a/crates/typst/src/layout/hide.rs +++ b/crates/typst/src/layout/hide.rs @@ -1,9 +1,6 @@ -use smallvec::smallvec; - use crate::diag::SourceResult; use crate::engine::Engine; use crate::foundations::{elem, Content, Packed, Show, StyleChain}; -use crate::introspection::{Meta, MetaElem}; /// Hides content without affecting layout. /// @@ -22,11 +19,16 @@ pub struct HideElem { /// The content to hide. #[required] pub body: Content, + + /// This style is set on the content contained in the `hide` element. + #[internal] + #[ghost] + pub hidden: bool, } impl Show for Packed { #[typst_macros::time(name = "hide", span = self.span())] fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { - Ok(self.body().clone().styled(MetaElem::set_data(smallvec![Meta::Hide]))) + Ok(self.body().clone().styled(HideElem::set_hidden(true))) } } diff --git a/crates/typst/src/layout/inline/mod.rs b/crates/typst/src/layout/inline/mod.rs index 5a74d3d24..6a39537c0 100644 --- a/crates/typst/src/layout/inline/mod.rs +++ b/crates/typst/src/layout/inline/mod.rs @@ -14,10 +14,10 @@ use crate::diag::{bail, SourceResult}; use crate::engine::{Engine, Route}; use crate::eval::Tracer; use crate::foundations::{Content, Packed, Resolve, Smart, StyleChain, StyledElem}; -use crate::introspection::{Introspector, Locator, MetaElem}; +use crate::introspection::{Introspector, Locator, TagElem}; use crate::layout::{ - Abs, AlignElem, Axes, BoxElem, Dir, Em, FixedAlignment, Fr, Fragment, Frame, HElem, - Point, Regions, Size, Sizing, Spacing, + Abs, AlignElem, Axes, BoxElem, Dir, Em, FixedAlignment, Fr, Fragment, Frame, + FrameItem, HElem, Point, Regions, Size, Sizing, Spacing, }; use crate::math::{EquationElem, MathParItem}; use crate::model::{Linebreaks, ParElem}; @@ -201,8 +201,8 @@ enum Segment<'a> { Equation(Vec), /// A box with arbitrary content. Box(&'a Packed, bool), - /// Metadata. - Meta, + /// A tag. + Tag(&'a Packed), } impl Segment<'_> { @@ -220,7 +220,7 @@ impl Segment<'_> { .chain([LTR_ISOLATE, POP_ISOLATE]) .map(char::len_utf8) .sum(), - Self::Meta => 0, + Self::Tag(_) => 0, } } } @@ -236,8 +236,8 @@ enum Item<'a> { Fractional(Fr, Option<(&'a Packed, StyleChain<'a>)>), /// Layouted inline-level content. Frame(Frame), - /// Metadata. - Meta(Frame), + /// A tag. + Tag(&'a Packed), /// An item that is invisible and needs to be skipped, e.g. a Unicode /// isolate. Skip(char), @@ -266,7 +266,7 @@ impl<'a> Item<'a> { Self::Text(shaped) => shaped.text.len(), Self::Absolute(_) | Self::Fractional(_, _) => SPACING_REPLACE.len_utf8(), Self::Frame(_) => OBJ_REPLACE.len_utf8(), - Self::Meta(_) => 0, + Self::Tag(_) => 0, Self::Skip(c) => c.len_utf8(), } } @@ -277,7 +277,7 @@ impl<'a> Item<'a> { Self::Text(shaped) => shaped.width, Self::Absolute(v) => *v, Self::Frame(frame) => frame.width(), - Self::Fractional(_, _) | Self::Meta(_) => Abs::zero(), + Self::Fractional(_, _) | Self::Tag(_) => Abs::zero(), Self::Skip(_) => Abs::zero(), } } @@ -531,6 +531,9 @@ fn collect<'a>( } else if child.is::() || child.is::() || child.is::() + // This is a temporary hack. We should rather skip these + // and peek at the next child. + || child.is::() { Some(SPACING_REPLACE) } else { @@ -548,7 +551,7 @@ fn collect<'a>( let mut items = elem.layout_inline(engine, styles, pod)?; for item in &mut items { let MathParItem::Frame(frame) = item else { continue }; - frame.meta(styles, false); + frame.post_process(styles); } full.push(LTR_ISOLATE); full.extend(items.iter().map(MathParItem::text)); @@ -558,8 +561,8 @@ fn collect<'a>( let frac = elem.width(styles).is_fractional(); full.push(if frac { SPACING_REPLACE } else { OBJ_REPLACE }); Segment::Box(elem, frac) - } else if child.is::() { - Segment::Meta + } else if let Some(elem) = child.to_packed::() { + Segment::Tag(elem) } else { bail!(child.span(), "unexpected paragraph child"); }; @@ -643,15 +646,13 @@ fn prepare<'a>( } else { let pod = Regions::one(region, Axes::splat(false)); let mut frame = elem.layout(engine, styles, pod)?; - frame.meta(styles, false); + frame.post_process(styles); frame.translate(Point::with_y(TextElem::baseline_in(styles))); items.push(Item::Frame(frame)); } } - Segment::Meta => { - let mut frame = Frame::soft(Size::zero()); - frame.meta(styles, true); - items.push(Item::Meta(frame)); + Segment::Tag(tag) => { + items.push(Item::Tag(tag)); } } @@ -687,7 +688,7 @@ fn prepare<'a>( /// See Requirements for Chinese Text Layout, Section 3.2.2 Mixed Text Composition in Horizontal /// Written Mode fn add_cjk_latin_spacing(items: &mut [Item]) { - let mut items = items.iter_mut().filter(|x| !matches!(x, Item::Meta(_))).peekable(); + let mut items = items.iter_mut().filter(|x| !matches!(x, Item::Tag(_))).peekable(); let mut prev: Option<&ShapedGlyph> = None; while let Some(item) = items.next() { let Some(text) = item.text_mut() else { @@ -1410,7 +1411,7 @@ fn commit( let region = Size::new(amount, full); let pod = Regions::one(region, Axes::new(true, false)); let mut frame = elem.layout(engine, *styles, pod)?; - frame.meta(*styles, false); + frame.post_process(*styles); frame.translate(Point::with_y(TextElem::baseline_in(*styles))); push(&mut offset, frame); } else { @@ -1420,12 +1421,17 @@ fn commit( Item::Text(shaped) => { let mut frame = shaped.build(engine, justification_ratio, extra_justification); - frame.meta(shaped.styles, false); + frame.post_process(shaped.styles); push(&mut offset, frame); } - Item::Frame(frame) | Item::Meta(frame) => { + Item::Frame(frame) => { push(&mut offset, frame.clone()); } + Item::Tag(tag) => { + let mut frame = Frame::soft(Size::zero()); + frame.push(Point::zero(), FrameItem::Tag(tag.elem.clone())); + frames.push((offset, frame)); + } Item::Skip(_) => {} } } diff --git a/crates/typst/src/math/fragment.rs b/crates/typst/src/math/fragment.rs index 084a12419..ef865b380 100644 --- a/crates/typst/src/math/fragment.rs +++ b/crates/typst/src/math/fragment.rs @@ -6,11 +6,11 @@ use ttf_parser::{GlyphId, Rect}; use unicode_math_class::MathClass; use crate::foundations::StyleChain; -use crate::introspection::{Meta, MetaElem}; -use crate::layout::{Abs, Corner, Em, Frame, FrameItem, Point, Size}; +use crate::layout::{Abs, Corner, Em, Frame, FrameItem, HideElem, Point, Size}; use crate::math::{ scaled_font_size, EquationElem, Limits, MathContext, MathSize, Scaled, }; +use crate::model::{Destination, LinkElem}; use crate::syntax::Span; use crate::text::{Font, Glyph, Lang, Region, TextElem, TextItem}; use crate::visualize::Paint; @@ -218,7 +218,8 @@ pub struct GlyphFragment { pub class: MathClass, pub math_size: MathSize, pub span: Span, - pub meta: SmallVec<[Meta; 1]>, + pub dests: SmallVec<[Destination; 1]>, + pub hidden: bool, pub limits: Limits, } @@ -273,7 +274,8 @@ impl GlyphFragment { accent_attach: Abs::zero(), class, span, - meta: MetaElem::data_in(styles), + dests: LinkElem::dests_in(styles), + hidden: HideElem::hidden_in(styles), }; fragment.set_id(ctx, id); fragment @@ -357,7 +359,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.meta_iter(self.meta); + frame.post_process_raw(self.dests, self.hidden); frame } @@ -436,7 +438,7 @@ impl FrameFragment { pub fn new(ctx: &MathContext, styles: StyleChain, mut frame: Frame) -> Self { let base_ascent = frame.ascent(); let accent_attach = frame.width() / 2.0; - frame.meta(styles, false); + frame.post_process(styles); Self { frame, font_size: scaled_font_size(ctx, styles), diff --git a/crates/typst/src/math/mod.rs b/crates/typst/src/math/mod.rs index c98cac7b4..305f91885 100644 --- a/crates/typst/src/math/mod.rs +++ b/crates/typst/src/math/mod.rs @@ -41,12 +41,12 @@ use self::row::*; use self::spacing::*; use crate::diag::SourceResult; -use crate::foundations::SequenceElem; -use crate::foundations::StyledElem; use crate::foundations::{ - category, Category, Content, Module, Resolve, Scope, StyleChain, + category, Category, Content, Module, Resolve, Scope, SequenceElem, StyleChain, + StyledElem, }; -use crate::layout::{BoxElem, HElem, Spacing}; +use crate::introspection::TagElem; +use crate::layout::{BoxElem, Frame, FrameItem, HElem, Point, Size, Spacing}; use crate::realize::{process, BehavedBuilder}; use crate::text::{LinebreakElem, SpaceElem, TextElem}; @@ -296,6 +296,13 @@ impl LayoutMath for Content { return Ok(()); } + if let Some(tag) = self.to_packed::() { + let mut frame = Frame::soft(Size::zero()); + frame.push(Point::zero(), FrameItem::Tag(tag.elem.clone())); + ctx.push(FrameFragment::new(ctx, styles, frame)); + return Ok(()); + } + if let Some(elem) = self.with::() { return elem.layout_math(ctx, styles); } diff --git a/crates/typst/src/math/stretch.rs b/crates/typst/src/math/stretch.rs index f05e38714..cc71ced6d 100644 --- a/crates/typst/src/math/stretch.rs +++ b/crates/typst/src/math/stretch.rs @@ -167,7 +167,7 @@ fn assemble( let mut frame = Frame::soft(size); let mut offset = Abs::zero(); frame.set_baseline(baseline); - frame.meta_iter(base.meta); + frame.post_process_raw(base.dests, base.hidden); for (fragment, advance) in selected { let pos = if horizontal { diff --git a/crates/typst/src/model/link.rs b/crates/typst/src/model/link.rs index fb93f060d..107b0d9ae 100644 --- a/crates/typst/src/model/link.rs +++ b/crates/typst/src/model/link.rs @@ -1,4 +1,5 @@ use ecow::{eco_format, EcoString}; +use smallvec::SmallVec; use crate::diag::{At, SourceResult}; use crate::engine::Engine; @@ -79,6 +80,11 @@ pub struct LinkElem { _ => args.expect("body")?, })] pub body: Content, + + /// This style is set on the content contained in the `link` element. + #[internal] + #[ghost] + pub dests: SmallVec<[Destination; 1]>, } impl LinkElem { diff --git a/crates/typst/src/realize/mod.rs b/crates/typst/src/realize/mod.rs index e06059ff9..98b5c1271 100644 --- a/crates/typst/src/realize/mod.rs +++ b/crates/typst/src/realize/mod.rs @@ -23,7 +23,7 @@ use crate::engine::{Engine, Route}; use crate::foundations::{ Content, NativeElement, Packed, SequenceElem, StyleChain, StyledElem, Styles, }; -use crate::introspection::MetaElem; +use crate::introspection::TagElem; use crate::layout::{ AlignElem, BlockElem, BoxElem, ColbreakElem, FlowElem, HElem, LayoutMultiple, LayoutSingle, PageElem, PagebreakElem, Parity, PlaceElem, VElem, @@ -389,7 +389,7 @@ impl<'a> FlowBuilder<'a> { if content.is::() || content.is::() - || content.is::() + || content.is::() || content.is::() { self.0.push(content, styles); @@ -451,7 +451,7 @@ impl<'a> ParBuilder<'a> { /// content could not be merged, and paragraph building should be /// interrupted so that the content can be added elsewhere. fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool { - if content.is::() { + if content.is::() { if !self.0.is_empty() { self.0.push(content, styles); return true; @@ -612,7 +612,7 @@ impl<'a> CiteGroupBuilder<'a> { /// interrupted so that the content can be added elsewhere. fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool { if !self.items.is_empty() - && (content.is::() || content.is::()) + && (content.is::() || content.is::()) { self.staged.push((content, styles)); return true; diff --git a/crates/typst/src/realize/process.rs b/crates/typst/src/realize/process.rs index 7aa8b9279..6ddb493d6 100644 --- a/crates/typst/src/realize/process.rs +++ b/crates/typst/src/realize/process.rs @@ -1,7 +1,6 @@ use std::cell::OnceCell; use comemo::{Track, Tracked}; -use smallvec::smallvec; use crate::diag::SourceResult; use crate::engine::Engine; @@ -9,7 +8,7 @@ use crate::foundations::{ Content, Context, Packed, Recipe, RecipeIndex, Regex, Selector, Show, ShowSet, Style, StyleChain, Styles, Synthesize, Transformation, }; -use crate::introspection::{Locatable, Meta, MetaElem}; +use crate::introspection::{Locatable, TagElem}; use crate::text::TextElem; use crate::utils::{hash128, BitSet}; @@ -57,9 +56,9 @@ pub fn process( // If the element isn't yet prepared (we're seeing it for the first time), // prepare it. - let mut meta = None; + let mut tag = None; if !prepared { - meta = prepare(engine, &mut target, &mut map, styles)?; + tag = prepare(engine, &mut target, &mut map, styles)?; } // Apply a step, if there is one. @@ -76,9 +75,9 @@ pub fn process( None => target, }; - // If necessary, apply metadata generated in the preparation. - if let Some(meta) = meta { - output += meta.pack(); + // If necessary, add the tag generated in the preparation. + if let Some(tag) = tag { + output = tag + output; } Ok(Some(output.styled_with_map(map))) @@ -194,7 +193,7 @@ fn prepare( target: &mut Content, map: &mut Styles, styles: StyleChain, -) -> SourceResult>> { +) -> SourceResult> { // Generate a location for the element, which uniquely identifies it in // the document. This has some overhead, so we only do it for elements // that are explicitly marked as locatable and labelled elements. @@ -225,24 +224,18 @@ fn prepare( // available in rules. target.materialize(styles.chain(map)); - if located { - // Apply metadata to be able to find the element in the frames. Do this - // after synthesis and materialization, so that it includes the - // synthesized fields. Do it before marking as prepared so that show-set - // rules will apply to this element when queried. This adds a style to - // the whole element's subtree identifying it as belonging to the - // element. - map.set(MetaElem::set_data(smallvec![Meta::Elem(target.clone())])); - } + // If the element is locatable, create a tag element to be able to find the + // element in the frames after layout. Do this after synthesis and + // materialization, so that it includes the synthesized fields. Do it before + // marking as prepared so that show-set rules will apply to this element + // when queried. + let tag = located.then(|| TagElem::packed(target.clone())); // Ensure that this preparation only runs once by marking the element as // prepared. target.mark_prepared(); - // If the element is located, return an extra meta elem that will be - // attached so that the metadata styles are not lost in case the element's - // show rule results in nothing. - Ok(located.then(|| Packed::new(MetaElem::new()).spanned(target.span()))) + Ok(tag) } /// Apply a step. diff --git a/tests/ref/eval-mode.png b/tests/ref/eval-mode.png index 94357ff4ffdad35d51be6503649dcf899f435eb3..5edfa62dcbc81667a16aacfddc886fc9e7699a54 100644 GIT binary patch delta 827 zcmV-B1H}CC2GRzQB!3x6L_t(|+U?cbQ;cyO$8mpwduA}M7*{Tgj4777D94D#c}j$k znH**^gcVD)q*^wl6^l(t$#mWx8=bT$TAh$$msV}IQf)nNH_wbK7iON>CiA?1cb}Wr z-S;<%acYtRE3g9pSFk8OpvoX-R`F*BM3A~C01wQ3mkWM5wSNF%iU3d>MoghJznEM~ zTMs7RkJ4}=3pjnhYnoi}Pylpp1i+X^unVch{REVv>p*Jr7hwD6>QK4h+D(m(x(hrp zM6Tf zL;%DJ@p)=(Uw=XoKH`p~F~)^E=c+M@j|G=YR0DjEBXA&jxhL4&n`?dJ!D1Va+E{Uxq`E7Ne!#6yJ=EY; zn4sd5BNc*Me=}voyKsC`!$m=8)l_F_Meo96aTP~|?)Ub=X1ojIxO7GUCwLb=1?B{S z$25Dr443$~fTkLcw_&OAgGZvq3C=Otl;25#6?h`SzX4>*{~_Y8Ae8_B002ovPDHLk FV1gkWix~g_ delta 858 zcmV-g1Eu`Z2Jr@vB!4(bL_t(|+U?cbPm^Z=$8rCHWw%@A^=_7I$(FG=0%c>1n}Zl! z2&|2c!{#EJ$p$fKvO@}{%ApGr3&jCe66G*;Dj-sYz7{C9P!x!Y)Cxx$lk3_(C+tKaj(pEkIG@#Ac=7)no`{0&ud1?F-0DTqgN47C-Xz zK_Gj)KV2!<{oTljrIc3NUVa%NiMwU{1pvccdwTt1NyJ=bjAU82hIFi%%<*(JD+N~@ zolZm7ElOm7+7QZ6+q1>4JeIBWJsj-BS7Re$>5Nyjli0D)O>gTK=F{s|&4a~jwcbm^6X`_udS z%FMLba(~%#lO#q<_H_d6ywkR`ZqB@Fsw#}=E4~Qn2ph{k<;oeQU`y%HP?jUGqpg6? z82gE>k&$eG1KkH(|0-QB?jlagI?AM;pYMhAG|ejo=Z?X)#k$?&EPumsTObuzDL_@q z{o!OA2}`7*Tjg|RG!f%bf+z)lC=vaLcEga{nSVW;S2^>3^xzag>#r=l7bESOqh-zj zg=I6J^u(KDwMxNuYY*qF-dJNN{*zo9#9225z+?!9VXDT(ya&4ax#;X3SUI7+TpIj> zu;);v<)Nx@csvBbS<(8SwOLhoFwyLZ7W^Sv59j+-h55G0v?K_kwefFJUAWjbP$UTM z{8RQ*1yOZjjGkOUT92`g%CJ~$6cYRnjY5HrJe6U4#vQ1?PDE{3-p0W*yCNF{ kcRPIA@1(&R{3^ju0j}8pJpX(Y7XSbN07*qoM6N<$f+=#YLI3~& diff --git a/tests/ref/issue-785-cite-locate.png b/tests/ref/issue-785-cite-locate.png index 7c2a650a46e8b14e4618363665467a8f680d7d29..bcdcc9684785cac58bbcf74c4778cdf05c7bfb33 100644 GIT binary patch delta 13974 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 delta 14238 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 I5=Mdl2XU+FQ~&?~ diff --git a/tests/ref/link-on-block.png b/tests/ref/link-on-block.png index 9076983def2eaca2da5ebaa9414afb1673586fba..ed73b8668bf03580bc1e768603dece965b3f6240 100644 GIT binary patch delta 2411 zcmV-x36%Et67~|1B!4|gL_t(|+U=V8Qxs;J_Q+6(4cD~-Y5cz*=X%<8Q1+~_bEF&Whq9t zB&kZJ-+HR+dEf5$>F&?_eV=!DoA>^Mm!GA)hal1B33Wez3C2Ud#bASQ>?hnJ=+8PIW^yY-n*nBzH;_uIicr zhUFjH+`-QFGqVd}UEjL1 z4u*r?95kJwK*R<<7Zvi0IBb=WuLE@hU$v$jk(t>XR46KDFyu6vOdvEAs-0S`+hMJ0 z_ql5v#(&|?mipR8Ow?grudV+L2mP|Sefxc zD{Nqh%);mCC1MkZS|~Iq6{gO5zo%5&=C2G*O*A%jVE4MTv^*EOge!UzGdo)lq=d^> zbJ&IKd?n-umC}wNCYj7E5}6RG*<|wT^d3~}R)4ANTD=z$o1`*xMhd0b?`;lv+iJX1 zqr(@@FV@sH+09lxing_N25JT3M z%SyrY#a=)!7MtjF5uKP(@AG!Hv~+iLj06WA=2E#-?x_smi{a*`(Rsx@l#wgQrz3n` z5r2oJf@EF5l(SeWb)gf8L&_EkbUe12L6z`0kgrWNnp7l2a|juhVx8Az2i%Q*pWAAK z9F70#cr9qH(J$hmIYbsQgPBK@0A((_kVX;ZWN`p1l`3XX#R4u$C-c*iD8zI|LB5Ji z7E{UGLaD@{Rv6U^M`;NmHH}NBj*XwiR)56@Yv1CaH(3l>3`u4>Jt8tSJ~lHqn@6RH z7&L@J;xVX#oNRtt3LSFy^L4V2K^4)c2;n4`MB>rMZ+LG0 z($*+?cJ7PD=I%>NH@;fFkJ-rBTvyKsIPYi>nLwlk2d7>=)fb$MPawS50uL}Ze}DNc zBL|1h{rJy2gQbB+=0`=PQJ5&Up>Z@eCn_oxS`Hmb-@YR{>OgW@LeBoZ@!S3wx&6Z< zM~-G6Je*#nl=JiRK+YYlZNZ-InhK{;COF+SFfced)ZO{6phqV!MnokdijwKGf5mM3 zj!18H2RQF;DH(KsUH7J^c7AgB#eb^T->ElNyk*$#{?LzuhMl8avT*O71lTPC&|pdF z+?~5(A|jKMQ}Po|&|{C0vNGsN@!1g(i92@2#3$x4IYoL@%b`&;QiPqChY0vBjZNG< zy4z|V?`){|*S;%gcy6AsXxHv&aKPM^Ezz-rw$8DU@rAy@v%_N-CZ@mW9e89nA*iMjgp?d3vxG3Pr*oapR6=B&D*T z+SEEZJGaQBP&sr8!eNn##D7A$VzjrT%V*Q@IHAzu7Jyb4+2a#Qn2HUiQ`9_3b!}I3 z`|$7I-*u|04d}!k&jj??O9qERX@tD;Q%#>AN``7KU0iDQ<zjk&_V^Pybxr-?P#6*r!CNZ2*xHy$U;*g(1Ud#< zXR3{hBjx0X4CaRLCMq*ePE4gHJqO*;JidJ6KCsqmO(a6Py??mKVbs&IG9&iv4Gv6f z0cgkr8m$wa{x7=@0tj&AYBQEDhsMss+k)=knlLPoFenc5s{*HwC1invb%$BMdv7dO z-TCn$!2K#{7*x#v__OG+^Ac#7OyTx}(P_XP8+91qw*+l1>&T_6(g-~9|8g{Htci}v zjz3N&W(lwsxPK;}Ate)8Nhhd~(;;=h<`K4$BoWI`#Kfc?k2|nu-+{=e$?0=j02a`3zC!R-~(vb@&^OZUtYWG zt8Q?(JT`m9%-Ka~*@EBEb93j%$EP1Z{^#oI#y8)5_vq0-pgJ*eW_|r3G}`TDD=Vwu zylB_1-G3Yy7{M-h`0&x-;OP1D7qL^&eROnkW+nvwojdm-&eYT_c4}keTP!l%=<3yF zaF&*?Ub*rWIF~Pf38ydzj`Q;i%geXGSzKI#j&MzX|ImX6Uxyz6o^xa4wRZ2`*DEU< zuiC9!t2f~k7_VD}f{{{B+>9Z$Kp02L0-Me>x zW##_k$A5>GXV3oq6Mmcxg+i5;mH1cJpZqI$d3pJhCr@yK_`e(ApmET6!9nAo@q&ZK dLH`=de*tHXO~J^uQLq32002ovPDHLkV1nljs?Y!c delta 2412 zcmV-y36u8r68932B!50hL_t(|+U=V8Q&dMD!1H%()z)rps;u2Lag9lg2N*;V1d$u$ z9OmMf`+75P4(2g8%sntTGr$bja3hBd_bI|Cf(Bg^@kS9y%tmW>%ed|@yWjFGI0yL2b&;)2A5ukse zpl{#Z_{GV<@P)QhGp`f3Z_ruY4^rXogB%yF$esE_6Cc^ zYcf@1N-L&xC`;_RQsdnCa9y1r_iF<5n?bw0f#_W(|Z{1l3 zAwX{qnnsr*LM@k#@wuferku}JgLea8g|ZTn7?~`LFVNBHD3vPV@wFuiyGrG>o2xr& zoOL$+aDP{8W4#}D>X2Wrt^bVx{jyY5_ZJr{xGap#QVRK64hv%z$(c+go2>?{T%Hz{ zT0jtqiObQ5ga&YGK3^+C4PA|1SGlU)>kdv$_?tWNcU@Xqo(o&<%!(ieiNq)n7!a}1VDPFnE==W=%YUsZjRz4L#1dm#a(;`~)8g~A*LkK! zhcC=8*3~y#jb;sowYT>Wpf{0HU9DVdDyEAWMKU%^!77%cDEO=f4nsKfWKs(thNz87 z%0VdXSQi(f42E1$Vh7?7viUqUhpC`bL>v~xYXg-k7Vxm#jC7Mu?QvKEx8GaiG+Q7> z6TdoM3tFZ33OHD9CL=Swuz)H8%4}u{l`P23W&u_TMM$Rzd2EbE;-)5&Gt=nBMRF2J zNG5Sg#3HRis#i#DOjbX-!R35;r0um0XD77aB)maw8&Ape8ahZTpU>hyzKf@wxl=#%=p!`1TJE zA3l;3c_^(^hH{Gvz@0nW+5^2kbyaq~gm}t2L(is7J3l$}Vt>)&?b7J0-qLJufAGga!_84XiNAMGJlqxmXpqD- z_Rd|=Vc|(h$wl$UX)#ChveRjaaXDdO2|IR1$0ZaLFfk3LVo}LTF~Te;KzQ6%e>1y) z<}@3}yP6uk_3sKA#?9fE?%ExN&%x<#jf%-=?;0B!UpPHDJ3Mw_V)~1|fq%0j6N{mF zPR?BJ?3oM|yHB4PpS~0-!o$!P*E6yizJ}gV)8OcNFbHNr|76b*$b0%{rp{ggWo+sq zbOfclZyGlO@5Ot8ad^f51sceb$YOxV78r5R5l3>sb0ZFCIb6MX4Vf&W=dojtq$j2@ zpy+R(JUh2oNT#r8WQ4^aWq)RtpwiL4&h8qElEVrH7qYaow%;saZ z%0{S%*&?U`oX%+V;(s&5QDSNbPTb!*2x|a3xbO!%KMxF_+r9576v0Wi{dw=J9xYXv zxI#6qfAr~LK#wbso9(_rrWBOp$@z^fflz(i@!W>yeo)AC5jeqHdbG~$&&pr`=(u@fPj>^cY_KtUBoJX{Wq&BJWL@8FpbEO24)I9#vxojMwy4GP{JcKz2HQ6c3e(6E_8^#`L;fjhqHu)uE#+Fa3@Pm`x+aD@Mh(U`t2 zDmo|b7%4NGhkw_=GevYUDU*?SoB}Z&LI+$P;TK6FA$mMII^|gGfj#>UghxzHpWgz| zFhkh0FnX>)1u+B<+0;S=cek_?g}4uf1i%4Plg$T<0VpF5CA(`nbmeZKk2@e3g#7_F z!Og=DCWOd>!OR?HRxS%-F6@RsZ2J@fJp|w1{PqLbG=B_E3G{_}SSq8X7L?z;|B=6K z2&}=0+SWM=IRu;#=r?|02}z{~kpyQYOPpV zSq0@qxqo);=D@%R{=mbBj|K-v=jSitP0)RGbaG}U2>P8n_rcE8)LFb~W8+)gGQ8;O z)n!nYmabm8@)anTFMkP5xB~gPa|_GMw?J83T!N19On?8-g9l%S1_0yS*m$knyZ80V z%Eqg5>(=T`_)G8hgS&Uvp?LlJ%EI}J3+EPQr)7hnudO}&{`-G{Jbn7?$&;t6t84e} z-CtR`|M>CWq2}4MfB%FZXM@3@+wCU4y8h%}!7D2(pFDX&5G4NHfB;Q^CK3Uf08JzU eGy(e8nEne&7EQj#uswDF0000l5qQZxQm}5 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 delta 2936 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 { + FrameItem::Link(_, size) => { let w = size.x.to_pt() as f32; let h = size.y.to_pt() as f32; let rect = sk::Rect::from_xywh(0.0, 0.0, w, h).unwrap(); @@ -416,7 +415,7 @@ fn skippable(page: &Page) -> bool { fn skippable_frame(frame: &Frame) -> bool { frame.items().all(|(_, item)| match item { FrameItem::Group(group) => skippable_frame(&group.frame), - FrameItem::Meta(..) => true, + FrameItem::Tag(_) => true, _ => false, }) } diff --git a/tests/suite/introspection/locate.typ b/tests/suite/introspection/locate.typ index 981f8c465..b3a77fdef 100644 --- a/tests/suite/introspection/locate.typ +++ b/tests/suite/introspection/locate.typ @@ -4,6 +4,13 @@ = Introduction #context test(locate().position().y, 20pt) +--- locate-position-trailing-tag --- +// Test locating the position of a tag with no following content. +#context test(here().position().y, 10pt) +#box[] +#v(10pt) +#context test(here().position().y, 20pt) + --- locate-missing-label --- // Error: 10-25 label `` does not exist in the document #context locate()