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 94357ff4f..5edfa62dc 100644 Binary files a/tests/ref/eval-mode.png and b/tests/ref/eval-mode.png differ diff --git a/tests/ref/issue-785-cite-locate.png b/tests/ref/issue-785-cite-locate.png index 7c2a650a4..bcdcc9684 100644 Binary files a/tests/ref/issue-785-cite-locate.png and b/tests/ref/issue-785-cite-locate.png differ diff --git a/tests/ref/link-on-block.png b/tests/ref/link-on-block.png index 9076983de..ed73b8668 100644 Binary files a/tests/ref/link-on-block.png and b/tests/ref/link-on-block.png differ diff --git a/tests/ref/locate-position-trailing-tag.png b/tests/ref/locate-position-trailing-tag.png new file mode 100644 index 000000000..b64188a7a Binary files /dev/null and b/tests/ref/locate-position-trailing-tag.png differ diff --git a/tests/ref/quote-cite-format-note.png b/tests/ref/quote-cite-format-note.png index 1092ffdbf..cb2c4c0a0 100644 Binary files a/tests/ref/quote-cite-format-note.png and b/tests/ref/quote-cite-format-note.png differ diff --git a/tests/src/run.rs b/tests/src/run.rs index 47760e164..a59ce5369 100644 --- a/tests/src/run.rs +++ b/tests/src/run.rs @@ -7,7 +7,6 @@ use tiny_skia as sk; use typst::diag::SourceDiagnostic; use typst::eval::Tracer; use typst::foundations::Smart; -use typst::introspection::Meta; use typst::layout::{Abs, Frame, FrameItem, Page, Transform}; use typst::model::Document; use typst::visualize::Color; @@ -392,7 +391,7 @@ fn render_links(canvas: &mut sk::Pixmap, ts: sk::Transform, frame: &Frame) { let ts = ts.pre_concat(to_sk_transform(&group.transform)); render_links(canvas, ts, &group.frame); } - FrameItem::Meta(Meta::Link(_), size) => { + 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()