From e25389a85e2c4bb7bab5f5d68ab148395704960d Mon Sep 17 00:00:00 2001 From: Laurenz Date: Wed, 25 Sep 2024 10:26:41 +0200 Subject: [PATCH] New flow layout, with multi-column floats (#5017) --- crates/typst/src/introspection/counter.rs | 11 +- crates/typst/src/layout/columns.rs | 52 +- crates/typst/src/layout/container.rs | 390 ++--- crates/typst/src/layout/flow/collect.rs | 625 +++++--- crates/typst/src/layout/flow/compose.rs | 843 +++++++++++ crates/typst/src/layout/flow/distribute.rs | 512 +++++++ crates/typst/src/layout/flow/mod.rs | 1289 +++-------------- crates/typst/src/layout/frame.rs | 6 + crates/typst/src/layout/inline/line.rs | 11 +- crates/typst/src/layout/pages/mod.rs | 2 + crates/typst/src/layout/pages/run.rs | 13 +- crates/typst/src/layout/place.rs | 27 +- crates/typst/src/layout/regions.rs | 13 - crates/typst/src/math/fragment.rs | 5 +- crates/typst/src/model/figure.rs | 13 +- crates/typst/src/model/footnote.rs | 3 + crates/typst/src/model/par.rs | 6 +- crates/typst/src/visualize/image/mod.rs | 6 +- crates/typst/src/visualize/shape.rs | 14 +- docs/guides/page-setup.md | 61 +- tests/ref/block-fr-height-auto-width.png | Bin 0 -> 1069 bytes tests/ref/block-fr-height-first-child.png | Bin 0 -> 149 bytes tests/ref/block-fr-height-multiple.png | Bin 0 -> 152 bytes tests/ref/block-fr-height.png | Bin 0 -> 167 bytes tests/ref/block-sticky-alone.png | Bin 0 -> 166 bytes tests/ref/block-sticky-colbreak.png | Bin 0 -> 283 bytes tests/ref/block-sticky-many.png | Bin 0 -> 570 bytes tests/ref/block-sticky.png | Bin 0 -> 469 bytes .../{box-width-fr.png => box-fr-width.png} | Bin tests/ref/colbreak-weak.png | Bin 0 -> 197 bytes tests/ref/figure-placement.png | Bin 0 -> 2302 bytes tests/ref/footnote-block-at-end.png | Bin 0 -> 620 bytes .../ref/footnote-break-across-pages-block.png | Bin 0 -> 1296 bytes .../ref/footnote-break-across-pages-float.png | Bin 0 -> 1399 bytes .../footnote-break-across-pages-nested.png | Bin 0 -> 1320 bytes tests/ref/footnote-break-across-pages.png | Bin 5424 -> 5425 bytes tests/ref/footnote-float-priority.png | Bin 0 -> 1393 bytes tests/ref/footnote-in-list.png | Bin 0 -> 2513 bytes tests/ref/footnote-in-place.png | Bin 0 -> 1127 bytes tests/ref/footnote-in-table.png | Bin 12311 -> 12423 bytes tests/ref/footnote-multiple-in-one-line.png | Bin 0 -> 693 bytes tests/ref/footnote-nested-same-frame.png | Bin 743 -> 0 bytes tests/ref/footnote-nested.png | Bin 2469 -> 2581 bytes tests/ref/issue-2213-align-fr.png | Bin 0 -> 291 bytes tests/ref/issue-3481-cite-location.png | Bin 602 -> 504 bytes tests/ref/issue-3641-float-loop.png | Bin 704 -> 678 bytes tests/ref/issue-3866-block-migration.png | Bin 0 -> 777 bytes tests/ref/issue-footnotes-skip-first-page.png | Bin 571 -> 512 bytes .../issue-multiple-footnote-in-one-line.png | Bin 704 -> 0 bytes tests/ref/place-float-block-backlog.png | Bin 0 -> 826 bytes tests/ref/place-float-clearance-empty.png | Bin 0 -> 1235 bytes tests/ref/place-float-column-align-auto.png | Bin 0 -> 932 bytes ...eued.png => place-float-column-queued.png} | Bin tests/ref/place-float-counter.png | Bin 0 -> 674 bytes tests/ref/place-float-delta.png | Bin 0 -> 317 bytes tests/ref/place-float-flow-size-alone.png | Bin 0 -> 125 bytes tests/ref/place-float-flow-size.png | Bin 0 -> 347 bytes tests/ref/place-float-fr.png | Bin 0 -> 507 bytes .../ref/place-float-in-column-align-auto.png | Bin 843 -> 0 bytes tests/ref/place-float-rel-sizing.png | Bin 0 -> 335 bytes .../place-float-threecolumn-block-backlog.png | Bin 0 -> 707 bytes tests/ref/place-float-threecolumn.png | Bin 0 -> 1286 bytes .../ref/place-float-twocolumn-align-auto.png | Bin 0 -> 719 bytes tests/ref/place-float-twocolumn-fits-not.png | Bin 0 -> 1043 bytes tests/ref/place-float-twocolumn-fits.png | Bin 0 -> 1001 bytes tests/ref/place-float-twocolumn-queued.png | Bin 0 -> 862 bytes tests/ref/place-float-twocolumn.png | Bin 0 -> 1259 bytes tests/ref/query-running-header.png | Bin 9017 -> 9231 bytes tests/skip.txt | 0 tests/src/collect.rs | 13 + tests/src/tests.rs | 3 + tests/suite/introspection/query.typ | 4 +- tests/suite/layout/align.typ | 8 + tests/suite/layout/columns.typ | 7 + tests/suite/layout/container.typ | 61 +- tests/suite/layout/flow/flow.typ | 13 +- .../suite/{model => layout/flow}/footnote.typ | 116 +- tests/suite/layout/flow/place-float.typ | 83 -- tests/suite/layout/flow/place-flush.typ | 29 - tests/suite/layout/flow/place.typ | 290 ++++ tests/suite/model/cite.typ | 3 +- tests/suite/model/figure.typ | 36 + 82 files changed, 2884 insertions(+), 1684 deletions(-) create mode 100644 crates/typst/src/layout/flow/compose.rs create mode 100644 crates/typst/src/layout/flow/distribute.rs create mode 100644 tests/ref/block-fr-height-auto-width.png create mode 100644 tests/ref/block-fr-height-first-child.png create mode 100644 tests/ref/block-fr-height-multiple.png create mode 100644 tests/ref/block-fr-height.png create mode 100644 tests/ref/block-sticky-alone.png create mode 100644 tests/ref/block-sticky-colbreak.png create mode 100644 tests/ref/block-sticky-many.png create mode 100644 tests/ref/block-sticky.png rename tests/ref/{box-width-fr.png => box-fr-width.png} (100%) create mode 100644 tests/ref/colbreak-weak.png create mode 100644 tests/ref/figure-placement.png create mode 100644 tests/ref/footnote-block-at-end.png create mode 100644 tests/ref/footnote-break-across-pages-block.png create mode 100644 tests/ref/footnote-break-across-pages-float.png create mode 100644 tests/ref/footnote-break-across-pages-nested.png create mode 100644 tests/ref/footnote-float-priority.png create mode 100644 tests/ref/footnote-in-list.png create mode 100644 tests/ref/footnote-in-place.png create mode 100644 tests/ref/footnote-multiple-in-one-line.png delete mode 100644 tests/ref/footnote-nested-same-frame.png create mode 100644 tests/ref/issue-2213-align-fr.png create mode 100644 tests/ref/issue-3866-block-migration.png delete mode 100644 tests/ref/issue-multiple-footnote-in-one-line.png create mode 100644 tests/ref/place-float-block-backlog.png create mode 100644 tests/ref/place-float-clearance-empty.png create mode 100644 tests/ref/place-float-column-align-auto.png rename tests/ref/{place-float-in-column-queued.png => place-float-column-queued.png} (100%) create mode 100644 tests/ref/place-float-counter.png create mode 100644 tests/ref/place-float-delta.png create mode 100644 tests/ref/place-float-flow-size-alone.png create mode 100644 tests/ref/place-float-flow-size.png create mode 100644 tests/ref/place-float-fr.png delete mode 100644 tests/ref/place-float-in-column-align-auto.png create mode 100644 tests/ref/place-float-rel-sizing.png create mode 100644 tests/ref/place-float-threecolumn-block-backlog.png create mode 100644 tests/ref/place-float-threecolumn.png create mode 100644 tests/ref/place-float-twocolumn-align-auto.png create mode 100644 tests/ref/place-float-twocolumn-fits-not.png create mode 100644 tests/ref/place-float-twocolumn-fits.png create mode 100644 tests/ref/place-float-twocolumn-queued.png create mode 100644 tests/ref/place-float-twocolumn.png create mode 100644 tests/skip.txt rename tests/suite/{model => layout/flow}/footnote.typ (74%) delete mode 100644 tests/suite/layout/flow/place-float.typ delete mode 100644 tests/suite/layout/flow/place-flush.typ diff --git a/crates/typst/src/introspection/counter.rs b/crates/typst/src/introspection/counter.rs index e7dd2ba08..ba126e180 100644 --- a/crates/typst/src/introspection/counter.rs +++ b/crates/typst/src/introspection/counter.rs @@ -304,7 +304,7 @@ impl Counter { route: Route::extend(route).unnested(), }; - let mut state = CounterState::init(&self.0); + let mut state = CounterState::init(matches!(self.0, CounterKey::Page)); let mut page = NonZeroUsize::ONE; let mut stops = eco_vec![(state.clone(), page)]; @@ -656,12 +656,9 @@ pub struct CounterState(pub SmallVec<[usize; 3]>); impl CounterState { /// Get the initial counter state for the key. - pub fn init(key: &CounterKey) -> Self { - Self(match key { - // special case, because pages always start at one. - CounterKey::Page => smallvec![1], - _ => smallvec![0], - }) + pub fn init(page: bool) -> Self { + // Special case, because pages always start at one. + Self(smallvec![usize::from(page)]) } /// Advance the counter and return the numbers for the given heading. diff --git a/crates/typst/src/layout/columns.rs b/crates/typst/src/layout/columns.rs index b46351f8e..5812e38aa 100644 --- a/crates/typst/src/layout/columns.rs +++ b/crates/typst/src/layout/columns.rs @@ -10,34 +10,39 @@ use crate::layout::{ /// Separates a region into multiple equally sized columns. /// -/// The `column` function allows to separate the interior of any container into -/// multiple columns. It will not equalize the height of the columns, instead, -/// the columns will take up the height of their container or the remaining -/// height on the page. The columns function can break across pages if -/// necessary. +/// The `column` function lets you separate the interior of any container into +/// multiple columns. It will currently not balance the height of the columns. +/// Instead, the columns will take up the height of their container or the +/// remaining height on the page. Support for balanced columns is planned for +/// the future. /// -/// If you need to insert columns across your whole document, you can use the -/// [`{page}` function's `columns` parameter]($page.columns) instead. +/// # Page-level columns { #page-level } +/// If you need to insert columns across your whole document, use the `{page}` +/// function's [`columns` parameter]($page.columns) instead. This will create +/// the columns directly at the page-level rather than wrapping all of your +/// content in a layout container. As a result, things like +/// [pagebreaks]($pagebreak), [footnotes]($footnote), and [line +/// numbers]($par.line) will continue to work as expected. For more information, +/// also read the [relevant part of the page setup +/// guide]($guides/page-setup/#columns). /// -/// # Example -/// ```example -/// = Towards Advanced Deep Learning +/// # Breaking out of columns { #breaking-out } +/// To temporarily break out of columns (e.g. for a paper's title), use +/// page-scoped floating placement: /// -/// #box(height: 68pt, -/// columns(2, gutter: 11pt)[ -/// #set par(justify: true) -/// This research was funded by the -/// National Academy of Sciences. -/// NAoS provided support for field -/// tests and interviews with a -/// grant of up to USD 40.000 for a -/// period of 6 months. -/// ] +/// ```example:single +/// #set page(columns: 2, height: 150pt) +/// +/// #place( +/// top + center, +/// scope: "page", +/// float: true, +/// text(1.4em, weight: "bold")[ +/// My document +/// ], /// ) /// -/// In recent years, deep learning has -/// increasingly been used to solve a -/// variety of problems. +/// #lorem(40) /// ``` #[elem(Show)] pub struct ColumnsElem { @@ -59,7 +64,6 @@ pub struct ColumnsElem { impl Show for Packed { fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { Ok(BlockElem::multi_layouter(self.clone(), layout_columns) - .with_rootable(true) .pack() .spanned(self.span())) } diff --git a/crates/typst/src/layout/container.rs b/crates/typst/src/layout/container.rs index b5a8f7f83..2ff0f0fd6 100644 --- a/crates/typst/src/layout/container.rs +++ b/crates/typst/src/layout/container.rs @@ -143,7 +143,7 @@ impl Packed { let inset = self.inset(styles).unwrap_or_default(); // Build the pod region. - let pod = Self::pod(&width, &height, &inset, styles, region); + let pod = unbreakable_pod(&width, &height.into(), &inset, styles, region); // Layout the body. let mut frame = match self.body(styles) { @@ -166,14 +166,6 @@ impl Packed { crate::layout::grow(&mut frame, &inset); } - // Apply baseline shift. Do this after setting the size and applying the - // inset, so that a relative shift is resolved relative to the final - // height. - let shift = self.baseline(styles).relative_to(frame.height()); - if !shift.is_zero() { - frame.set_baseline(frame.baseline() - shift); - } - // Prepare fill and stroke. let fill = self.fill(styles); let stroke = self @@ -196,49 +188,20 @@ impl Packed { frame.fill_and_stroke(fill, &stroke, &outset, &radius, self.span()); } + // Assign label to the frame. if let Some(label) = self.label() { frame.group(|group| group.label = Some(label)) } - Ok(frame) - } - - /// Builds the pod region for box layout. - fn pod( - width: &Sizing, - height: &Smart, - inset: &Sides>, - styles: StyleChain, - region: Size, - ) -> Region { - // Resolve the size. - let mut size = Size::new( - match width { - // For auto, the whole region is available. - Sizing::Auto => region.x, - // Resolve the relative sizing. - Sizing::Rel(rel) => rel.resolve(styles).relative_to(region.x), - // Fr is handled outside and already factored into the `region`, - // so we can treat it equivalently to 100%. - Sizing::Fr(_) => region.x, - }, - match height { - // See above. Note that fr is not supported on this axis. - Smart::Auto => region.y, - Smart::Custom(rel) => rel.resolve(styles).relative_to(region.y), - }, - ); - - // Take the inset, if any, into account. - if !inset.is_zero() { - size = crate::layout::shrink(size, inset); + // Apply baseline shift. Do this after setting the size and applying the + // inset, so that a relative shift is resolved relative to the final + // height. + let shift = self.baseline(styles).relative_to(frame.height()); + if !shift.is_zero() { + frame.set_baseline(frame.baseline() - shift); } - // If the child is not auto-sized, the size is forced and we should - // enable expansion. - let expand = Axes::new(*width != Sizing::Auto, *height != Smart::Auto); - - Region::new(size, expand) + Ok(frame) } } @@ -355,7 +318,7 @@ pub struct BlockElem { /// fill: aqua, /// ) /// ``` - pub height: Smart>, + pub height: Sizing, /// Whether the block can be broken and continue on the next page. /// @@ -453,20 +416,16 @@ pub struct BlockElem { #[default(false)] pub clip: bool, - /// Whether this block must stick to the following one. + /// Whether this block must stick to the following one, with no break in + /// between. /// - /// Use this to prevent page breaks between e.g. a heading and its body. - #[internal] + /// This is, by default, set on heading blocks to prevent orphaned headings + /// at the bottom of the page. + /// + /// Marking a block as sticky makes it unbreakable. #[default(false)] - #[parse(None)] pub sticky: bool, - /// Whether this block can host footnotes. - #[internal] - #[default(false)] - #[parse(None)] - pub rootable: bool, - /// The contents of the block. #[positional] #[borrowed] @@ -513,9 +472,97 @@ impl BlockElem { } impl Packed { - /// Layout this block as part of a flow. + /// Lay this out as an unbreakable block. #[typst_macros::time(name = "block", span = self.span())] - pub fn layout( + pub fn layout_single( + &self, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + base: Size, + ) -> SourceResult { + // Fetch sizing properties. + let width = self.width(styles); + let height = self.height(styles); + let inset = self.inset(styles).unwrap_or_default(); + + // Build the pod regions. + let pod = unbreakable_pod(&width.into(), &height, &inset, styles, base); + + // Layout the body. + let body = self.body(styles); + let mut frame = match body { + // If we have no body, just create one frame. Its size will be + // adjusted below. + None => Frame::hard(Size::zero()), + + // If we have content as our body, just layout it. + Some(BlockBody::Content(body)) => { + layout_frame(engine, body, locator.relayout(), styles, pod)? + } + + // If we have a child that wants to layout with just access to the + // base region, give it that. + Some(BlockBody::SingleLayouter(callback)) => { + callback.call(engine, locator, styles, pod)? + } + + // If we have a child that wants to layout with full region access, + // we layout it. + Some(BlockBody::MultiLayouter(callback)) => { + callback.call(engine, locator, styles, pod.into())?.into_frame() + } + }; + + // Explicit blocks are boundaries for gradient relativeness. + if matches!(body, None | Some(BlockBody::Content(_))) { + frame.set_kind(FrameKind::Hard); + } + + // Enforce a correct frame size on the expanded axes. Do this before + // applying the inset, since the pod shrunk. + frame.set_size(pod.expand.select(pod.size, frame.size())); + + // Apply the inset. + if !inset.is_zero() { + crate::layout::grow(&mut frame, &inset); + } + + // Prepare fill and stroke. + let fill = self.fill(styles); + let stroke = self + .stroke(styles) + .unwrap_or_default() + .map(|s| s.map(Stroke::unwrap_or_default)); + + // Only fetch these if necessary (for clipping or filling/stroking). + let outset = Lazy::new(|| self.outset(styles).unwrap_or_default()); + let radius = Lazy::new(|| self.radius(styles).unwrap_or_default()); + + // Clip the contents, if requested. + if self.clip(styles) { + let size = frame.size() + outset.relative_to(frame.size()).sum_by_axis(); + frame.clip(clip_rect(size, &radius, &stroke)); + } + + // Add fill and/or stroke. + if fill.is_some() || stroke.iter().any(Option::is_some) { + frame.fill_and_stroke(fill, &stroke, &outset, &radius, self.span()); + } + + // Assign label to each frame in the fragment. + if let Some(label) = self.label() { + frame.group(|group| group.label = Some(label)); + } + + Ok(frame) + } +} + +impl Packed { + /// Lay this out as a breakable block. + #[typst_macros::time(name = "block", span = self.span())] + pub fn layout_multiple( &self, engine: &mut Engine, locator: Locator, @@ -526,14 +573,13 @@ impl Packed { let width = self.width(styles); let height = self.height(styles); let inset = self.inset(styles).unwrap_or_default(); - let breakable = self.breakable(styles); // Allocate a small vector for backlogs. let mut buf = SmallVec::<[Abs; 2]>::new(); // Build the pod regions. let pod = - Self::pod(&width, &height, &inset, breakable, styles, regions, &mut buf); + breakable_pod(&width.into(), &height, &inset, styles, regions, &mut buf); // Layout the body. let body = self.body(styles); @@ -673,116 +719,6 @@ impl Packed { Ok(fragment) } - - /// Builds the pod regions for block layout. - /// - /// If `breakable` is `false`, this will only ever return a single region. - fn pod<'a>( - width: &Smart, - height: &Smart, - inset: &Sides>, - breakable: bool, - styles: StyleChain, - regions: Regions, - buf: &'a mut SmallVec<[Abs; 2]>, - ) -> Regions<'a> { - let base = regions.base(); - - // The vertical region sizes we're about to build. - let first; - let full; - let backlog: &mut [Abs]; - let last; - - // If the block has a fixed height, things are very different, so we - // handle that case completely separately. - match height { - Smart::Auto => { - if breakable { - // If the block automatically sized and breakable, we can - // just inherit the regions. - first = regions.size.y; - buf.extend_from_slice(regions.backlog); - backlog = buf; - last = regions.last; - } else { - // If the block is automatically sized, but not breakable, - // we provide the full base height. It doesn't really make - // sense to provide just the remaining height to an - // unbreakable block. - first = regions.full; - backlog = &mut []; - last = None; - } - - // Since we're automatically sized, we inherit the base size. - full = regions.full; - } - - Smart::Custom(rel) => { - // Resolve the sizing to a concrete size. - let resolved = rel.resolve(styles).relative_to(base.y); - - if breakable { - // If the block is fixed-height and breakable, distribute - // the fixed height across a start region and a backlog. - (first, backlog) = distribute(resolved, regions, buf); - } else { - // If the block is fixed-height, but not breakable, the - // fixed height is all in the first region, and we have no - // backlog. - first = resolved; - backlog = &mut []; - } - - // Since we're manually sized, the resolved size is also the - // base height. - full = resolved; - - // If the height is manually sized, we don't want a final - // repeatable region. - last = None; - } - }; - - // Resolve the horizontal sizing to a concrete width and combine - // `width` and `first` into `size`. - let mut size = Size::new( - match width { - Smart::Auto => regions.size.x, - Smart::Custom(rel) => rel.resolve(styles).relative_to(base.x), - }, - first, - ); - - // Take the inset, if any, into account, applying it to the - // individual region components. - let (mut full, mut last) = (full, last); - if !inset.is_zero() { - crate::layout::shrink_multiple( - &mut size, &mut full, backlog, &mut last, inset, - ); - } - - // If the child is manually sized along an axis (i.e. not `auto`), then - // it should expand along that axis. We also ensure that we only expand - // if the size is finite because it just doesn't make sense to expand - // into infinite regions. - let expand = Axes::new(*width != Smart::Auto, *height != Smart::Auto) - & size.map(Abs::is_finite); - - Regions { - size, - full, - backlog, - last, - expand, - // This will only ever be set by the flow if the block is - // `rootable`. It is important that we propagate this, so that - // columns can hold footnotes. - root: regions.root, - } - } } /// The contents of a block. @@ -873,6 +809,118 @@ cast! { v: Fr => Self::Fr(v), } +/// Builds the pod region for an unbreakable sized container. +fn unbreakable_pod( + width: &Sizing, + height: &Sizing, + inset: &Sides>, + styles: StyleChain, + base: Size, +) -> Region { + // Resolve the size. + let mut size = Size::new( + match width { + // - For auto, the whole region is available. + // - Fr is handled outside and already factored into the `region`, + // so we can treat it equivalently to 100%. + Sizing::Auto | Sizing::Fr(_) => base.x, + // Resolve the relative sizing. + Sizing::Rel(rel) => rel.resolve(styles).relative_to(base.x), + }, + match height { + Sizing::Auto | Sizing::Fr(_) => base.y, + Sizing::Rel(rel) => rel.resolve(styles).relative_to(base.y), + }, + ); + + // Take the inset, if any, into account. + if !inset.is_zero() { + size = crate::layout::shrink(size, inset); + } + + // If the child is manually, the size is forced and we should enable + // expansion. + let expand = Axes::new( + *width != Sizing::Auto && size.x.is_finite(), + *height != Sizing::Auto && size.y.is_finite(), + ); + + Region::new(size, expand) +} + +/// Builds the pod regions for a breakable sized container. +fn breakable_pod<'a>( + width: &Sizing, + height: &Sizing, + inset: &Sides>, + styles: StyleChain, + regions: Regions, + buf: &'a mut SmallVec<[Abs; 2]>, +) -> Regions<'a> { + let base = regions.base(); + + // The vertical region sizes we're about to build. + let first; + let full; + let backlog: &mut [Abs]; + let last; + + // If the block has a fixed height, things are very different, so we + // handle that case completely separately. + match height { + Sizing::Auto | Sizing::Fr(_) => { + // If the block is automatically sized, we can just inherit the + // regions. + first = regions.size.y; + full = regions.full; + buf.extend_from_slice(regions.backlog); + backlog = buf; + last = regions.last; + } + + Sizing::Rel(rel) => { + // Resolve the sizing to a concrete size. + let resolved = rel.resolve(styles).relative_to(base.y); + + // Since we're manually sized, the resolved size is the base height. + full = resolved; + + // Distribute the fixed height across a start region and a backlog. + (first, backlog) = distribute(resolved, regions, buf); + + // If the height is manually sized, we don't want a final repeatable + // region. + last = None; + } + }; + + // Resolve the horizontal sizing to a concrete width and combine + // `width` and `first` into `size`. + let mut size = Size::new( + match width { + Sizing::Auto | Sizing::Fr(_) => regions.size.x, + Sizing::Rel(rel) => rel.resolve(styles).relative_to(base.x), + }, + first, + ); + + // Take the inset, if any, into account, applying it to the + // individual region components. + let (mut full, mut last) = (full, last); + if !inset.is_zero() { + crate::layout::shrink_multiple(&mut size, &mut full, backlog, &mut last, inset); + } + + // If the child is manually, the size is forced and we should enable + // expansion. + let expand = Axes::new( + *width != Sizing::Auto && size.x.is_finite(), + *height != Sizing::Auto && size.y.is_finite(), + ); + + Regions { size, full, backlog, last, expand } +} + /// Distribute a fixed height spread over existing regions into a new first /// height and a new backlog. fn distribute<'a>( diff --git a/crates/typst/src/layout/flow/collect.rs b/crates/typst/src/layout/flow/collect.rs index 13b49d860..73659ee2d 100644 --- a/crates/typst/src/layout/flow/collect.rs +++ b/crates/typst/src/layout/flow/collect.rs @@ -1,3 +1,7 @@ +use std::cell::RefCell; +use std::fmt::{self, Debug, Formatter}; +use std::hash::Hash; + use bumpalo::boxed::Box as BumpBox; use bumpalo::Bump; use once_cell::unsync::Lazy; @@ -5,39 +9,18 @@ use once_cell::unsync::Lazy; use crate::diag::{bail, SourceResult}; use crate::engine::Engine; use crate::foundations::{Packed, Resolve, Smart, StyleChain}; -use crate::introspection::{Locator, Tag, TagElem}; +use crate::introspection::{Locator, SplitLocator, Tag, TagElem}; use crate::layout::{ layout_frame, Abs, AlignElem, Alignment, Axes, BlockElem, ColbreakElem, - FixedAlignment, FlushElem, Fr, Fragment, Frame, PagebreakElem, PlaceElem, Ratio, - Region, Regions, Rel, Size, Spacing, VElem, + FixedAlignment, FlushElem, Fr, Fragment, Frame, PagebreakElem, PlaceElem, + PlacementScope, Ratio, Region, Regions, Rel, Size, Sizing, Spacing, VElem, }; use crate::model::ParElem; use crate::realize::Pair; use crate::text::TextElem; -/// A prepared child in flow layout. -/// -/// The larger variants are bump-boxed to keep the enum size down. -pub enum Child<'a> { - /// An introspection tag. - Tag(&'a Tag), - /// Relative spacing with a specific weakness. - Rel(Rel, u8), - /// Fractional spacing. - Fr(Fr), - /// An already layouted line of a paragraph. - Line(BumpBox<'a, LineChild>), - /// A potentially breakable block. - Block(BumpBox<'a, BlockChild<'a>>), - /// An absolutely or floatingly placed element. - Placed(BumpBox<'a, PlacedChild<'a>>), - /// A column break. - Break(bool), - /// A place flush. - Flush, -} - -/// Collects all content of the flow into prepared children. +/// Collects all elements of the flow into prepared children. These are much +/// simpler to handle than the raw elements. #[typst_macros::time] pub fn collect<'a>( engine: &mut Engine, @@ -47,218 +30,510 @@ pub fn collect<'a>( base: Size, expand: bool, ) -> SourceResult>> { - let mut locator = locator.split(); - let mut output = Vec::with_capacity(children.len()); - let mut last_was_par = false; + Collector { + engine, + bump, + children, + locator: locator.split(), + base, + expand, + output: Vec::with_capacity(children.len()), + last_was_par: false, + } + .run() +} - for &(child, styles) in children { - if let Some(elem) = child.to_packed::() { - output.push(Child::Tag(&elem.tag)); - } else if let Some(elem) = child.to_packed::() { - output.push(match elem.amount { - Spacing::Rel(rel) => { - Child::Rel(rel.resolve(styles), elem.weak(styles) as u8) - } - Spacing::Fr(fr) => Child::Fr(fr), - }); - } else if let Some(elem) = child.to_packed::() { - output.push(Child::Break(elem.weak(styles))); - } else if let Some(elem) = child.to_packed::() { - let align = AlignElem::alignment_in(styles).resolve(styles); - let leading = ParElem::leading_in(styles); - let spacing = ParElem::spacing_in(styles); - let costs = TextElem::costs_in(styles); +/// State for collection. +struct Collector<'a, 'x, 'y> { + engine: &'x mut Engine<'y>, + bump: &'a Bump, + children: &'x [Pair<'a>], + base: Size, + expand: bool, + locator: SplitLocator<'a>, + output: Vec>, + last_was_par: bool, +} - let lines = crate::layout::layout_inline( - engine, - &elem.children, - locator.next(&elem.span()), - styles, - last_was_par, - base, - expand, - )? - .into_frames(); +impl<'a> Collector<'a, '_, '_> { + /// Perform the collection. + fn run(mut self) -> SourceResult>> { + for (idx, &(child, styles)) in self.children.iter().enumerate() { + if let Some(elem) = child.to_packed::() { + self.output.push(Child::Tag(&elem.tag)); + } else if let Some(elem) = child.to_packed::() { + self.v(elem, styles); + } else if let Some(elem) = child.to_packed::() { + self.par(elem, styles)?; + } else if let Some(elem) = child.to_packed::() { + self.block(elem, styles); + } else if let Some(elem) = child.to_packed::() { + self.place(idx, elem, styles)?; + } else if child.is::() { + self.output.push(Child::Flush); + } else if let Some(elem) = child.to_packed::() { + self.output.push(Child::Break(elem.weak(styles))); + } else if child.is::() { + bail!( + child.span(), "pagebreaks are not allowed inside of containers"; + hint: "try using a `#colbreak()` instead", + ); + } else { + bail!(child.span(), "{} is not allowed here", child.func().name()); + } + } - output.push(Child::Rel(spacing.into(), 4)); + Ok(self.output) + } - // Determine whether to prevent widow and orphans. - let len = lines.len(); - let prevent_orphans = - costs.orphan() > Ratio::zero() && len >= 2 && !lines[1].is_empty(); - let prevent_widows = - costs.widow() > Ratio::zero() && len >= 2 && !lines[len - 2].is_empty(); - let prevent_all = len == 3 && prevent_orphans && prevent_widows; + /// Collect vertical spacing into a relative or fractional child. + fn v(&mut self, elem: &'a Packed, styles: StyleChain<'a>) { + self.output.push(match elem.amount { + Spacing::Rel(rel) => Child::Rel(rel.resolve(styles), elem.weak(styles) as u8), + Spacing::Fr(fr) => Child::Fr(fr), + }); + } - // Store the heights of lines at the edges because we'll potentially - // need these later when `lines` is already moved. - let height_at = |i| lines.get(i).map(Frame::height).unwrap_or_default(); - let front_1 = height_at(0); - let front_2 = height_at(1); - let back_2 = height_at(len.saturating_sub(2)); - let back_1 = height_at(len.saturating_sub(1)); + /// Collect a paragraph into [`LineChild`]ren. This already performs line + /// layout since it is not dependent on the concrete regions. + fn par( + &mut self, + elem: &'a Packed, + styles: StyleChain<'a>, + ) -> SourceResult<()> { + let align = AlignElem::alignment_in(styles).resolve(styles); + let leading = ParElem::leading_in(styles); + let spacing = ParElem::spacing_in(styles); + let costs = TextElem::costs_in(styles); - for (i, frame) in lines.into_iter().enumerate() { - if i > 0 { - output.push(Child::Rel(leading.into(), 5)); - } + let lines = crate::layout::layout_inline( + self.engine, + &elem.children, + self.locator.next(&elem.span()), + styles, + self.last_was_par, + self.base, + self.expand, + )? + .into_frames(); - // To prevent widows and orphans, we require enough space for - // - all lines if it's just three - // - the first two lines if we're at the first line - // - the last two lines if we're at the second to last line - let need = if prevent_all && i == 0 { - front_1 + leading + front_2 + leading + back_1 - } else if prevent_orphans && i == 0 { - front_1 + leading + front_2 - } else if prevent_widows && i >= 2 && i + 2 == len { - back_2 + leading + back_1 - } else { - frame.height() - }; + self.output.push(Child::Rel(spacing.into(), 4)); - let child = LineChild { frame, align, need }; - output.push(Child::Line(BumpBox::new_in(child, bump))); + // Determine whether to prevent widow and orphans. + let len = lines.len(); + let prevent_orphans = + costs.orphan() > Ratio::zero() && len >= 2 && !lines[1].is_empty(); + let prevent_widows = + costs.widow() > Ratio::zero() && len >= 2 && !lines[len - 2].is_empty(); + let prevent_all = len == 3 && prevent_orphans && prevent_widows; + + // Store the heights of lines at the edges because we'll potentially + // need these later when `lines` is already moved. + let height_at = |i| lines.get(i).map(Frame::height).unwrap_or_default(); + let front_1 = height_at(0); + let front_2 = height_at(1); + let back_2 = height_at(len.saturating_sub(2)); + let back_1 = height_at(len.saturating_sub(1)); + + for (i, frame) in lines.into_iter().enumerate() { + if i > 0 { + self.output.push(Child::Rel(leading.into(), 5)); } - output.push(Child::Rel(spacing.into(), 4)); - last_was_par = true; - } else if let Some(elem) = child.to_packed::() { - let locator = locator.next(&elem.span()); - let align = AlignElem::alignment_in(styles).resolve(styles); - let sticky = elem.sticky(styles); - let rootable = elem.rootable(styles); - - let fallback = Lazy::new(|| ParElem::spacing_in(styles)); - let spacing = |amount| match amount { - Smart::Auto => Child::Rel((*fallback).into(), 4), - Smart::Custom(Spacing::Rel(rel)) => Child::Rel(rel.resolve(styles), 3), - Smart::Custom(Spacing::Fr(fr)) => Child::Fr(fr), + // To prevent widows and orphans, we require enough space for + // - all lines if it's just three + // - the first two lines if we're at the first line + // - the last two lines if we're at the second to last line + let need = if prevent_all && i == 0 { + front_1 + leading + front_2 + leading + back_1 + } else if prevent_orphans && i == 0 { + front_1 + leading + front_2 + } else if prevent_widows && i >= 2 && i + 2 == len { + back_2 + leading + back_1 + } else { + frame.height() }; - output.push(spacing(elem.above(styles))); + self.output + .push(Child::Line(self.boxed(LineChild { frame, align, need }))); + } - let child = BlockChild { align, sticky, rootable, elem, styles, locator }; - output.push(Child::Block(BumpBox::new_in(child, bump))); + self.output.push(Child::Rel(spacing.into(), 4)); + self.last_was_par = true; - output.push(spacing(elem.below(styles))); - last_was_par = false; - } else if let Some(elem) = child.to_packed::() { - let locator = locator.next(&elem.span()); - let float = elem.float(styles); - let clearance = elem.clearance(styles); - let delta = Axes::new(elem.dx(styles), elem.dy(styles)).resolve(styles); + Ok(()) + } - let alignment = elem.alignment(styles); - let align_x = alignment.map_or(FixedAlignment::Center, |align| { - align.x().unwrap_or_default().resolve(styles) - }); - let align_y = alignment.map(|align| align.y().map(|y| y.resolve(styles))); + /// Collect a block into a [`SingleChild`] or [`MultiChild`] depending on + /// whether it is breakable. + fn block(&mut self, elem: &'a Packed, styles: StyleChain<'a>) { + let locator = self.locator.next(&elem.span()); + let align = AlignElem::alignment_in(styles).resolve(styles); + let sticky = elem.sticky(styles); + let breakable = elem.breakable(styles); + let fr = match elem.height(styles) { + Sizing::Fr(fr) => Some(fr), + _ => None, + }; - match (float, align_y) { - (true, Smart::Custom(None | Some(FixedAlignment::Center))) => bail!( - elem.span(), - "floating placement must be `auto`, `top`, or `bottom`" - ), - (false, Smart::Auto) => bail!( - elem.span(), - "automatic positioning is only available for floating placement"; - hint: "you can enable floating placement with `place(float: true, ..)`" - ), - _ => {} - } + let fallback = Lazy::new(|| ParElem::spacing_in(styles)); + let spacing = |amount| match amount { + Smart::Auto => Child::Rel((*fallback).into(), 4), + Smart::Custom(Spacing::Rel(rel)) => Child::Rel(rel.resolve(styles), 3), + Smart::Custom(Spacing::Fr(fr)) => Child::Fr(fr), + }; - let child = PlacedChild { - float, - clearance, - delta, - align_x, - align_y, + self.output.push(spacing(elem.above(styles))); + + if !breakable || sticky || fr.is_some() { + self.output.push(Child::Single(self.boxed(SingleChild { + align, + sticky, + fr, elem, styles, locator, - alignment, - }; - output.push(Child::Placed(BumpBox::new_in(child, bump))); - } else if child.is::() { - output.push(Child::Flush); - } else if child.is::() { - bail!( - child.span(), "pagebreaks are not allowed inside of containers"; - hint: "try using a `#colbreak()` instead", - ); + cell: CachedCell::new(), + }))); } else { - bail!(child.span(), "{} is not allowed here", child.func().name()); - } + let alone = self.children.len() == 1; + self.output.push(Child::Multi(self.boxed(MultiChild { + align, + alone, + elem, + styles, + locator, + cell: CachedCell::new(), + }))); + }; + + self.output.push(spacing(elem.below(styles))); + self.last_was_par = false; } - Ok(output) + /// Collects a placed element into a [`PlacedChild`]. + fn place( + &mut self, + idx: usize, + elem: &'a Packed, + styles: StyleChain<'a>, + ) -> SourceResult<()> { + let alignment = elem.alignment(styles); + let align_x = alignment.map_or(FixedAlignment::Center, |align| { + align.x().unwrap_or_default().resolve(styles) + }); + let align_y = alignment.map(|align| align.y().map(|y| y.resolve(styles))); + let scope = elem.scope(styles); + let float = elem.float(styles); + + match (float, align_y) { + (true, Smart::Custom(None | Some(FixedAlignment::Center))) => bail!( + elem.span(), + "vertical floating placement must be `auto`, `top`, or `bottom`" + ), + (false, Smart::Auto) => bail!( + elem.span(), + "automatic positioning is only available for floating placement"; + hint: "you can enable floating placement with `place(float: true, ..)`" + ), + _ => {} + } + + if !float && scope == PlacementScope::Page { + bail!( + elem.span(), + "page-scoped positioning is currently only available for floating placement"; + hint: "you can enable floating placement with `place(float: true, ..)`" + ); + } + + let locator = self.locator.next(&elem.span()); + let clearance = elem.clearance(styles); + let delta = Axes::new(elem.dx(styles), elem.dy(styles)).resolve(styles); + self.output.push(Child::Placed(self.boxed(PlacedChild { + idx, + align_x, + align_y, + scope, + float, + clearance, + delta, + elem, + styles, + locator, + alignment, + cell: CachedCell::new(), + }))); + + Ok(()) + } + + /// Wraps a value in a bump-allocated box to reduce its footprint in the + /// [`Child`] enum. + fn boxed(&self, value: T) -> BumpBox<'a, T> { + BumpBox::new_in(value, self.bump) + } } -/// A child that encapsulates a paragraph line. +/// A prepared child in flow layout. +/// +/// The larger variants are bump-boxed to keep the enum size down. +#[derive(Debug)] +pub enum Child<'a> { + /// An introspection tag. + Tag(&'a Tag), + /// Relative spacing with a specific weakness level. + Rel(Rel, u8), + /// Fractional spacing. + Fr(Fr), + /// An already layouted line of a paragraph. + Line(BumpBox<'a, LineChild>), + /// An unbreakable block. + Single(BumpBox<'a, SingleChild<'a>>), + /// A breakable block. + Multi(BumpBox<'a, MultiChild<'a>>), + /// An absolutely or floatingly placed element. + Placed(BumpBox<'a, PlacedChild<'a>>), + /// A place flush. + Flush, + /// An explicit column break. + Break(bool), +} + +/// A child that encapsulates a layouted line of a paragraph. +#[derive(Debug)] pub struct LineChild { pub frame: Frame, pub align: Axes, pub need: Abs, } -/// A child that encapsulates a prepared block. -pub struct BlockChild<'a> { +/// A child that encapsulates a prepared unbreakable block. +#[derive(Debug)] +pub struct SingleChild<'a> { pub align: Axes, pub sticky: bool, - pub rootable: bool, + pub fr: Option, elem: &'a Packed, styles: StyleChain<'a>, locator: Locator<'a>, + cell: CachedCell>, } -impl BlockChild<'_> { +impl SingleChild<'_> { + /// Build the child's frame given the region's base size. + pub fn layout(&self, engine: &mut Engine, base: Size) -> SourceResult { + self.cell.get_or_init(base, |base| { + self.elem + .layout_single(engine, self.locator.relayout(), self.styles, base) + .map(|frame| frame.post_processed(self.styles)) + }) + } +} + +/// A child that encapsulates a prepared breakable block. +#[derive(Debug)] +pub struct MultiChild<'a> { + pub align: Axes, + alone: bool, + elem: &'a Packed, + styles: StyleChain<'a>, + locator: Locator<'a>, + cell: CachedCell>, +} + +impl<'a> MultiChild<'a> { /// Build the child's frames given regions. - pub fn layout( + pub fn layout<'b>( + &'b self, + engine: &mut Engine, + regions: Regions, + ) -> SourceResult<(Frame, Option>)> { + let fragment = self.layout_impl(engine, regions)?; + + // Extract the first frame. + let mut frames = fragment.into_iter(); + let frame = frames.next().unwrap(); + + // If there's more, return a `spill`. + let mut spill = None; + if frames.next().is_some() { + spill = Some(MultiSpill { + multi: self, + full: regions.full, + first: regions.size.y, + backlog: vec![], + }); + } + + Ok((frame, spill)) + } + + /// The shared internal implementation of [`Self::layout`] and + /// [`MultiSpill::layout`]. + fn layout_impl( &self, engine: &mut Engine, regions: Regions, ) -> SourceResult { - let mut fragment = + self.cell.get_or_init(regions, |mut regions| { + // Vertical expansion is only kept if this block is the only child. + regions.expand.y &= self.alone; self.elem - .layout(engine, self.locator.relayout(), self.styles, regions)?; + .layout_multiple(engine, self.locator.relayout(), self.styles, regions) + .map(|mut fragment| { + for frame in &mut fragment { + frame.post_process(self.styles); + } + fragment + }) + }) + } +} - for frame in &mut fragment { - frame.post_process(self.styles); +/// The spilled remains of a `MultiChild` that broke across two regions. +#[derive(Debug, Clone)] +pub struct MultiSpill<'a, 'b> { + multi: &'b MultiChild<'a>, + first: Abs, + full: Abs, + backlog: Vec, +} + +impl MultiSpill<'_, '_> { + /// Build the spill's frames given regions. + pub fn layout( + mut self, + engine: &mut Engine, + regions: Regions, + ) -> SourceResult<(Frame, Option)> { + // We build regions for the whole `MultiChild` with the sizes passed to + // earlier parts of it plus the new regions. Then, we layout the + // complete block, but extract only the suffix that interests us. + self.backlog.push(regions.size.y); + + let mut backlog: Vec<_> = + self.backlog.iter().chain(regions.backlog).copied().collect(); + + // Remove unnecessary backlog items (also to prevent it from growing + // unnecessarily, which would change the region's hash). + while !backlog.is_empty() && backlog.last().copied() == regions.last { + backlog.pop(); } - Ok(fragment) + // Build the pod with the merged regions. + let pod = Regions { + size: Size::new(regions.size.x, self.first), + expand: regions.expand, + full: self.full, + backlog: &backlog, + last: regions.last, + }; + + // Extract the not-yet-processed frames. + let mut frames = self + .multi + .layout_impl(engine, pod)? + .into_iter() + .skip(self.backlog.len()); + + // Save the first frame. + let frame = frames.next().unwrap(); + + // If there's more, return a `spill`. + let mut spill = None; + if frames.next().is_some() { + spill = Some(self); + } + + Ok((frame, spill)) + } + + /// The alignment of the breakable block. + pub fn align(&self) -> Axes { + self.multi.align } } /// A child that encapsulates a prepared placed element. +#[derive(Debug)] pub struct PlacedChild<'a> { + pub idx: usize, + pub align_x: FixedAlignment, + pub align_y: Smart>, + pub scope: PlacementScope, pub float: bool, pub clearance: Abs, pub delta: Axes>, - pub align_x: FixedAlignment, - pub align_y: Smart>, elem: &'a Packed, styles: StyleChain<'a>, locator: Locator<'a>, alignment: Smart, + cell: CachedCell>, } impl PlacedChild<'_> { /// Build the child's frame given the region's base size. pub fn layout(&self, engine: &mut Engine, base: Size) -> SourceResult { - let align = self.alignment.unwrap_or_else(|| Alignment::CENTER); - let aligned = AlignElem::set_alignment(align).wrap(); - - let mut frame = layout_frame( - engine, - &self.elem.body, - self.locator.relayout(), - self.styles.chain(&aligned), - Region::new(base, Axes::splat(false)), - )?; - - frame.post_process(self.styles); - Ok(frame) + self.cell.get_or_init(base, |base| { + let align = self.alignment.unwrap_or_else(|| Alignment::CENTER); + let aligned = AlignElem::set_alignment(align).wrap(); + layout_frame( + engine, + &self.elem.body, + self.locator.relayout(), + self.styles.chain(&aligned), + Region::new(base, Axes::splat(false)), + ) + .map(|frame| frame.post_processed(self.styles)) + }) + } +} + +/// Wraps a parameterized computation and caches its latest output. +/// +/// - When the computation is performed multiple times consecutively with the +/// same argument, reuses the cache. +/// - When the argument changes, the new output is cached. +#[derive(Clone)] +struct CachedCell(RefCell>); + +impl CachedCell { + /// Create an empty cached cell. + fn new() -> Self { + Self(RefCell::new(None)) + } + + /// Perform the computation `f` with caching. + fn get_or_init(&self, input: I, f: F) -> T + where + I: Hash, + T: Clone, + F: FnOnce(I) -> T, + { + let input_hash = crate::utils::hash128(&input); + + let mut slot = self.0.borrow_mut(); + if let Some((hash, output)) = &*slot { + if *hash == input_hash { + return output.clone(); + } + } + + let output = f(input); + *slot = Some((input_hash, output.clone())); + output + } +} + +impl Default for CachedCell { + fn default() -> Self { + Self::new() + } +} + +impl Debug for CachedCell { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.pad("CachedCell(..)") } } diff --git a/crates/typst/src/layout/flow/compose.rs b/crates/typst/src/layout/flow/compose.rs new file mode 100644 index 000000000..e5d6f7b12 --- /dev/null +++ b/crates/typst/src/layout/flow/compose.rs @@ -0,0 +1,843 @@ +use std::num::NonZeroUsize; + +use super::{distribute, Config, FlowResult, PlacedChild, Skip, Stop, Work}; +use crate::diag::SourceResult; +use crate::engine::Engine; +use crate::foundations::{Content, NativeElement, Packed, Resolve, Smart}; +use crate::introspection::{ + Counter, CounterDisplayElem, CounterState, CounterUpdate, Locator, SplitLocator, + TagKind, +}; +use crate::layout::{ + layout_fragment, layout_frame, Abs, Axes, Dir, FixedAlignment, Frame, FrameItem, + OuterHAlignment, PlacementScope, Point, Region, Regions, Rel, Size, +}; +use crate::model::{ + FootnoteElem, FootnoteEntry, LineNumberingScope, Numbering, ParLine, ParLineMarker, +}; +use crate::syntax::Span; +use crate::utils::NonZeroExt; + +/// Composes the contents of a single page/region. A region can have multiple +/// columns/subregions. +/// +/// The composer is primarily concerned with layout of out-of-flow insertions +/// (floats and footnotes). It does this in per-page and per-column loops that +/// rerun when a new float is added (since it affects the regions available to +/// the distributor). +/// +/// To lay out the in-flow contents of individual subregions, the composer +/// invokes [distribution](distribute). +pub fn compose( + engine: &mut Engine, + work: &mut Work, + config: &Config, + locator: Locator, + regions: Regions, +) -> SourceResult { + Composer { + engine, + config, + page_base: regions.base(), + column: 0, + page_insertions: Insertions::default(), + column_insertions: Insertions::default(), + work, + footnote_spill: None, + footnote_queue: vec![], + } + .page(locator, regions) +} + +/// State for composition. +/// +/// Sadly, we need that many lifetimes because &mut references are invariant and +/// it would force the lifetimes of various things to be equal if they +/// shared a lifetime. +/// +/// The only interesting lifetimes are 'a and 'b. See [Work] for more details +/// about them. +pub struct Composer<'a, 'b, 'x, 'y> { + pub engine: &'x mut Engine<'y>, + pub work: &'x mut Work<'a, 'b>, + pub config: &'x Config<'x>, + column: usize, + page_base: Size, + page_insertions: Insertions<'a, 'b>, + column_insertions: Insertions<'a, 'b>, + // These are here because they have to survive relayout (we could lose the + // footnotes otherwise). For floats, we revisit them anyway, so it's okay to + // use `work.floats` directly. This is not super clean; probably there's a + // better way. + footnote_spill: Option>, + footnote_queue: Vec>, +} + +impl<'a, 'b> Composer<'a, 'b, '_, '_> { + /// Lay out a container/page region, including container/page insertions. + fn page(mut self, locator: Locator, regions: Regions) -> SourceResult { + // This loop can restart region layout when requested to do so by a + // `Stop`. This happens when there is a page-scoped float. + let checkpoint = self.work.clone(); + let output = loop { + // Shrink the available space by the space used by page + // insertions. + let mut pod = regions; + pod.size.y -= self.page_insertions.height(); + + match self.page_contents(locator.relayout(), pod) { + Ok(frame) => break frame, + Err(Stop::Finish(_)) => unreachable!(), + Err(Stop::Relayout(PlacementScope::Column)) => unreachable!(), + Err(Stop::Relayout(PlacementScope::Page)) => { + *self.work = checkpoint.clone(); + continue; + } + Err(Stop::Error(err)) => return Err(err), + }; + }; + drop(checkpoint); + + Ok(self.page_insertions.finalize(self.work, self.config, output)) + } + + /// Lay out the inner contents of a container/page. + fn page_contents(&mut self, locator: Locator, regions: Regions) -> FlowResult { + // No point in create column regions, if there's just one! + if self.config.columns.count == 1 { + return self.column(locator, regions); + } + + // Create a backlog for multi-column layout. + let column_height = regions.size.y; + let backlog: Vec<_> = std::iter::once(&column_height) + .chain(regions.backlog) + .flat_map(|&h| std::iter::repeat(h).take(self.config.columns.count)) + .skip(1) + .collect(); + + // Subregions for column layout. + let mut inner = Regions { + size: Size::new(self.config.columns.width, column_height), + backlog: &backlog, + expand: Axes::new(true, regions.expand.y), + ..regions + }; + + // The size of the merged frame hosting multiple columns. + let size = Size::new( + regions.size.x, + if regions.expand.y { regions.size.y } else { Abs::zero() }, + ); + + let mut output = Frame::hard(size); + let mut offset = Abs::zero(); + let mut locator = locator.split(); + + // Lay out the columns and stitch them together. + for i in 0..self.config.columns.count { + self.column = i; + let frame = self.column(locator.next(&()), inner)?; + + if !regions.expand.y { + output.size_mut().y.set_max(frame.height()); + } + + let width = frame.width(); + let x = if self.config.columns.dir == Dir::LTR { + offset + } else { + regions.size.x - offset - width + }; + offset += width + self.config.columns.gutter; + + output.push_frame(Point::with_x(x), frame); + inner.next(); + } + + Ok(output) + } + + /// Lay out a column, including column insertions. + fn column(&mut self, locator: Locator, regions: Regions) -> FlowResult { + // Reset column insertion when starting a new column. + self.column_insertions = Insertions::default(); + + // Process footnote spill. + if let Some(spill) = self.work.footnote_spill.take() { + self.footnote_spill(spill, regions.base())?; + } + + // This loop can restart column layout when requested to do so by a + // `Stop`. This happens when there is a column-scoped float. + let checkpoint = self.work.clone(); + let inner = loop { + // Shrink the available space by the space used by column + // insertions. + let mut pod = regions; + pod.size.y -= self.column_insertions.height(); + + match self.column_contents(pod) { + Ok(frame) => break frame, + Err(Stop::Finish(_)) => unreachable!(), + Err(Stop::Relayout(PlacementScope::Column)) => { + *self.work = checkpoint.clone(); + continue; + } + err => return err, + } + }; + drop(checkpoint); + + self.work.footnotes.extend(self.footnote_queue.drain(..)); + if let Some(spill) = self.footnote_spill.take() { + self.work.footnote_spill = Some(spill); + } + + let insertions = std::mem::take(&mut self.column_insertions); + let mut output = insertions.finalize(self.work, self.config, inner); + + // Lay out per-column line numbers. + if self.config.root { + layout_line_numbers( + self.engine, + self.config, + locator, + self.column, + &mut output, + )?; + } + + Ok(output) + } + + /// Lay out the inner contents of a column. + fn column_contents(&mut self, regions: Regions) -> FlowResult { + // Process pending footnotes. + for note in std::mem::take(&mut self.work.footnotes) { + self.footnote(note, &mut regions.clone(), Abs::zero(), false)?; + } + + // Process pending floats. + for placed in std::mem::take(&mut self.work.floats) { + self.float(placed, ®ions, true)?; + } + + distribute(self, regions) + } + + /// Lays out an item with floating placement. + /// + /// This is called from within [`distribute`]. When the float fits, this + /// returns an `Err(Stop::Relayout(..))`, which bubbles all the way through + /// distribution and is handled in [`Self::page`] or [`Self::column`] + /// (depending on `placed.scope`). + /// + /// When the float does not fit, it is queued into `work.floats`. The + /// value of `clearance` that between the float and flow content is needed + /// --- it is set if there are already distributed items. + pub fn float( + &mut self, + placed: &'b PlacedChild<'a>, + regions: &Regions, + clearance: bool, + ) -> FlowResult<()> { + // If the float is already processed, skip it. + if self.skipped(Skip::Placed(placed.idx)) { + return Ok(()); + } + + // If there is already a queued float, queue this one as well. We + // don't want to disrupt the order. + if !self.work.floats.is_empty() { + self.work.floats.push(placed); + return Ok(()); + } + + // Determine the base size of the chosen scope. + let base = match placed.scope { + PlacementScope::Column => regions.base(), + PlacementScope::Page => self.page_base, + }; + + // Lay out the placed element. + let frame = placed.layout(self.engine, base)?; + + // Determine the remaining space in the scope. This is exact for column + // placement, but only an approximation for page placement. + let remaining = match placed.scope { + PlacementScope::Column => regions.size.y, + PlacementScope::Page => { + let remaining: Abs = regions + .iter() + .map(|size| size.y) + .take(self.config.columns.count - self.column) + .sum(); + remaining / self.config.columns.count as f64 + } + }; + + // We only require clearance if there is other content. + let clearance = if clearance { Abs::zero() } else { placed.clearance }; + let need = frame.height() + clearance; + + // If the float doesn't fit, queue it for the next region. + if !remaining.fits(need) && !regions.in_last() { + self.work.floats.push(placed); + return Ok(()); + } + + // Handle footnotes in the float. + self.footnotes(regions, &frame, need, false)?; + + // Determine the float's vertical alignment. We can unwrap the inner + // `Option` because `Custom(None)` is checked for during collection. + let align_y = placed.align_y.map(Option::unwrap).unwrap_or_else(|| { + // When the float's vertical midpoint would be above the middle of + // the page if it were layouted in-flow, we use top alignment. + // Otherwise, we use bottom alignment. + let used = base.y - remaining; + let half = need / 2.0; + let ratio = (used + half) / base.y; + if ratio <= 0.5 { + FixedAlignment::Start + } else { + FixedAlignment::End + } + }); + + // Select the insertion area where we'll put this float. + let area = match placed.scope { + PlacementScope::Column => &mut self.column_insertions, + PlacementScope::Page => &mut self.page_insertions, + }; + + // Put the float there. + area.push_float(placed, frame, align_y); + area.skips.push(Skip::Placed(placed.idx)); + + // Trigger relayout. + Err(Stop::Relayout(placed.scope)) + } + + /// Lays out footnotes in the `frame` if this is the root flow and there are + /// any. The value of `breakable` indicates whether the element that + /// produced the frame is breakable. If not, the frame is treated as atomic. + pub fn footnotes( + &mut self, + regions: &Regions, + frame: &Frame, + flow_need: Abs, + breakable: bool, + ) -> FlowResult<()> { + // Footnotes are only supported at the root level. + if !self.config.root { + return Ok(()); + } + + // Search for footnotes. + let notes = find_in_frame::(frame); + if notes.is_empty() { + return Ok(()); + } + + let mut relayout = false; + let mut regions = *regions; + let mut migratable = !breakable && !regions.in_last(); + + for (y, elem) in notes { + // The amount of space used by the in-flow content that contains the + // footnote marker. For a breakable frame, it's the y position of + // the marker. For an unbreakable frame, it's the full height. + let flow_need = if breakable { y } else { flow_need }; + + // Process the footnote. + match self.footnote(elem, &mut regions, flow_need, migratable) { + // The footnote was already processed or queued. + Ok(()) => {} + // First handle more footnotes before relayouting. + Err(Stop::Relayout(_)) => relayout = true, + // Either of + // - A `Stop::Finish` indicating that the frame's origin element + // should migrate to uphold the footnote invariant. + // - A fatal error. + err => return err, + } + + // We only migrate the origin frame if the first footnote's first + // line didn't fit. + migratable = false; + } + + // If this is set, we laid out at least one footnote, so we need a + // relayout. + if relayout { + return Err(Stop::Relayout(PlacementScope::Column)); + } + + Ok(()) + } + + /// Handles a single footnote. + fn footnote( + &mut self, + elem: Packed, + regions: &mut Regions, + flow_need: Abs, + migratable: bool, + ) -> FlowResult<()> { + // Ignore reference footnotes and already processed ones. + let loc = elem.location().unwrap(); + if elem.is_ref() || self.skipped(Skip::Footnote(loc)) { + return Ok(()); + } + + // If there is already a queued spill or footnote, queue this one as + // well. We don't want to disrupt the order. + let area = &mut self.column_insertions; + if self.footnote_spill.is_some() || !self.footnote_queue.is_empty() { + self.footnote_queue.push(elem); + return Ok(()); + } + + // If there weren't any footnotes so far, account for the footnote + // separator. + let mut separator = None; + let mut separator_need = Abs::zero(); + if area.footnotes.is_empty() { + let frame = + layout_footnote_separator(self.engine, self.config, regions.base())?; + separator_need += self.config.footnote.clearance + frame.height(); + separator = Some(frame); + } + + // Prepare regions for the footnote. + let mut pod = *regions; + pod.expand.y = false; + pod.size.y -= flow_need + separator_need + self.config.footnote.gap; + + // Layout the footnote entry. + let frames = layout_fragment( + self.engine, + &FootnoteEntry::new(elem.clone()).pack(), + Locator::synthesize(elem.location().unwrap()), + self.config.shared, + pod, + )? + .into_frames(); + + // Find nested footnotes in the entry. + let nested = find_in_frames::(&frames); + + // Extract the first frame. + let mut iter = frames.into_iter(); + let first = iter.next().unwrap(); + let note_need = self.config.footnote.gap + first.height(); + + // If the first frame is empty, then none of its content fit. If + // possible, we then migrate the origin frame to the next region to + // uphold the footnote invariant (that marker and entry are on the same + // page). If not, we just queue the footnote for the next page. + if first.is_empty() { + if migratable { + return Err(Stop::Finish(false)); + } else { + self.footnote_queue.push(elem); + return Ok(()); + } + } + + // Save the separator. + if let Some(frame) = separator { + area.push_footnote_separator(self.config, frame); + regions.size.y -= separator_need; + } + + // Save the footnote's frame. + area.push_footnote(self.config, first); + area.skips.push(Skip::Footnote(loc)); + regions.size.y -= note_need; + + // Save the spill. + if !iter.as_slice().is_empty() { + self.footnote_spill = Some(iter); + } + + // Lay out nested footnotes. + for (_, note) in nested { + self.footnote(note, regions, flow_need, migratable)?; + } + + // Since we laid out a footnote, we need a relayout. + Err(Stop::Relayout(PlacementScope::Column)) + } + + /// Handles spillover from a footnote. + fn footnote_spill( + &mut self, + mut iter: std::vec::IntoIter, + base: Size, + ) -> SourceResult<()> { + let area = &mut self.column_insertions; + + // Create and save the separator. + let separator = layout_footnote_separator(self.engine, self.config, base)?; + area.push_footnote_separator(self.config, separator); + + // Save the footnote's frame. + let frame = iter.next().unwrap(); + area.push_footnote(self.config, frame); + + // Save the spill. + if !iter.as_slice().is_empty() { + self.footnote_spill = Some(iter); + } + + Ok(()) + } + + /// Checks whether an insertion was already processed and doesn't need to be + /// handled again. + fn skipped(&self, skip: Skip) -> bool { + self.work.skips.contains(&skip) + || self.page_insertions.skips.contains(&skip) + || self.column_insertions.skips.contains(&skip) + } + + /// The amount of width needed by insertions. + pub fn insertion_width(&self) -> Abs { + self.column_insertions.width.max(self.page_insertions.width) + } +} + +/// Lay out the footnote separator, typically a line. +fn layout_footnote_separator( + engine: &mut Engine, + config: &Config, + base: Size, +) -> SourceResult { + layout_frame( + engine, + &config.footnote.separator, + Locator::root(), + config.shared, + Region::new(base, Axes::new(config.footnote.expand, false)), + ) +} + +/// An additive list of insertions. +#[derive(Default)] +struct Insertions<'a, 'b> { + top_floats: Vec<(&'b PlacedChild<'a>, Frame)>, + bottom_floats: Vec<(&'b PlacedChild<'a>, Frame)>, + footnotes: Vec, + footnote_separator: Option, + top_size: Abs, + bottom_size: Abs, + width: Abs, + skips: Vec, +} + +impl<'a, 'b> Insertions<'a, 'b> { + /// Add a float to the top or bottom area. + fn push_float( + &mut self, + placed: &'b PlacedChild<'a>, + frame: Frame, + align_y: FixedAlignment, + ) { + self.width.set_max(frame.width()); + + let amount = frame.height() + placed.clearance; + let pair = (placed, frame); + + if align_y == FixedAlignment::Start { + self.top_size += amount; + self.top_floats.push(pair); + } else { + self.bottom_size += amount; + self.bottom_floats.push(pair); + } + } + + /// Add a footnote to the bottom area. + fn push_footnote(&mut self, config: &Config, frame: Frame) { + self.width.set_max(frame.width()); + self.bottom_size += config.footnote.gap + frame.height(); + self.footnotes.push(frame); + } + + /// Add a footnote separator to the bottom area. + fn push_footnote_separator(&mut self, config: &Config, frame: Frame) { + self.width.set_max(frame.width()); + self.bottom_size += config.footnote.clearance + frame.height(); + self.footnote_separator = Some(frame); + } + + /// The combined height of the top and bottom area (includings clearances). + /// Subtracting this from the total region size yields the available space + /// for distribution. + fn height(&self) -> Abs { + self.top_size + self.bottom_size + } + + /// Produce a frame for the full region based on the `inner` frame produced + /// by distribution or column layout. + fn finalize(self, work: &mut Work, config: &Config, inner: Frame) -> Frame { + work.extend_skips(&self.skips); + + if self.top_floats.is_empty() + && self.bottom_floats.is_empty() + && self.footnote_separator.is_none() + && self.footnotes.is_empty() + { + return inner; + } + + let size = inner.size() + Size::with_y(self.height()); + + let mut output = Frame::soft(size); + let mut offset_top = Abs::zero(); + let mut offset_bottom = size.y - self.bottom_size; + + for (placed, frame) in self.top_floats { + let x = placed.align_x.position(size.x - frame.width()); + let y = offset_top; + let delta = placed.delta.zip_map(size, Rel::relative_to).to_point(); + offset_top += frame.height() + placed.clearance; + output.push_frame(Point::new(x, y) + delta, frame); + } + + output.push_frame(Point::with_y(self.top_size), inner); + + if let Some(frame) = self.footnote_separator { + offset_bottom += config.footnote.clearance; + let y = offset_bottom; + offset_bottom += frame.height(); + output.push_frame(Point::with_y(y), frame); + } + + for frame in self.footnotes { + offset_bottom += config.footnote.gap; + let y = offset_bottom; + offset_bottom += frame.height(); + output.push_frame(Point::with_y(y), frame); + } + + for (placed, frame) in self.bottom_floats { + offset_bottom += placed.clearance; + let x = placed.align_x.position(size.x - frame.width()); + let y = offset_bottom; + let delta = placed.delta.zip_map(size, Rel::relative_to).to_point(); + offset_bottom += frame.height(); + output.push_frame(Point::new(x, y) + delta, frame); + } + + output + } +} + +/// Lay out the given collected lines' line numbers to an output frame. +/// +/// The numbers are placed either on the left margin (left border of the frame) +/// or on the right margin (right border). Before they are placed, a line number +/// counter reset is inserted if we're in the first column of the page being +/// currently laid out and the user requested for line numbers to be reset at +/// the start of every page. +fn layout_line_numbers( + engine: &mut Engine, + config: &Config, + locator: Locator, + column: usize, + output: &mut Frame, +) -> SourceResult<()> { + let mut locator = locator.split(); + + // Reset page-scoped line numbers if currently at the first column. + if column == 0 + && ParLine::numbering_scope_in(config.shared) == LineNumberingScope::Page + { + let reset = layout_line_number_reset(engine, config, &mut locator)?; + output.push_frame(Point::zero(), reset); + } + + // Find all line markers. + let mut lines = find_in_frame::(output); + if lines.is_empty() { + return Ok(()); + } + + // Assume the line numbers aren't sorted by height. They must be sorted so + // we can deduplicate line numbers below based on vertical proximity. + lines.sort_by_key(|&(y, _)| y); + + // Used for horizontal alignment. + let mut max_number_width = Abs::zero(); + + // This is used to skip lines that are too close together. + let mut prev_bottom = None; + + // Buffer line number frames so we can align them horizontally later before + // placing, based on the width of the largest line number. + let mut line_numbers = vec![]; + + // Layout the lines. + for &(y, ref marker) in &lines { + if prev_bottom.is_some_and(|bottom| y < bottom) { + // Lines are too close together. Display as the same line number. + continue; + } + + // Layout the number and record its width in search of the maximium. + let frame = layout_line_number(engine, config, &mut locator, &marker.numbering)?; + + // Note that this line.y is larger than the previous due to sorting. + // Therefore, the check at the top of the loop ensures no line numbers + // will reasonably intersect with each other. We enforce a minimum + // spacing of 1pt between consecutive line numbers in case a zero-height + // frame is used. + prev_bottom = Some(y + frame.height().max(Abs::pt(1.0))); + max_number_width.set_max(frame.width()); + line_numbers.push((y, marker, frame)); + } + + for (y, marker, frame) in line_numbers { + // The last column will always place line numbers at the end + // margin. This should become configurable in the future. + let margin = { + let opposite = + config.columns.count >= 2 && column + 1 == config.columns.count; + if opposite { OuterHAlignment::End } else { marker.number_margin } + .resolve(config.shared) + }; + + // Compute the marker's horizontal position. Will be adjusted based on + // the maximum number width later. + let clearance = marker.number_clearance.resolve(config.shared); + + // Compute the base X position. + let x = match margin { + // Move the number to the left of the left edge (at 0pt) by the maximum + // width and the clearance. + FixedAlignment::Start => -max_number_width - clearance, + // Move the number to the right edge and add clearance. + FixedAlignment::End => output.width() + clearance, + // Can't happen due to `OuterHAlignment`. + FixedAlignment::Center => unreachable!(), + }; + + // Determine how much to shift the number due to its alignment. + let shift = { + let align = marker + .number_align + .map(|align| align.resolve(config.shared)) + .unwrap_or_else(|| margin.inv()); + align.position(max_number_width - frame.width()) + }; + + // Compute the final position of the number and add it to the output. + let pos = Point::new(x + shift, y); + output.push_frame(pos, frame); + } + + Ok(()) +} + +/// Creates a frame that resets the line number counter. +fn layout_line_number_reset( + engine: &mut Engine, + config: &Config, + locator: &mut SplitLocator, +) -> SourceResult { + let counter = Counter::of(ParLineMarker::elem()); + let update = CounterUpdate::Set(CounterState::init(false)); + let content = counter.update(Span::detached(), update); + layout_frame( + engine, + &content, + locator.next(&()), + config.shared, + Region::new(Axes::splat(Abs::zero()), Axes::splat(false)), + ) +} + +/// Layout the line number associated with the given line marker. +/// +/// Produces a counter update and counter display with counter key +/// `ParLineMarker`. We use `ParLineMarker` as it is an element which is not +/// exposed to the user and we don't want to expose the line number counter at +/// the moment, given that its semantics are inconsistent with that of normal +/// counters (the counter is updated based on height and not on frame order / +/// layer). When we find a solution to this, we should switch to a counter on +/// `ParLine` instead, thus exposing the counter as `counter(par.line)` to the +/// user. +fn layout_line_number( + engine: &mut Engine, + config: &Config, + locator: &mut SplitLocator, + numbering: &Numbering, +) -> SourceResult { + let counter = Counter::of(ParLineMarker::elem()); + let update = CounterUpdate::Step(NonZeroUsize::ONE); + let numbering = Smart::Custom(numbering.clone()); + + // Combine counter update and display into the content we'll layout. + let content = Content::sequence(vec![ + counter.clone().update(Span::detached(), update), + CounterDisplayElem::new(counter, numbering, false).pack(), + ]); + + // Layout the number. + let mut frame = layout_frame( + engine, + &content, + locator.next(&()), + config.shared, + Region::new(Axes::splat(Abs::inf()), Axes::splat(false)), + )?; + + // Ensure the baseline of the line number aligns with the line's baseline. + frame.translate(Point::with_y(-frame.baseline())); + + Ok(frame) +} + +/// Collect all matching elements and their vertical positions in the frame. +/// +/// On each subframe we encounter, we add that subframe's position to `prev_y`, +/// until we reach a tag, at which point we add the tag's position and finish. +/// That gives us the absolute height of the tag from the start of the root +/// frame. +fn find_in_frame(frame: &Frame) -> Vec<(Abs, Packed)> { + let mut output = vec![]; + find_in_frame_impl(&mut output, frame, Abs::zero()); + output +} + +/// Collect all matching elements and their vertical positions in the frames. +fn find_in_frames(frames: &[Frame]) -> Vec<(Abs, Packed)> { + let mut output = vec![]; + for frame in frames { + find_in_frame_impl(&mut output, frame, Abs::zero()); + } + output +} + +fn find_in_frame_impl( + output: &mut Vec<(Abs, Packed)>, + frame: &Frame, + y_offset: Abs, +) { + for (pos, item) in frame.items() { + let y = y_offset + pos.y; + match item { + FrameItem::Group(group) => find_in_frame_impl(output, &group.frame, y), + FrameItem::Tag(tag) if tag.kind() == TagKind::Start => { + if let Some(elem) = tag.elem().to_packed::() { + output.push((y, elem.clone())); + } + } + _ => {} + } + } +} diff --git a/crates/typst/src/layout/flow/distribute.rs b/crates/typst/src/layout/flow/distribute.rs new file mode 100644 index 000000000..71f9598b8 --- /dev/null +++ b/crates/typst/src/layout/flow/distribute.rs @@ -0,0 +1,512 @@ +use super::{ + Child, Composer, FlowResult, LineChild, MultiChild, MultiSpill, PlacedChild, + SingleChild, Stop, Work, +}; +use crate::introspection::Tag; +use crate::layout::{ + Abs, Axes, FixedAlignment, Fr, Frame, FrameItem, Point, Region, Regions, Rel, Size, +}; +use crate::utils::Numeric; + +/// Distributes as many children as fit from `composer.work` into the first +/// region and returns the resulting frame. +pub fn distribute(composer: &mut Composer, regions: Regions) -> FlowResult { + let mut distributor = Distributor { + composer, + regions, + items: vec![], + sticky: None, + stickable: false, + }; + let init = distributor.snapshot(); + let forced = match distributor.run() { + Ok(()) => true, + Err(Stop::Finish(forced)) => forced, + Err(err) => return Err(err), + }; + let region = Region::new(regions.size, regions.expand); + distributor.finalize(region, init, forced) +} + +/// State for distribution. +/// +/// See [Composer] regarding lifetimes. +struct Distributor<'a, 'b, 'x, 'y, 'z> { + /// The composer that is used to handle insertions. + composer: &'z mut Composer<'a, 'b, 'x, 'y>, + /// Regions which are continously shrunk as new items are added. + regions: Regions<'z>, + /// Already laid out items, not yet aligned. + items: Vec>, + /// A snapshot which can be restored to migrate a suffix of sticky blocks to + /// the next region. + sticky: Option>, + /// Whether there was at least one proper block. Otherwise, sticky blocks + /// are disabled (or else they'd keep being migrated). + stickable: bool, +} + +/// A snapshot of the distribution state. +struct DistributionSnapshot<'a, 'b> { + work: Work<'a, 'b>, + items: usize, +} + +/// A laid out item in a distribution. +enum Item<'a, 'b> { + /// Absolute spacing and its weakness level. + Abs(Abs, u8), + /// Fractional spacing or a fractional block. + Fr(Fr, Option<&'b SingleChild<'a>>), + /// A frame for a laid out line or block. + Frame(Frame, Axes), + /// A frame for an absolutely (not floatingly) placed child. + Placed(Frame, &'b PlacedChild<'a>), +} + +impl Item<'_, '_> { + /// Whether this item should be migrated to the next region if the region + /// consists solely of such items. + fn migratable(&self) -> bool { + match self { + Self::Frame(frame, _) => { + frame.size().is_zero() + && frame.items().all(|(_, item)| { + matches!(item, FrameItem::Link(_, _) | FrameItem::Tag(_)) + }) + } + Self::Placed(_, placed) => !placed.float, + _ => false, + } + } +} + +impl<'a, 'b> Distributor<'a, 'b, '_, '_, '_> { + /// Distributes content into the region. + fn run(&mut self) -> FlowResult<()> { + // First, handle spill of a breakable block. + if let Some(spill) = self.composer.work.spill.take() { + self.multi_spill(spill)?; + } + + // If spill are taken care of, process children until no space is left + // or no children are left. + while let Some(child) = self.composer.work.head() { + self.child(child)?; + self.composer.work.advance(); + } + + Ok(()) + } + + /// Processes a single child. + /// + /// - Returns `Ok(())` if the child was successfully processed. + /// - Returns `Err(Stop::Finish)` if a region break should be triggered. + /// - Returns `Err(Stop::Relayout(_))` if the region needs to be relayouted + /// due to an insertion (float/footnote). + /// - Returns `Err(Stop::Error(_))` if there was a fatal error. + fn child(&mut self, child: &'b Child<'a>) -> FlowResult<()> { + match child { + Child::Tag(tag) => self.tag(tag), + Child::Rel(amount, weakness) => self.rel(*amount, *weakness), + Child::Fr(fr) => self.fr(*fr), + Child::Line(line) => self.line(line)?, + Child::Single(single) => self.single(single)?, + Child::Multi(multi) => self.multi(multi)?, + Child::Placed(placed) => self.placed(placed)?, + Child::Flush => self.flush()?, + Child::Break(weak) => self.break_(*weak)?, + } + Ok(()) + } + + /// Processes a tag. + fn tag(&mut self, tag: &'a Tag) { + self.composer.work.tags.push(tag); + } + + /// Processes relative spacing. + fn rel(&mut self, amount: Rel, weakness: u8) { + let amount = amount.relative_to(self.regions.base().y); + if weakness > 0 && !self.keep_spacing(amount, weakness) { + return; + } + + self.regions.size.y -= amount; + self.items.push(Item::Abs(amount, weakness)); + } + + /// Processes fractional spacing. + fn fr(&mut self, fr: Fr) { + self.trim_spacing(); + self.items.push(Item::Fr(fr, None)); + } + + /// Decides whether to keep weak spacing based on previous items. If there + /// is a preceding weak spacing, it might be patched in place. + fn keep_spacing(&mut self, amount: Abs, weakness: u8) -> bool { + for item in self.items.iter_mut().rev() { + match *item { + Item::Abs(prev_amount, prev_weakness @ 1..) => { + if weakness <= prev_weakness + && (weakness < prev_weakness || amount > prev_amount) + { + self.regions.size.y -= amount - prev_amount; + *item = Item::Abs(amount, weakness); + } + return false; + } + Item::Abs(..) | Item::Placed(..) => {} + Item::Fr(.., None) => return false, + Item::Frame(..) | Item::Fr(.., Some(_)) => return true, + } + } + false + } + + /// Trims trailing weak spacing from the items. + fn trim_spacing(&mut self) { + for (i, item) in self.items.iter().enumerate().rev() { + match *item { + Item::Abs(amount, 1..) => { + self.regions.size.y += amount; + self.items.remove(i); + break; + } + Item::Abs(..) | Item::Placed(..) => {} + Item::Frame(..) | Item::Fr(..) => break, + } + } + } + + /// The amount of trailing weak spacing. + fn weak_spacing(&mut self) -> Abs { + for item in self.items.iter().rev() { + match *item { + Item::Abs(amount, 1..) => return amount, + Item::Abs(..) | Item::Placed(..) => {} + Item::Frame(..) | Item::Fr(..) => break, + } + } + Abs::zero() + } + + /// Processes a line of a paragraph. + fn line(&mut self, line: &'b LineChild) -> FlowResult<()> { + // If the line doesn't fit and we're allowed to break, finish the + // region. + if !self.regions.size.y.fits(line.frame.height()) && !self.regions.in_last() { + return Err(Stop::Finish(false)); + } + + // If the line's need, which includes its own height and that of + // following lines grouped by widow/orphan prevention, does not fit into + // the current region, but does fit into the next region, finish the + // region. + if !self.regions.size.y.fits(line.need) + && self + .regions + .iter() + .nth(1) + .is_some_and(|region| region.y.fits(line.need)) + { + return Err(Stop::Finish(false)); + } + + self.frame(line.frame.clone(), line.align, false, false) + } + + /// Processes an unbreakable block. + fn single(&mut self, single: &'b SingleChild<'a>) -> FlowResult<()> { + // Handle fractionally sized blocks. + if let Some(fr) = single.fr { + self.items.push(Item::Fr(fr, Some(single))); + return Ok(()); + } + + // Lay out the block. + let frame = single.layout(self.composer.engine, self.regions.base())?; + + // If the block doesn't fit and we're allowed to break, finish the + // region. + if !self.regions.size.y.fits(frame.height()) && !self.regions.in_last() { + return Err(Stop::Finish(false)); + } + + self.frame(frame, single.align, single.sticky, false) + } + + /// Processes a breakable block. + fn multi(&mut self, multi: &'b MultiChild<'a>) -> FlowResult<()> { + // Skip directly if the region is already (over)full. `line` and + // `single` implicitly do this through their `fits` checks. + if self.regions.is_full() { + return Err(Stop::Finish(false)); + } + + // Lay out the block. + let (frame, spill) = multi.layout(self.composer.engine, self.regions)?; + self.frame(frame, multi.align, false, true)?; + + // If the block didn't fully fit into the current region, save it into + // the `spill` and finish the region. + if let Some(spill) = spill { + self.composer.work.spill = Some(spill); + self.composer.work.advance(); + return Err(Stop::Finish(false)); + } + + Ok(()) + } + + /// Processes spillover from a breakable block. + fn multi_spill(&mut self, spill: MultiSpill<'a, 'b>) -> FlowResult<()> { + // Skip directly if the region is already (over)full. + if self.regions.is_full() { + self.composer.work.spill = Some(spill); + return Err(Stop::Finish(false)); + } + + // Lay out the spilled remains. + let align = spill.align(); + let (frame, spill) = spill.layout(self.composer.engine, self.regions)?; + self.frame(frame, align, false, true)?; + + // If there's still more, save it into the `spill` and finish the + // region. + if let Some(spill) = spill { + self.composer.work.spill = Some(spill); + return Err(Stop::Finish(false)); + } + + Ok(()) + } + + /// Processes an in-flow frame, generated from a line or block. + fn frame( + &mut self, + mut frame: Frame, + align: Axes, + sticky: bool, + breakable: bool, + ) -> FlowResult<()> { + if sticky { + // If the frame is sticky and we haven't remember a preceding sticky + // element, make a checkpoint which we can restore should we end on + // this sticky element. + if self.stickable && self.sticky.is_none() { + self.sticky = Some(self.snapshot()); + } + } else if !frame.is_empty() { + // If the frame isn't sticky, we can forget a previous snapshot. + self.stickable = true; + self.sticky = None; + } + + if !frame.is_empty() { + // Drain tags. + let tags = &mut self.composer.work.tags; + if !tags.is_empty() { + frame.prepend_multiple( + tags.iter().map(|&tag| (Point::zero(), FrameItem::Tag(tag.clone()))), + ); + } + + // Handle footnotes. + self.composer + .footnotes(&self.regions, &frame, frame.height(), breakable)?; + + // Clear the drained tags _after_ the footnotes are handled because + // a [`Stop::Finish`] could otherwise lose them. + self.composer.work.tags.clear(); + } + + // Push an item for the frame. + self.regions.size.y -= frame.height(); + self.items.push(Item::Frame(frame, align)); + Ok(()) + } + + /// Processes an absolutely or floatingly placed child. + fn placed(&mut self, placed: &'b PlacedChild<'a>) -> FlowResult<()> { + if placed.float { + // If the element is floatingly placed, let the composer handle it. + // It might require relayout because the area available for + // distribution shrinks. We make the spacing occupied by weak + // spacing temporarily available again because it can collapse if it + // ends up at a break due to the float. + let weak_spacing = self.weak_spacing(); + self.regions.size.y += weak_spacing; + self.composer.float(placed, &self.regions, self.items.is_empty())?; + self.regions.size.y -= weak_spacing; + } else { + let frame = placed.layout(self.composer.engine, self.regions.base())?; + self.composer.footnotes(&self.regions, &frame, Abs::zero(), true)?; + self.items.push(Item::Placed(frame, placed)); + } + Ok(()) + } + + /// Processes a float flush. + fn flush(&mut self) -> FlowResult<()> { + // If there are still pending floats, finish the region instead of + // adding more content to it. + if !self.composer.work.floats.is_empty() { + return Err(Stop::Finish(false)); + } + Ok(()) + } + + /// Processes a column break. + fn break_(&mut self, weak: bool) -> FlowResult<()> { + // If there is a region to break into, break into it. + if (!weak || !self.items.is_empty()) + && (!self.regions.backlog.is_empty() || self.regions.last.is_some()) + { + self.composer.work.advance(); + return Err(Stop::Finish(true)); + } + Ok(()) + } + + /// Arranges the produced items into an output frame. + /// + /// This performs alignment and resolves fractional spacing and blocks. + fn finalize( + mut self, + region: Region, + init: DistributionSnapshot<'a, 'b>, + forced: bool, + ) -> FlowResult { + if !forced { + if !self.items.is_empty() && self.items.iter().all(Item::migratable) { + // Restore the initial state of all items are migratable. + self.restore(init); + } else { + // If we ended on a sticky block, but are not yet at the end of + // the flow, restore the saved checkpoint to move the sticky + // suffix to the next region. + if let Some(snapshot) = self.sticky.take() { + self.restore(snapshot) + } + } + } + + self.trim_spacing(); + + let mut frs = Fr::zero(); + let mut used = Size::zero(); + + // Determine the amount of used space and the sum of fractionals. + for item in &self.items { + match item { + Item::Abs(v, _) => used.y += *v, + Item::Fr(v, _) => frs += *v, + Item::Frame(frame, _) => { + used.y += frame.height(); + used.x.set_max(frame.width()); + } + Item::Placed(..) => {} + } + } + + // When we have fractional spacing, occupy the remaining space with it. + let mut fr_space = Abs::zero(); + let mut fr_frames = vec![]; + if frs.get() > 0.0 && region.size.y.is_finite() { + fr_space = region.size.y - used.y; + used.y = region.size.y; + + // Lay out fractionally sized blocks. + for item in &self.items { + let Item::Fr(v, Some(single)) = item else { continue }; + let length = v.share(frs, fr_space); + let base = Size::new(region.size.x, length); + let frame = single.layout(self.composer.engine, base)?; + used.x.set_max(frame.width()); + fr_frames.push(frame); + } + } + + // Also consider the width of insertions for alignment. + if !region.expand.x { + used.x.set_max(self.composer.insertion_width()); + } + + // Determine the region's size. + let size = region.expand.select(region.size, used.min(region.size)); + + let mut output = Frame::soft(size); + let mut ruler = FixedAlignment::Start; + let mut offset = Abs::zero(); + let mut fr_frames = fr_frames.into_iter(); + + // Position all items. + for item in self.items { + match item { + Item::Abs(v, _) => { + offset += v; + } + Item::Fr(v, single) => { + let length = v.share(frs, fr_space); + if let Some(single) = single { + let frame = fr_frames.next().unwrap(); + let x = single.align.x.position(size.x - frame.width()); + let pos = Point::new(x, offset); + output.push_frame(pos, frame); + } + offset += length; + } + Item::Frame(frame, align) => { + ruler = ruler.max(align.y); + + let x = align.x.position(size.x - frame.width()); + let y = offset + ruler.position(size.y - used.y); + let pos = Point::new(x, y); + offset += frame.height(); + + output.push_frame(pos, frame); + } + Item::Placed(frame, placed) => { + let x = placed.align_x.position(size.x - frame.width()); + let y = match placed.align_y.unwrap_or_default() { + Some(align) => align.position(size.y - frame.height()), + _ => offset + ruler.position(size.y - used.y), + }; + + let pos = Point::new(x, y) + + placed.delta.zip_map(size, Rel::relative_to).to_point(); + + output.push_frame(pos, frame); + } + } + } + + // If this is the very end of the flow, drain trailing tags. + if forced && !self.composer.work.tags.is_empty() { + let tags = &mut self.composer.work.tags; + let pos = Point::with_y(offset); + output.push_multiple( + tags.iter().map(|&tag| (pos, FrameItem::Tag(tag.clone()))), + ); + tags.clear(); + } + + Ok(output) + } + + /// Create a snapshot of the work and items. + fn snapshot(&self) -> DistributionSnapshot<'a, 'b> { + DistributionSnapshot { + work: self.composer.work.clone(), + items: self.items.len(), + } + } + + /// Restore a snapshot of the work and items. + fn restore(&mut self, snapshot: DistributionSnapshot<'a, 'b>) { + *self.composer.work = snapshot.work; + self.items.truncate(snapshot.items); + } +} diff --git a/crates/typst/src/layout/flow/mod.rs b/crates/typst/src/layout/flow/mod.rs index 351ef6b11..2491ef7c8 100644 --- a/crates/typst/src/layout/flow/mod.rs +++ b/crates/typst/src/layout/flow/mod.rs @@ -1,41 +1,40 @@ -//! Layout of content -//! - at the top-level, into a [`Document`]. -//! - inside of a container, into a [`Frame`] or [`Fragment`]. +//! Layout of content into a [`Frame`] or [`Fragment`]. mod collect; +mod compose; +mod distribute; use std::collections::HashSet; use std::num::NonZeroUsize; +use std::rc::Rc; use bumpalo::Bump; use comemo::{Track, Tracked, TrackedMut}; +use ecow::EcoVec; -use self::collect::{collect, BlockChild, Child, LineChild, PlacedChild}; -use crate::diag::{bail, At, SourceResult}; -use crate::engine::{Engine, Route, Sink, Traced}; -use crate::foundations::{ - Content, NativeElement, Packed, Resolve, SequenceElem, Smart, StyleChain, +use self::collect::{ + collect, Child, LineChild, MultiChild, MultiSpill, PlacedChild, SingleChild, }; +use self::compose::{compose, Composer}; +use self::distribute::distribute; +use crate::diag::{bail, At, SourceDiagnostic, SourceResult}; +use crate::engine::{Engine, Route, Sink, Traced}; +use crate::foundations::{Content, Packed, StyleChain}; use crate::introspection::{ - Counter, CounterDisplayElem, CounterKey, CounterState, CounterUpdate, Introspector, - Location, Locator, LocatorLink, SplitLocator, Tag, + Introspector, Location, Locator, LocatorLink, SplitLocator, Tag, }; use crate::layout::{ - Abs, Axes, BlockElem, Dir, FixedAlignment, Fr, Fragment, Frame, FrameItem, - OuterHAlignment, Point, Region, Regions, Rel, Size, -}; -use crate::model::{ - FootnoteElem, FootnoteEntry, ParLine, ParLineMarker, ParLineNumberingScope, + Abs, Dir, Fragment, Frame, PlacementScope, Region, Regions, Rel, Size, }; +use crate::model::{FootnoteElem, FootnoteEntry}; use crate::realize::{realize, Arenas, Pair, RealizationKind}; -use crate::syntax::Span; use crate::text::TextElem; use crate::utils::{NonZeroExt, Numeric}; use crate::World; -/// Layout content into multiple regions. +/// Lays out content into multiple regions. /// -/// When just layouting into a single region, prefer [`layout_frame`]. +/// When laying out into just one region, prefer [`layout_frame`]. pub fn layout_fragment( engine: &mut Engine, content: &Content, @@ -58,12 +57,10 @@ pub fn layout_fragment( ) } -/// Layout content into regions with columns. +/// Lays out content into regions with columns. /// -/// For now, this just invokes normal layout on cycled smaller regions. However, -/// in the future, columns will be able to interact (e.g. through floating -/// figures), so this is already factored out because it'll be conceptually -/// different from just layouting into more smaller regions. +/// This is different from just laying out into column-sized regions as the +/// columns can interact due to page-scoped placed elements. pub fn layout_fragment_with_columns( engine: &mut Engine, content: &Content, @@ -88,7 +85,7 @@ pub fn layout_fragment_with_columns( ) } -/// Layout content into a single region. +/// Lays out content into a single region, producing a single frame. pub fn layout_frame( engine: &mut Engine, content: &Content, @@ -116,6 +113,13 @@ fn layout_fragment_impl( columns: NonZeroUsize, column_gutter: Rel, ) -> SourceResult { + if !regions.size.x.is_finite() && regions.expand.x { + bail!(content.span(), "cannot expand into infinite width"); + } + if !regions.size.y.is_finite() && regions.expand.y { + bail!(content.span(), "cannot expand into infinite height"); + } + let link = LocatorLink::new(locator); let mut locator = Locator::link(&link).split(); let mut engine = Engine { @@ -140,1108 +144,219 @@ fn layout_fragment_impl( layout_flow( &mut engine, - &arenas.bump, &children, &mut locator, styles, regions, columns, column_gutter, - content.span(), + false, ) } -/// Layout flow content. +/// Lays out realized content into regions, potentially with columns. #[allow(clippy::too_many_arguments)] pub(crate) fn layout_flow( engine: &mut Engine, - bump: &Bump, children: &[Pair], locator: &mut SplitLocator, shared: StyleChain, - regions: Regions, + mut regions: Regions, columns: NonZeroUsize, column_gutter: Rel, - span: Span, + root: bool, ) -> SourceResult { - // Separating the infinite space into infinite columns does not make - // much sense. - let mut columns = columns.get(); - if !regions.size.x.is_finite() { - columns = 1; - } - - // Determine the width of the gutter and each column. - let column_gutter = column_gutter.relative_to(regions.base().x); - - let backlog: Vec; - let mut pod = if columns > 1 { - backlog = std::iter::once(®ions.size.y) - .chain(regions.backlog) - .flat_map(|&height| std::iter::repeat(height).take(columns)) - .skip(1) - .collect(); - - let width = - (regions.size.x - column_gutter * (columns - 1) as f64) / columns as f64; - - // Create the pod regions. - Regions { - size: Size::new(width, regions.size.y), - full: regions.full, - backlog: &backlog, - last: regions.last, - expand: Axes::new(true, regions.expand.y), - root: regions.root, - } - } else { - regions - }; - - // The children aren't root. - pod.root = false; - - // Check whether we have just a single multiple-layoutable element. In - // that case, we do not set `expand.y` to `false`, but rather keep it at - // its original value (since that element can take the full space). - // - // Consider the following code: `block(height: 5cm, pad(10pt, - // align(bottom, ..)))`. Thanks to the code below, the expansion will be - // passed all the way through the block & pad and reach the innermost - // flow, so that things are properly bottom-aligned. - let mut alone = false; - if let [(child, _)] = children { - alone = child.is::(); - } - - // Disable vertical expansion when there are multiple or not directly - // layoutable children. - if !alone { - pod.expand.y = false; - } - - let children = - collect(engine, bump, children, locator.next(&()), pod.base(), pod.expand.x)?; - - let layouter = FlowLayouter { - engine, - span, - root: regions.root, - locator, + // Prepare configuration that is shared across the whole flow. + let config = Config { + root, shared, - columns, - column_gutter, - regions: pod, - expand: regions.expand, - initial: pod.size, - items: vec![], - pending_tags: vec![], - pending_floats: vec![], - has_footnotes: false, - footnote_config: FootnoteConfig { + columns: { + let mut count = columns.get(); + if !regions.size.x.is_finite() { + count = 1; + } + + let gutter = column_gutter.relative_to(regions.base().x); + let width = (regions.size.x - gutter * (count - 1) as f64) / count as f64; + let dir = TextElem::dir_in(shared); + ColumnConfig { count, width, gutter, dir } + }, + footnote: FootnoteConfig { separator: FootnoteEntry::separator_in(shared), clearance: FootnoteEntry::clearance_in(shared), gap: FootnoteEntry::gap_in(shared), + expand: regions.expand.x, }, - visited_footnotes: HashSet::new(), - finished: vec![], }; - layouter.layout(&children, regions) + // Collect the elements into pre-processed children. These are much easier + // to handle than the raw elements. + let bump = Bump::new(); + let children = collect( + engine, + &bump, + children, + locator.next(&()), + Size::new(config.columns.width, regions.full), + regions.expand.x, + )?; + + let mut work = Work::new(&children); + let mut finished = vec![]; + + // This loop runs once per region produced by the flow layout. + loop { + let frame = compose(engine, &mut work, &config, locator.next(&()), regions)?; + finished.push(frame); + + // Terminate the loop when everything is processed, though draining the + // backlog if necessary. + if work.done() && (!regions.expand.y || regions.backlog.is_empty()) { + break; + } + + regions.next(); + } + + Ok(Fragment::frames(finished)) } -/// Layouts a collection of block-level elements. -struct FlowLayouter<'a, 'b, 'x, 'y> { - /// The engine. - engine: &'a mut Engine<'x>, - /// A span to use for errors. - span: Span, - /// Whether this is the root flow. +/// The work that is left to do by flow layout. +/// +/// The lifetimes 'a and 'b are used across flow layout: +/// - 'a is that of the content coming out of realization +/// - 'b is that of the collected/prepared children +#[derive(Clone)] +struct Work<'a, 'b> { + /// Children that we haven't processed yet. This slice shrinks over time. + children: &'b [Child<'a>], + /// Leftovers from a breakable block. + spill: Option>, + /// Queued floats that didn't fit in previous regions. + floats: EcoVec<&'b PlacedChild<'a>>, + /// Queued footnotes that didn't fit in previous regions. + footnotes: EcoVec>, + /// Spilled frames of a footnote that didn't fully fit. Similar to `spill`. + footnote_spill: Option>, + /// Queued tags that will be attached to the next frame. + tags: EcoVec<&'a Tag>, + /// Identifies floats and footnotes that can be skipped if visited because + /// they were already handled and incorporated as column or page level + /// insertions. + skips: Rc>, +} + +/// Identifies an element that that can be skipped if visited because it was +/// already processed. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +enum Skip { + /// Uniquely identifies a placed elements. We can't use a [`Location`] + /// because `PlaceElem` is not currently locatable. + Placed(usize), + /// Uniquely identifies a footnote. + Footnote(Location), +} + +impl<'a, 'b> Work<'a, 'b> { + /// Create the initial work state from a list of children. + fn new(children: &'b [Child<'a>]) -> Self { + Self { + children, + spill: None, + floats: EcoVec::new(), + footnotes: EcoVec::new(), + footnote_spill: None, + tags: EcoVec::new(), + skips: Rc::new(HashSet::new()), + } + } + + /// Get the first unprocessed child, from the start of the slice. + fn head(&self) -> Option<&'b Child<'a>> { + self.children.first() + } + + /// Mark the `head()` child as processed, advancing the slice by one. + fn advance(&mut self) { + self.children = &self.children[1..]; + } + + /// Whether all work is done. This means we can terminate flow layout. + fn done(&self) -> bool { + self.children.is_empty() + && self.spill.is_none() + && self.floats.is_empty() + && self.footnote_spill.is_none() + && self.footnotes.is_empty() + } + + /// Add skipped floats and footnotes from the insertion areas to the skip + /// set. + fn extend_skips(&mut self, skips: &[Skip]) { + if !skips.is_empty() { + Rc::make_mut(&mut self.skips).extend(skips.iter().copied()); + } + } +} + +/// Shared configuration for the whole flow. +struct Config<'x> { + /// Whether this is the root flow, which can host footnotes and line + /// numbers. root: bool, - /// Provides unique locations to the flow's children. - locator: &'a mut SplitLocator<'y>, - /// The shared styles. - shared: StyleChain<'a>, - /// The number of columns. - columns: usize, - /// The gutter between columns. - column_gutter: Abs, - /// The regions to layout children into. These already incorporate the - /// columns. - regions: Regions<'a>, - /// Whether the flow should expand to fill the region. - expand: Axes, - /// The initial size of `regions.size` that was available before we started - /// subtracting. - initial: Size, - /// 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 Tag>, - /// A queue of floating elements. - pending_floats: Vec>, - /// Whether we have any footnotes in the current region. - has_footnotes: bool, - /// Footnote configuration. - footnote_config: FootnoteConfig, - /// Footnotes that we have already processed. - visited_footnotes: HashSet, - /// Finished frames for previous regions. - finished: Vec, + /// The styles shared by the whole flow. This is used for footnotes and line + /// numbers. + shared: StyleChain<'x>, + /// Settings for columns. + columns: ColumnConfig, + /// Settings for footnotes. + footnote: FootnoteConfig, } -/// Cached footnote configuration. +/// Configuration of footnotes. struct FootnoteConfig { + /// The separator between flow content and footnotes. Typically a line. separator: Content, + /// The amount of space left above the separator. clearance: Abs, + /// The gap between footnote entries. gap: Abs, + /// Whether horizontal expansion is enabled for footnotes. + expand: bool, } -/// Information needed to generate a line number. -struct CollectedParLine { - y: Abs, - marker: Packed, +/// Configuration of columns. +struct ColumnConfig { + /// The number of columns. + count: usize, + /// The width of each column. + width: Abs, + /// The amount of space between columns. + gutter: Abs, + /// The horizontal direction in which columns progress. Defined by + /// `text.dir`. + dir: Dir, } -/// A prepared item in a flow layout. -enum FlowItem<'a, 'b> { - /// Spacing between other items and its weakness level. - Absolute(Abs, u8), - /// Fractional spacing between other items. - Fractional(Fr), - /// A frame for a layouted block. - Frame { - /// The frame itself. - frame: Frame, - /// How to align the frame. - align: Axes, - /// Whether the frame sticks to the item after it (for orphan prevention). - sticky: bool, - /// Whether the frame comes from a rootable block, which may be laid - /// out as a root flow and thus display its own line numbers. - /// Therefore, we do not display line numbers for these frames. - /// - /// Currently, this is only used by columns. - rootable: bool, - /// Whether the frame is movable; that is, kept together with its - /// footnotes. - /// - /// This is true for frames created by paragraphs and - /// [`BlockElem::single_layouter`] elements. - movable: bool, - }, - /// An absolutely placed frame. - Placed(&'b PlacedChild<'a>, Frame, Smart>), - /// A footnote frame (can also be the separator). - Footnote(Frame), +/// The result type for flow layout. +/// +/// The `Err(_)` variant incorporate control flow events for finishing and +/// relayouting regions. +type FlowResult = Result; + +/// A control flow event during flow layout. +enum Stop { + /// Indicates that the current subregion should be finished. Can be caused + /// by a lack of space (`false`) or an explicit column break (`true`). + Finish(bool), + /// Indicates that the given scope should be relayouted. + Relayout(PlacementScope), + /// A fatal error. + Error(EcoVec), } -impl FlowItem<'_, '_> { - /// Whether this item is out-of-flow. - /// - /// Out-of-flow items are guaranteed to have a [zero size][Size::zero()]. - fn is_out_of_flow(&self) -> bool { - match self { - Self::Placed(placed, ..) => !placed.float, - Self::Frame { frame, .. } => { - frame.size().is_zero() - && frame.items().all(|(_, item)| { - matches!(item, FrameItem::Link(_, _) | FrameItem::Tag(_)) - }) - } - _ => false, - } - } -} - -impl<'a, 'b, 'x, 'y> FlowLayouter<'a, 'b, 'x, 'y> { - /// Layout the flow. - fn layout( - mut self, - children: &'b [Child<'a>], - regions: Regions, - ) -> SourceResult { - for child in children { - match child { - Child::Tag(tag) => { - self.pending_tags.push(tag); - } - Child::Rel(amount, weakness) => { - self.handle_rel(*amount, *weakness)?; - } - Child::Fr(fr) => { - self.handle_item(FlowItem::Fractional(*fr))?; - } - Child::Line(line) => { - self.handle_line(line)?; - } - Child::Block(block) => { - self.handle_block(block)?; - } - Child::Placed(placed) => { - self.handle_placed(placed)?; - } - Child::Break(weak) => { - self.handle_colbreak(*weak)?; - } - Child::Flush => { - self.handle_flush()?; - } - } - } - - self.finish(regions) - } - - /// Layout relative spacing, handling weakness. - fn handle_rel(&mut self, amount: Rel, weakness: u8) -> SourceResult<()> { - self.handle_item(FlowItem::Absolute( - // Resolve the spacing relative to the current base height. - amount.relative_to(self.initial.y), - weakness, - )) - } - - /// Layout a paragraph. - fn handle_line(&mut self, line: &LineChild) -> SourceResult<()> { - // If the first line doesn’t fit in this region, then defer any - // previous sticky frame to the next region (if available) - if !self.regions.in_last() - && !self.regions.size.y.fits(line.need) - && self - .regions - .iter() - .nth(1) - .is_some_and(|region| region.y.fits(line.need)) - { - self.finish_region_with_migration()?; - } - - let mut frame = line.frame.clone(); - self.drain_tag(&mut frame); - self.handle_item(FlowItem::Frame { - frame, - align: line.align, - sticky: false, - rootable: false, - movable: true, - }) - } - - /// Layout into multiple regions. - fn handle_block(&mut self, block: &BlockChild) -> SourceResult<()> { - // If the block is "rootable" it may host footnotes. In that case, we - // defer rootness to it temporarily. We disable our own rootness to - // prevent duplicate footnotes. - let is_root = self.root; - if is_root && block.rootable { - self.root = false; - self.regions.root = true; - } - - // Skip directly if region is already full. - if self.regions.is_full() { - self.finish_region(false)?; - } - - // Layout the block itself. - let fragment = block.layout(self.engine, self.regions)?; - - let mut notes = Vec::new(); - for (i, mut frame) in fragment.into_iter().enumerate() { - // Find footnotes in the frame. - if self.root { - self.collect_footnotes(&mut notes, &frame); - } - - if i > 0 { - self.finish_region(false)?; - } - - self.drain_tag(&mut frame); - self.handle_item(FlowItem::Frame { - frame, - align: block.align, - sticky: block.sticky, - rootable: block.rootable, - movable: false, - })?; - } - - self.try_handle_footnotes(notes)?; - - self.root = is_root; - self.regions.root = false; - - Ok(()) - } - - /// Layout a placed element. - fn handle_placed(&mut self, placed: &'b PlacedChild<'a>) -> SourceResult<()> { - let frame = placed.layout(self.engine, self.regions.base())?; - self.handle_item(FlowItem::Placed(placed, frame, placed.align_y)) - } - - /// Layout a column break. - fn handle_colbreak(&mut self, _weak: bool) -> SourceResult<()> { - // If there is still an available region, skip to it. - // TODO: Turn this into a region abstraction. - if !self.regions.backlog.is_empty() || self.regions.last.is_some() { - self.finish_region(true)?; - } - Ok(()) - } - - /// Lays out all floating elements before continuing with other content. - fn handle_flush(&mut self) -> SourceResult<()> { - for item in std::mem::take(&mut self.pending_floats) { - self.handle_item(item)?; - } - while !self.pending_floats.is_empty() { - self.finish_region(false)?; - } - Ok(()) - } - - /// Layout a finished frame. - fn handle_item(&mut self, mut item: FlowItem<'a, 'b>) -> SourceResult<()> { - match item { - FlowItem::Absolute(v, weakness) => { - if weakness > 0 { - let mut has_frame = false; - for prev in self.items.iter_mut().rev() { - match prev { - FlowItem::Frame { .. } => { - has_frame = true; - break; - } - FlowItem::Absolute(prev_amount, prev_level) - if *prev_level > 0 => - { - if *prev_level >= weakness { - let diff = v - *prev_amount; - if *prev_level > weakness || diff > Abs::zero() { - self.regions.size.y -= diff; - *prev = item; - } - } - return Ok(()); - } - FlowItem::Fractional(_) => return Ok(()), - _ => {} - } - } - if !has_frame { - return Ok(()); - } - } - self.regions.size.y -= v; - } - FlowItem::Fractional(..) => { - self.trim_weak_spacing(); - } - FlowItem::Frame { ref frame, movable, .. } => { - let height = frame.height(); - while !self.regions.size.y.fits(height) && !self.regions.in_last() { - self.finish_region(false)?; - } - - let in_last = self.regions.in_last(); - self.regions.size.y -= height; - if self.root && movable { - let mut notes = Vec::new(); - self.collect_footnotes(&mut notes, frame); - self.items.push(item); - - // When we are already in_last, we can directly force the - // footnotes. - if !self.handle_footnotes(&mut notes, true, in_last)? { - let item = self.items.pop(); - self.finish_region(false)?; - self.items.extend(item); - self.regions.size.y -= height; - self.handle_footnotes(&mut notes, true, true)?; - } - return Ok(()); - } - } - FlowItem::Placed(placed, ..) if !placed.float => {} - FlowItem::Placed(placed, ref mut frame, ref mut align_y) => { - // If there is a queued float in front or if the float doesn't - // fit, queue it for the next region. - if !self.pending_floats.is_empty() - || (!self.regions.size.y.fits(frame.height() + placed.clearance) - && !self.regions.in_last()) - { - self.pending_floats.push(item); - return Ok(()); - } - - // Select the closer placement, top or bottom. - if align_y.is_auto() { - // When the figure's vertical midpoint would be above the - // middle of the page if it were layouted in-flow, we use - // top alignment. Otherwise, we use bottom alignment. - let used = self.regions.full - self.regions.size.y; - let half = (frame.height() + placed.clearance) / 2.0; - let ratio = (used + half) / self.regions.full; - let better_align = if ratio <= 0.5 { - FixedAlignment::Start - } else { - FixedAlignment::End - }; - *align_y = Smart::Custom(Some(better_align)); - } - - // Add some clearance so that the float doesn't touch the main - // content. - frame.size_mut().y += placed.clearance; - if *align_y == Smart::Custom(Some(FixedAlignment::End)) { - frame.translate(Point::with_y(placed.clearance)); - } - - self.regions.size.y -= frame.height(); - - // Find footnotes in the frame. - if self.root { - let mut notes = vec![]; - self.collect_footnotes(&mut notes, frame); - self.try_handle_footnotes(notes)?; - } - } - FlowItem::Footnote(_) => {} - } - - self.items.push(item); - Ok(()) - } - - /// Trim trailing weak spacing from the items. - fn trim_weak_spacing(&mut self) { - for (i, item) in self.items.iter().enumerate().rev() { - match item { - FlowItem::Absolute(amount, 1..) => { - self.regions.size.y += *amount; - self.items.remove(i); - return; - } - FlowItem::Frame { .. } => return, - _ => {} - } - } - } - - /// 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(|tag| (Point::zero(), FrameItem::Tag(tag.clone()))), - ); - } - } - - /// Finisht the region, migrating all sticky items to the next one. - /// - /// Returns whether we migrated into a last region. - fn finish_region_with_migration(&mut self) -> SourceResult<()> { - // Find the suffix of sticky items. - let mut sticky = self.items.len(); - for (i, item) in self.items.iter().enumerate().rev() { - match *item { - FlowItem::Absolute(_, _) => {} - FlowItem::Frame { sticky: true, .. } => sticky = i, - _ => break, - } - } - - let carry: Vec<_> = self.items.drain(sticky..).collect(); - self.finish_region(false)?; - - for item in carry { - self.handle_item(item)?; - } - - Ok(()) - } - - /// Finish the frame for one region. - /// - /// Set `force` to `true` to allow creating a frame for out-of-flow elements - /// only (this is used to force the creation of a frame in case the - /// remaining elements are all out-of-flow). - fn finish_region(&mut self, force: bool) -> SourceResult<()> { - self.trim_weak_spacing(); - - // Early return if we don't have any relevant items. - if !force - && !self.items.is_empty() - && self.items.iter().all(FlowItem::is_out_of_flow) - { - // Run line number layout here even though we have no line numbers - // to ensure we reset line numbers at the start of the page if - // requested, which is still necessary if e.g. the first column is - // empty when the others aren't. - let mut output = Frame::soft(self.initial); - self.layout_line_numbers(&mut output, self.initial, vec![])?; - - self.finished.push(output); - self.regions.next(); - self.initial = self.regions.size; - return Ok(()); - } - - // Determine the used size. - let mut fr = Fr::zero(); - let mut used = Size::zero(); - let mut footnote_height = Abs::zero(); - let mut float_top_height = Abs::zero(); - let mut float_bottom_height = Abs::zero(); - let mut first_footnote = true; - for item in &self.items { - match item { - FlowItem::Absolute(v, _) => used.y += *v, - FlowItem::Fractional(v) => fr += *v, - FlowItem::Frame { frame, .. } => { - used.y += frame.height(); - used.x.set_max(frame.width()); - } - FlowItem::Placed(placed, ..) if !placed.float => {} - FlowItem::Placed(_, frame, align_y) => match align_y { - Smart::Custom(Some(FixedAlignment::Start)) => { - float_top_height += frame.height() - } - Smart::Custom(Some(FixedAlignment::End)) => { - float_bottom_height += frame.height() - } - _ => {} - }, - FlowItem::Footnote(frame) => { - footnote_height += frame.height(); - if !first_footnote { - footnote_height += self.footnote_config.gap; - } - first_footnote = false; - used.x.set_max(frame.width()); - } - } - } - used.y += footnote_height + float_top_height + float_bottom_height; - - // Determine the size of the flow in this region depending on whether - // the region expands. Also account for fractional spacing and - // footnotes. - let mut size = self.expand.select(self.initial, used).min(self.initial); - if (fr.get() > 0.0 || self.has_footnotes) && self.initial.y.is_finite() { - size.y = self.initial.y; - } - - if !self.regions.size.x.is_finite() && self.expand.x { - bail!(self.span, "cannot expand into infinite width"); - } - if !self.regions.size.y.is_finite() && self.expand.y { - bail!(self.span, "cannot expand into infinite height"); - } - - let mut output = Frame::soft(size); - let mut ruler = FixedAlignment::Start; - let mut float_top_offset = Abs::zero(); - let mut offset = float_top_height; - let mut float_bottom_offset = Abs::zero(); - let mut footnote_offset = Abs::zero(); - - let mut lines: Vec = vec![]; - - // Place all frames. - for item in self.items.drain(..) { - match item { - FlowItem::Absolute(v, _) => { - offset += v; - } - FlowItem::Fractional(v) => { - let remaining = self.initial.y - used.y; - let length = v.share(fr, remaining); - offset += length; - } - FlowItem::Frame { frame, align, rootable, .. } => { - ruler = ruler.max(align.y); - let x = align.x.position(size.x - frame.width()); - let y = offset + ruler.position(size.y - used.y); - let pos = Point::new(x, y); - offset += frame.height(); - - // Do not display line numbers for frames coming from - // rootable blocks as they will display their own line - // numbers when laid out as a root flow themselves. - if self.root && !rootable { - collect_par_lines(&mut lines, &frame, pos, Abs::zero()); - } - - output.push_frame(pos, frame); - } - FlowItem::Placed(placed, frame, align_y) => { - let x = placed.align_x.position(size.x - frame.width()); - let y = if placed.float { - match align_y { - Smart::Custom(Some(FixedAlignment::Start)) => { - let y = float_top_offset; - float_top_offset += frame.height(); - y - } - Smart::Custom(Some(FixedAlignment::End)) => { - let y = size.y - footnote_height - float_bottom_height - + float_bottom_offset; - float_bottom_offset += frame.height(); - y - } - _ => unreachable!("float must be y aligned"), - } - } else { - match align_y { - Smart::Custom(Some(align)) => { - align.position(size.y - frame.height()) - } - _ => offset + ruler.position(size.y - used.y), - } - }; - - let pos = Point::new(x, y) - + placed.delta.zip_map(size, Rel::relative_to).to_point(); - - if self.root { - collect_par_lines(&mut lines, &frame, pos, Abs::zero()); - } - - output.push_frame(pos, frame); - } - FlowItem::Footnote(frame) => { - let y = size.y - footnote_height + footnote_offset; - footnote_offset += frame.height() + self.footnote_config.gap; - output.push_frame(Point::with_y(y), frame); - } - } - } - - // Sort, deduplicate and layout line numbers. - // - // We do this after placing all frames since they might not necessarily - // be ordered by height (e.g. you can have a `place(bottom)` followed - // by a paragraph, but the paragraph appears at the top), so we buffer - // all line numbers to later sort and deduplicate them based on how - // close they are to each other in `layout_line_numbers`. - self.layout_line_numbers(&mut output, size, lines)?; - - if force && !self.pending_tags.is_empty() { - let pos = Point::with_y(offset); - output.push_multiple( - self.pending_tags - .drain(..) - .map(|tag| (pos, FrameItem::Tag(tag.clone()))), - ); - } - - // Advance to the next region. - self.finished.push(output); - self.regions.next(); - self.initial = self.regions.size; - self.has_footnotes = false; - - // Try to place floats into the next region. - for item in std::mem::take(&mut self.pending_floats) { - self.handle_item(item)?; - } - - Ok(()) - } - - /// Finish layouting and return the resulting fragment. - fn finish(mut self, regions: Regions) -> SourceResult { - if self.expand.y { - while !self.regions.backlog.is_empty() { - self.finish_region(true)?; - } - } - - self.finish_region(true)?; - while !self.items.is_empty() { - self.finish_region(true)?; - } - - if self.columns == 1 { - return Ok(Fragment::frames(self.finished)); - } - - // Stitch together the column for each region. - let dir = TextElem::dir_in(self.shared); - let total = (self.finished.len() as f32 / self.columns as f32).ceil() as usize; - - let mut collected = vec![]; - let mut iter = self.finished.into_iter(); - for region in regions.iter().take(total) { - // The height should be the parent height if we should expand. - // Otherwise its the maximum column height for the frame. In that - // case, the frame is first created with zero height and then - // resized. - let height = if regions.expand.y { region.y } else { Abs::zero() }; - let mut output = Frame::hard(Size::new(regions.size.x, height)); - let mut cursor = Abs::zero(); - - for _ in 0..self.columns { - let Some(frame) = iter.next() else { break }; - if !regions.expand.y { - output.size_mut().y.set_max(frame.height()); - } - - let width = frame.width(); - let x = if dir == Dir::LTR { - cursor - } else { - regions.size.x - cursor - width - }; - - output.push_frame(Point::with_x(x), frame); - cursor += width + self.column_gutter; - } - - collected.push(output); - } - - Ok(Fragment::frames(collected)) - } - - /// Tries to process all footnotes in the frame, placing them - /// in the next region if they could not be placed in the current - /// one. - fn try_handle_footnotes( - &mut self, - mut notes: Vec>, - ) -> SourceResult<()> { - // When we are already in_last, we can directly force the - // footnotes. - if self.root - && !self.handle_footnotes(&mut notes, false, self.regions.in_last())? - { - self.finish_region(false)?; - self.handle_footnotes(&mut notes, false, true)?; - } - Ok(()) - } - - /// Processes all footnotes in the frame. - /// - /// Returns true if the footnote entries fit in the allotted - /// regions. - fn handle_footnotes( - &mut self, - notes: &mut Vec>, - movable: bool, - force: bool, - ) -> SourceResult { - let prev_notes_len = notes.len(); - let prev_items_len = self.items.len(); - let prev_size = self.regions.size; - let prev_has_footnotes = self.has_footnotes; - - // Process footnotes one at a time. - let mut k = 0; - while k < notes.len() { - if notes[k].is_ref() { - k += 1; - continue; - } - - if !self.has_footnotes { - self.layout_footnote_separator()?; - } - - self.regions.size.y -= self.footnote_config.gap; - let frames = layout_fragment( - self.engine, - &FootnoteEntry::new(notes[k].clone()).pack(), - Locator::synthesize(notes[k].location().unwrap()), - self.shared, - self.regions.with_root(false), - )? - .into_frames(); - - // If the entries didn't fit, abort (to keep footnote and entry - // together). - if !force - && (k == 0 || movable) - && frames.first().is_some_and(Frame::is_empty) - { - // Undo everything. - notes.truncate(prev_notes_len); - self.items.truncate(prev_items_len); - self.regions.size = prev_size; - self.has_footnotes = prev_has_footnotes; - return Ok(false); - } - - let prev = notes.len(); - for (i, frame) in frames.into_iter().enumerate() { - self.collect_footnotes(notes, &frame); - if i > 0 { - self.finish_region(false)?; - self.layout_footnote_separator()?; - self.regions.size.y -= self.footnote_config.gap; - } - self.regions.size.y -= frame.height(); - self.items.push(FlowItem::Footnote(frame)); - } - - k += 1; - - // Process the nested notes before dealing with further top-level - // notes. - let nested = notes.len() - prev; - if nested > 0 { - notes[k..].rotate_right(nested); - } - } - - Ok(true) - } - - /// Layout and save the footnote separator, typically a line. - fn layout_footnote_separator(&mut self) -> SourceResult<()> { - let expand = Axes::new(self.regions.expand.x, false); - let pod = Region::new(self.regions.base(), expand); - let separator = &self.footnote_config.separator; - - // FIXME: Shouldn't use `root()` here. - let mut frame = - layout_frame(self.engine, separator, Locator::root(), self.shared, pod)?; - frame.size_mut().y += self.footnote_config.clearance; - frame.translate(Point::with_y(self.footnote_config.clearance)); - - self.has_footnotes = true; - self.regions.size.y -= frame.height(); - self.items.push(FlowItem::Footnote(frame)); - - Ok(()) - } - - /// Layout the given collected lines' line numbers to an output frame. - /// - /// The numbers are placed either on the left margin (left border of the - /// frame) or on the right margin (right border). Before they are placed, - /// a line number counter reset is inserted if we're in the first column of - /// the page being currently laid out and the user requested for line - /// numbers to be reset at the start of every page. - fn layout_line_numbers( - &mut self, - output: &mut Frame, - size: Size, - mut lines: Vec, - ) -> SourceResult<()> { - // Reset page-scoped line numbers if currently at the first column. - if self.root - && (self.columns == 1 || self.finished.len() % self.columns == 0) - && ParLine::numbering_scope_in(self.shared) == ParLineNumberingScope::Page - { - let reset = - CounterState::init(&CounterKey::Selector(ParLineMarker::elem().select())); - let counter = Counter::of(ParLineMarker::elem()); - let update = counter.update(Span::detached(), CounterUpdate::Set(reset)); - let locator = self.locator.next(&update); - let pod = Region::new(Axes::splat(Abs::zero()), Axes::splat(false)); - let reset_frame = - layout_frame(self.engine, &update, locator, self.shared, pod)?; - output.push_frame(Point::zero(), reset_frame); - } - - if lines.is_empty() { - // We always stop here if this is not the root flow. - return Ok(()); - } - - // Assume the line numbers aren't sorted by height. - // They must be sorted so we can deduplicate line numbers below based - // on vertical proximity. - lines.sort_by_key(|line| line.y); - - // Buffer line number frames so we can align them horizontally later - // before placing, based on the width of the largest line number. - let mut line_numbers = vec![]; - // Used for horizontal alignment. - let mut max_number_width = Abs::zero(); - let mut prev_bottom = None; - for line in lines { - if prev_bottom.is_some_and(|prev_bottom| line.y < prev_bottom) { - // Lines are too close together. Display as the same line - // number. - continue; - } - - let current_column = self.finished.len() % self.columns; - let number_margin = if self.columns >= 2 && current_column + 1 == self.columns - { - // The last column will always place line numbers at the end - // margin. This should become configurable in the future. - OuterHAlignment::End.resolve(self.shared) - } else { - line.marker.number_margin().resolve(self.shared) - }; - - let number_align = line - .marker - .number_align() - .map(|align| align.resolve(self.shared)) - .unwrap_or_else(|| number_margin.inv()); - - let number_clearance = line.marker.number_clearance().resolve(self.shared); - let number = self.layout_line_number(line.marker)?; - let number_x = match number_margin { - FixedAlignment::Start => -number_clearance, - FixedAlignment::End => size.x + number_clearance, - - // Shouldn't be specifiable by the user due to - // 'OuterHAlignment'. - FixedAlignment::Center => unreachable!(), - }; - let number_pos = Point::new(number_x, line.y); - - // Note that this line.y is larger than the previous due to - // sorting. Therefore, the check at the top of the loop ensures no - // line numbers will reasonably intersect with each other. - // - // We enforce a minimum spacing of 1pt between consecutive line - // numbers in case a zero-height frame is used. - prev_bottom = Some(line.y + number.height().max(Abs::pt(1.0))); - - // Collect line numbers and compute the max width so we can align - // them later. - max_number_width.set_max(number.width()); - line_numbers.push((number_pos, number, number_align, number_margin)); - } - - for (mut pos, number, align, margin) in line_numbers { - if matches!(margin, FixedAlignment::Start) { - // Move the line number backwards the more aligned to the left - // it is, instead of moving to the right when it's right - // aligned. We do it this way, without fully overriding the - // 'x' coordinate, to preserve the original clearance between - // the line numbers and the text. - pos.x -= - max_number_width - align.position(max_number_width - number.width()); - } else { - // Move the line number forwards when aligned to the right. - // Leave as is when aligned to the left. - pos.x += align.position(max_number_width - number.width()); - } - - output.push_frame(pos, number); - } - - Ok(()) - } - - /// Layout the line number associated with the given line marker. - /// - /// Produces a counter update and counter display with counter key - /// `ParLineMarker`. We use `ParLineMarker` as it is an element which is - /// not exposed to the user, as we don't want to expose the line number - /// counter at the moment, given that its semantics are inconsistent with - /// that of normal counters (the counter is updated based on height and not - /// on frame order / layer). When we find a solution to this, we should - /// switch to a counter on `ParLine` instead, thus exposing the counter as - /// `counter(par.line)` to the user. - fn layout_line_number( - &mut self, - marker: Packed, - ) -> SourceResult { - let counter = Counter::of(ParLineMarker::elem()); - let counter_update = counter - .clone() - .update(Span::detached(), CounterUpdate::Step(NonZeroUsize::ONE)); - let counter_display = CounterDisplayElem::new( - counter, - Smart::Custom(marker.numbering().clone()), - false, - ); - let number = SequenceElem::new(vec![counter_update, counter_display.pack()]); - let locator = self.locator.next(&number); - - let pod = Region::new(Axes::splat(Abs::inf()), Axes::splat(false)); - let mut frame = - layout_frame(self.engine, &number.pack(), locator, self.shared, pod)?; - - // Ensure the baseline of the line number aligns with the line's own - // baseline. - frame.translate(Point::with_y(-frame.baseline())); - - Ok(frame) - } - - /// Collect all footnotes in a frame. - fn collect_footnotes( - &mut self, - notes: &mut Vec>, - frame: &Frame, - ) { - for (_, item) in frame.items() { - match item { - FrameItem::Group(group) => self.collect_footnotes(notes, &group.frame), - FrameItem::Tag(tag) => { - let Some(footnote) = tag.elem().to_packed::() else { - continue; - }; - if self.visited_footnotes.insert(tag.location()) { - notes.push(footnote.clone()); - } - } - _ => {} - } - } - } -} - -/// Collect all numbered paragraph lines in the frame. -/// The 'prev_y' parameter starts at 0 on the first call to 'collect_par_lines'. -/// On each subframe we encounter, we add that subframe's position to 'prev_y', -/// until we reach a line's tag, at which point we add the tag's position -/// and finish. That gives us the relative height of the line from the start of -/// the initial frame. -fn collect_par_lines( - lines: &mut Vec, - frame: &Frame, - frame_pos: Point, - prev_y: Abs, -) { - for (pos, item) in frame.items() { - match item { - FrameItem::Group(group) => { - collect_par_lines(lines, &group.frame, frame_pos, prev_y + pos.y) - } - - // Unlike footnotes, we don't need to guard against duplicate tags - // here, since we already deduplicate line markers based on their - // height later on, in `finish_region`. - FrameItem::Tag(tag) => { - let Some(marker) = tag.elem().to_packed::() else { - continue; - }; - - // 1. 'prev_y' is the accumulated relative height from the top - // of the frame we're searching so far; - // 2. 'prev_y + pos.y' gives us the final relative height of - // the line we just found from the top of the initial frame; - // 3. 'frame_pos.y' is the height of the initial frame relative - // to the root flow (and thus its absolute 'y'); - // 4. Therefore, 'y' will be the line's absolute 'y' in the - // page based on its marker's position, and thus the 'y' we - // should use for line numbers. In particular, this represents - // the 'y' at the line's general baseline, due to the marker - // placement logic within the 'line::commit()' function in the - // 'inline' module. We only account for the line number's own - // baseline later, upon layout. - let y = frame_pos.y + prev_y + pos.y; - - lines.push(CollectedParLine { y, marker: marker.clone() }); - } - _ => {} - } +impl From> for Stop { + fn from(error: EcoVec) -> Self { + Stop::Error(error) } } diff --git a/crates/typst/src/layout/frame.rs b/crates/typst/src/layout/frame.rs index 60b690dd3..2f68e9361 100644 --- a/crates/typst/src/layout/frame.rs +++ b/crates/typst/src/layout/frame.rs @@ -320,6 +320,12 @@ impl Frame { /// 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( diff --git a/crates/typst/src/layout/inline/line.rs b/crates/typst/src/layout/inline/line.rs index 1ac56e521..7115630ef 100644 --- a/crates/typst/src/layout/inline/line.rs +++ b/crates/typst/src/layout/inline/line.rs @@ -511,28 +511,25 @@ pub fn commit( let region = Size::new(amount, full); let mut frame = elem.layout(engine, loc.relayout(), *styles, region)?; - frame.post_process(*styles); frame.translate(Point::with_y(TextElem::baseline_in(*styles))); - push(&mut offset, frame); + push(&mut offset, frame.post_processed(*styles)); } else { offset += amount; } } Item::Text(shaped) => { - let mut frame = shaped.build( + let frame = shaped.build( engine, &p.spans, justification_ratio, extra_justification, ); - frame.post_process(shaped.styles); - push(&mut offset, frame); + push(&mut offset, frame.post_processed(shaped.styles)); } Item::Frame(frame, styles) => { let mut frame = frame.clone(); - frame.post_process(*styles); frame.translate(Point::with_y(TextElem::baseline_in(*styles))); - push(&mut offset, frame); + push(&mut offset, frame.post_processed(*styles)); } Item::Tag(tag) => { let mut frame = Frame::soft(Size::zero()); diff --git a/crates/typst/src/layout/pages/mod.rs b/crates/typst/src/layout/pages/mod.rs index 860c43f94..574703d73 100644 --- a/crates/typst/src/layout/pages/mod.rs +++ b/crates/typst/src/layout/pages/mod.rs @@ -1,3 +1,5 @@ +//! Layout of content into a [`Document`]. + mod collect; mod finalize; mod run; diff --git a/crates/typst/src/layout/pages/run.rs b/crates/typst/src/layout/pages/run.rs index 091dba221..b5c2834fc 100644 --- a/crates/typst/src/layout/pages/run.rs +++ b/crates/typst/src/layout/pages/run.rs @@ -13,7 +13,6 @@ use crate::layout::{ }; use crate::model::Numbering; use crate::realize::Pair; -use crate::syntax::Span; use crate::text::TextElem; use crate::utils::Numeric; use crate::visualize::Paint; @@ -117,11 +116,6 @@ fn layout_page_run_impl( .resolve(styles) .relative_to(size); - // Realize columns. - let area = size - margin.sum_by_axis(); - let mut regions = Regions::repeat(area, area.map(Abs::is_finite)); - regions.root = true; - let fill = PageElem::fill_in(styles); let foreground = PageElem::foreground_in(styles); let background = PageElem::background_in(styles); @@ -167,17 +161,16 @@ fn layout_page_run_impl( }; // Layout the children. - let bump = bumpalo::Bump::new(); + let area = size - margin.sum_by_axis(); let fragment = layout_flow( &mut engine, - &bump, children, &mut locator, styles, - regions, + Regions::repeat(area, area.map(Abs::is_finite)), PageElem::columns_in(styles), ColumnsElem::gutter_in(styles), - Span::detached(), + true, )?; // Layouts a single marginal. diff --git a/crates/typst/src/layout/place.rs b/crates/typst/src/layout/place.rs index 5c508872b..dfca40f46 100644 --- a/crates/typst/src/layout/place.rs +++ b/crates/typst/src/layout/place.rs @@ -1,4 +1,4 @@ -use crate::foundations::{elem, scope, Content, Smart}; +use crate::foundations::{elem, scope, Cast, Content, Smart}; use crate::layout::{Alignment, Em, Length, Rel}; /// Places content at an absolute position. @@ -35,6 +35,19 @@ pub struct PlaceElem { #[default(Smart::Custom(Alignment::START))] pub alignment: Smart, + /// Relative to which containing scope something is placed. + /// + /// Page-scoped floating placement is primarily used with figures and, for + /// this reason, the figure function has a mirrored [`scope` + /// parameter]($figure.scope). Nonetheless, it can also be more generally + /// useful to break out of the columns. A typical example would be to + /// [create a single-column title section]($guides/page-setup/#columns) in a + /// two-column document. + /// + /// Note that page-scoped placement is currently only supported if `float` + /// is `{true}`. This may change in the future. + pub scope: PlacementScope, + /// Whether the placed element has floating layout. /// /// Floating elements are positioned at the top or bottom of the page, @@ -61,6 +74,8 @@ pub struct PlaceElem { pub float: bool, /// The amount of clearance the placed element has in a floating layout. + /// + /// Has no effect if `float` is `{false}`. #[default(Em::new(1.5).into())] #[resolve] pub clearance: Length, @@ -98,6 +113,16 @@ impl PlaceElem { type FlushElem; } +/// Relative to which containing scope something shall be placed. +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum PlacementScope { + /// Place into the current column. + #[default] + Column, + /// Place relative to the page, letting the content span over all columns. + Page, +} + /// Asks the layout algorithm to place pending floating elements before /// continuing with the content. /// diff --git a/crates/typst/src/layout/regions.rs b/crates/typst/src/layout/regions.rs index 7ff2e1c42..68ad4b7a9 100644 --- a/crates/typst/src/layout/regions.rs +++ b/crates/typst/src/layout/regions.rs @@ -27,7 +27,6 @@ impl From for Regions<'_> { full: region.size.y, backlog: &[], last: None, - root: false, } } } @@ -53,11 +52,6 @@ pub struct Regions<'a> { /// The height of the final region that is repeated once the backlog is /// drained. The width is the same for all regions. pub last: Option, - /// Whether these are the root regions or direct descendants. - /// - /// True for the padded page regions and columns directly in the page, - /// false otherwise. - pub root: bool, } impl Regions<'_> { @@ -69,7 +63,6 @@ impl Regions<'_> { backlog: &[], last: Some(size.y), expand, - root: false, } } @@ -98,7 +91,6 @@ impl Regions<'_> { backlog, last: self.last.map(|y| f(Size::new(x, y)).y), expand: self.expand, - root: self.root, } } @@ -114,11 +106,6 @@ impl Regions<'_> { self.backlog.is_empty() && self.last.map_or(true, |height| self.size.y == height) } - /// The same regions, but with different `root` configuration. - pub fn with_root(self, root: bool) -> Self { - Self { root, ..self } - } - /// Advance to the next region if there is any. pub fn next(&mut self) { if let Some(height) = self diff --git a/crates/typst/src/math/fragment.rs b/crates/typst/src/math/fragment.rs index 17e988785..a3fcc9c64 100644 --- a/crates/typst/src/math/fragment.rs +++ b/crates/typst/src/math/fragment.rs @@ -467,12 +467,11 @@ pub struct FrameFragment { } impl FrameFragment { - pub fn new(ctx: &MathContext, styles: StyleChain, mut frame: Frame) -> Self { + pub fn new(ctx: &MathContext, styles: StyleChain, frame: Frame) -> Self { let base_ascent = frame.ascent(); let accent_attach = frame.width() / 2.0; - frame.post_process(styles); Self { - frame, + frame: frame.post_processed(styles), font_size: scaled_font_size(ctx, styles), class: EquationElem::class_in(styles).unwrap_or(MathClass::Normal), math_size: EquationElem::size_in(styles), diff --git a/crates/typst/src/model/figure.rs b/crates/typst/src/model/figure.rs index 618a6b4e4..08606c265 100644 --- a/crates/typst/src/model/figure.rs +++ b/crates/typst/src/model/figure.rs @@ -15,7 +15,7 @@ use crate::introspection::{ }; use crate::layout::{ AlignElem, Alignment, BlockBody, BlockElem, Em, HAlignment, Length, OuterVAlignment, - PlaceElem, VAlignment, VElem, + PlaceElem, PlacementScope, VAlignment, VElem, }; use crate::model::{Numbering, NumberingPattern, Outlinable, Refable, Supplement}; use crate::text::{Lang, Region, TextElem}; @@ -133,6 +133,14 @@ pub struct FigureElem { /// ``` pub placement: Option>, + /// Relative to which containing scope something is placed. + /// + /// Set this to `{"page"}` to create a full-width figure in a two-column + /// document. + /// + /// Has no effect if `placement` is `{none}`. + pub scope: PlacementScope, + /// The figure's caption. pub caption: Option>, @@ -325,8 +333,9 @@ impl Show for Packed { // Wrap in a float. if let Some(align) = self.placement(styles) { realized = PlaceElem::new(realized) - .with_float(true) .with_alignment(align.map(|align| HAlignment::Center + align)) + .with_scope(self.scope(styles)) + .with_float(true) .pack() .spanned(self.span()); } diff --git a/crates/typst/src/model/footnote.rs b/crates/typst/src/model/footnote.rs index 2aeaad1a7..813990a99 100644 --- a/crates/typst/src/model/footnote.rs +++ b/crates/typst/src/model/footnote.rs @@ -125,6 +125,9 @@ impl Packed { let footnote = element .to_packed::() .ok_or("referenced element should be a footnote")?; + if self.location() == footnote.location() { + bail!("footnote cannot reference itself"); + } footnote.declaration_location(engine) } _ => Ok(self.location().unwrap()), diff --git a/crates/typst/src/model/par.rs b/crates/typst/src/model/par.rs index 326d151eb..2e23bd74c 100644 --- a/crates/typst/src/model/par.rs +++ b/crates/typst/src/model/par.rs @@ -297,8 +297,8 @@ pub struct ParLine { /// Second line again /// ``` #[ghost] - #[default(ParLineNumberingScope::Document)] - pub numbering_scope: ParLineNumberingScope, + #[default(LineNumberingScope::Document)] + pub numbering_scope: LineNumberingScope, } impl Construct for ParLine { @@ -310,7 +310,7 @@ impl Construct for ParLine { /// Possible line numbering scope options, indicating how often the line number /// counter should be reset. #[derive(Debug, Cast, Clone, Copy, PartialEq, Eq, Hash)] -pub enum ParLineNumberingScope { +pub enum LineNumberingScope { /// Indicates the line number counter spans the whole document, that is, /// is never automatically reset. Document, diff --git a/crates/typst/src/visualize/image/mod.rs b/crates/typst/src/visualize/image/mod.rs index b11cf5871..d267ca044 100644 --- a/crates/typst/src/visualize/image/mod.rs +++ b/crates/typst/src/visualize/image/mod.rs @@ -22,7 +22,7 @@ use crate::foundations::{ use crate::introspection::Locator; use crate::layout::{ Abs, Axes, BlockElem, FixedAlignment, Frame, FrameItem, Length, Point, Region, Rel, - Size, + Size, Sizing, }; use crate::loading::Readable; use crate::model::Figurable; @@ -79,7 +79,7 @@ pub struct ImageElem { pub width: Smart>, /// The height of the image. - pub height: Smart>, + pub height: Sizing, /// A text describing the image. pub alt: Option, @@ -127,7 +127,7 @@ impl ImageElem { width: Option>>, /// The height of the image. #[named] - height: Option>>, + height: Option, /// A text describing the image. #[named] alt: Option>, diff --git a/crates/typst/src/visualize/shape.rs b/crates/typst/src/visualize/shape.rs index b2125bf5b..633872ccf 100644 --- a/crates/typst/src/visualize/shape.rs +++ b/crates/typst/src/visualize/shape.rs @@ -8,7 +8,7 @@ use crate::foundations::{ use crate::introspection::Locator; use crate::layout::{ layout_frame, Abs, Axes, BlockElem, Corner, Corners, Frame, FrameItem, Length, Point, - Ratio, Region, Rel, Sides, Size, + Ratio, Region, Rel, Sides, Size, Sizing, }; use crate::syntax::Span; use crate::utils::Get; @@ -33,7 +33,7 @@ pub struct RectElem { pub width: Smart>, /// The rectangle's height, relative to its parent container. - pub height: Smart>, + pub height: Sizing, /// How to fill the rectangle. /// @@ -202,9 +202,9 @@ pub struct SquareElem { /// height. #[parse(match size { None => args.named("height")?, - size => size, + size => size.map(Into::into), })] - pub height: Smart>, + pub height: Sizing, /// How to fill the square. See the [rectangle's documentation]($rect.fill) /// for more details. @@ -293,7 +293,7 @@ pub struct EllipseElem { pub width: Smart>, /// The ellipse's height, relative to its parent container. - pub height: Smart>, + pub height: Sizing, /// How to fill the ellipse. See the [rectangle's documentation]($rect.fill) /// for more details. @@ -399,9 +399,9 @@ pub struct CircleElem { /// height. #[parse(match size { None => args.named("height")?, - size => size, + size => size.map(Into::into), })] - pub height: Smart>, + pub height: Sizing, /// How to fill the circle. See the [rectangle's documentation]($rect.fill) /// for more details. diff --git a/docs/guides/page-setup.md b/docs/guides/page-setup.md index 14c35e1ba..ac475583d 100644 --- a/docs/guides/page-setup.md +++ b/docs/guides/page-setup.md @@ -390,59 +390,52 @@ Add columns to your document to fit more on a page while maintaining legible line lengths. Columns are vertical blocks of text which are separated by some whitespace. This space is called the gutter. -If all of your content needs to be laid out in columns, you can just specify the -desired number of columns in the [`{page}`]($page.columns) set rule: +To lay out your content in columns, just specify the desired number of columns +in a [`{page}`]($page.columns) set rule. To adjust the amount of space between +the columns, add a set rule on the [`columns` function]($columns), specifying +the `gutter` parameter. ```example >>> #set page(height: 120pt) #set page(columns: 2) +#set columns(gutter: 12pt) + #lorem(30) ``` -If you need to adjust the gutter between the columns, refer to the method used -in the next section. - -### Use columns anywhere in your document { #columns-anywhere } Very commonly, scientific papers have a single-column title and abstract, while -the main body is set in two-columns. To achieve this effect, Typst includes a -standalone [`{columns}` function]($columns) that can be used to insert columns -anywhere on a page. - -Conceptually, the `columns` function must wrap the content of the columns: +the main body is set in two-columns. To achieve this effect, Typst's [`place` +function]($place) can temporarily escape the two-column layout by specifying +`{float: true}` and `{scope: "page"}`: ```example:single >>> #set page(height: 180pt) -= Impacts of Odobenidae - +#set page(columns: 2) #set par(justify: true) ->>> #h(11pt) -#columns(2)[ - == About seals in the wild - #lorem(80) -] -``` -However, we can use the ["everything show rule"]($styling/#show-rules) to reduce -nesting and write more legible Typst markup: - -```example:single ->>> #set page(height: 180pt) -= Impacts of Odobenidae - -#set par(justify: true) ->>> #h(11pt) -#show: columns.with(2) +#place( + top + center, + float: true, + scope: "page", + text(1.4em, weight: "bold")[ + Impacts of Odobenidae + ], +) == About seals in the wild #lorem(80) ``` -The show rule will wrap everything that comes after it in its function. The -[`with` method]($function.with) allows us to pass arguments, in this case, the -column count, to a function without calling it. +_Floating placement_ refers to elements being pushed to the top or bottom of the +column or page, with the remaining content flowing in between. It is also +frequently used for [figures]($figure.placement). -Another use of the `columns` function is to create columns inside of a container -like a rectangle or to customize gutter size: +### Use columns anywhere in your document { #columns-anywhere } +To create columns within a nested layout, e.g. within a rectangle, you can use +the [`columns` function]($columns) directly. However, it should really only be +used within nested layouts. At the page-level, the page set rule is preferrable +because it has better interactions with things like page-level floats, +footnotes, and line numbers. ```example #rect( diff --git a/tests/ref/block-fr-height-auto-width.png b/tests/ref/block-fr-height-auto-width.png new file mode 100644 index 0000000000000000000000000000000000000000..21cd3f519d88f517d16912f0e5952a80930bdfdc GIT binary patch literal 1069 zcmV+|1k(G7P)FFpaC~j_U z-QC?gJ3GO_!LF{ZR#sMmf`U;|Q8qRAP+1c5HgM;nu?b`O;va+(Fp`nQ3 zl1I{3+4bCq;gUwtR)mCveSLkgv9ZL&#HgsKl9G}d8XENU^n84LpP!$Yn3#s)kV(>5 z>+9>r#>R(-hm(_&hvAV)(OBl@=Frg4tgNh#j*ipQ)6~?|rKP2to14YO#f621CMG5< zEG)3Fux4gvTU%Rxetx>Tx}2PxqN1Xjnwq=2yEHU36B84Wk&&I9ojg1|prD|ZmX?f+ zjGmsJA|fJka&jLZAJ5OvSy@>!GBPkQF#G%aK0ZFz*ViB*AlvrdY;0^HAtCeg^Eo*= zaBy%lGc$2bo%}S#jRT}aJq2J zVxpKrcr{_7Rh2L%5?Ek?*CuQ+kxDq4Fi}+AP67)oaF)RZ7Dc@<36~Tnl=NlMB>XLo zlJYhPEU>`;6}H;#Wsq83F{xDp_V=?&U|ap(2L8^dg-xWk4ZzQz-emx@@|9#Z&}>CT zSOKiV5X1^-8p#a9;Meg1z90|+N})ev`1rA2x6#?Oxocy+3y8iMF{X!#m+9-BSb_5C zTQ?gkM<-&7*j=9oh~FRF&2rn;!tt?ihHz!`>tT9E>2%TpguF0g6fM#JvfBbUZd||S z8`J2bqKqJ(2Se?ezqOPIWC%NVI+y5iv&Bi|HylZxfe^`zmMd4?BM`w~P7v=D*p9UC z%M2c@u)(YD{f$)}A7{OX8YaV_z3mIn2OdubK6K<20$jKl92U_zA^tmVc4o$BiS#=e2ut(1~YVG&#a`{p? zaI!5x-IP4J1Qs|;;gTkcl1zWl=O+~PT>cgX7Fb}}7G+zM`A1-ZWm}YOF)QFRr*jnd n@Db^BHdlEIay?IyB!&G1Ylyyh_|G!v00000NkvXXu0mjfF|I8d literal 0 HcmV?d00001 diff --git a/tests/ref/block-fr-height-first-child.png b/tests/ref/block-fr-height-first-child.png new file mode 100644 index 0000000000000000000000000000000000000000..0a1795769975a858f3018739ea657dd356cfd8ed GIT binary patch literal 149 zcmeAS@N?(olHy`uVBq!ia0vp^6+oQA#0(@2vG%Ne-4IbVKGjbMRg1{@2rt!8mTj*ShS^Xg4 rXojI(lyr|*zgPdXuEk50PBJhAIvf8z{HWwC&^!iDS3j3^P6vG%Ne-4IbVKGjbMR3Yj#`)x}drEK|>A${GpZ z{floVgj9u8Z3&Ul6@AIC;Z<-J!BmO{2_o#cc++yAShb#;!hbzptyfB*zw1L6X L)z4*}Q$iB}y3#f& literal 0 HcmV?d00001 diff --git a/tests/ref/block-sticky-alone.png b/tests/ref/block-sticky-alone.png new file mode 100644 index 0000000000000000000000000000000000000000..74e30b3b1e4ae8cf2b7f14af913848bdab0370e6 GIT binary patch literal 166 zcmeAS@N?(olHy`uVBq!ia0vp^6+mpn0VEhUopE3RQn{Wkjv*Dd-d@?r>);^a{!!X< zap>$;w?*kq+8STnE^=-?#I^iVhnt;uM}1Lceq;RiIh{oZfZ+at$_odwFJ>pyrwRSg zSY8r-pzday8$AJ$7{Pm!CS$$1Jiq?tP~#HD!Q%5_#iy)iLA-n_Svbi&u%jE zw}2G?mk3jT%*$d|xo)orlh}L-L5??}i-H3l@2>A}RK3o4fcv{o?`wu1J1(EA(L2<_ zY0c=?DlM$$bK~ZRU6*u47&pu_aGk`kgTe~DWM4f!8>pF literal 0 HcmV?d00001 diff --git a/tests/ref/block-sticky-many.png b/tests/ref/block-sticky-many.png new file mode 100644 index 0000000000000000000000000000000000000000..cdcf291dd31805bf24f01693e251350d21680416 GIT binary patch literal 570 zcmeAS@N?(olHy`uVBq!ia0vp^6+rxn14u9|R*Bodz`(@l>EaktaqI0Z+w94VB5e=z z`1tgix>qzG?C9>EtWgpW5+K5E?r1K}k|`*-w9Dd!fJ^tp1&V(eHm@j|cUoX5c8tiHl^|SHb0x) zPZ~t8`)p>tceVCQZ|38bwW|DUu5Drbw!Y;n}nB z;k<}lkNvJPXsb=-)0KL#W63l>;XgliaqgSTD<5}H@2{nywA~zUo0l)w2?~HbrE&QE zuTMYM^Hr31XRf&VCCv2ueWv#(=FKwCwb_u-6e?$Y;ON=>-~IlQ4<cIeV zCmR|Y8?SSeAGON6Q*z*z?%wSOs>Q0F>wGwO=jbZdzSVl{G56IXA3X0AXJG+(^3#VO ztEW%q+hH91zxD_N|NQA+5;bdn)Ez5%yp8=*La|1elF{r5}E)cuJqyn literal 0 HcmV?d00001 diff --git a/tests/ref/block-sticky.png b/tests/ref/block-sticky.png new file mode 100644 index 0000000000000000000000000000000000000000..4f236c898dc4b438e4924d386557f112e9411fbf GIT binary patch literal 469 zcmeAS@N?(olHy`uVBq!ia0vp^6+nEF14uBOow@QJ0|Vn6PZ!6Kid%25IBGL9NE|CH zXD?S&H8mF(Uvwv6$&|_d2AUI9TAw`F#B8LSncVB~C`Enol%tKz#(Bx?YFm!3x%%xF z*Qr~^KljH!xPJNfmnkz}oN8!nY~*GxS26j}S{%r{ds42@kJJxKUivZaK9_$dn90I6 zYg*QwdwoCfz|=Hm zM@E?!@tkiA6%HI=ZJZyo`f)as#ogJPYUVS1+i~_yC?x*_xJr&?q79$U%4N0--dZ2Pg8 zfp5BetJQth54BIsgc5gx!opkXTe-fkp108TS-IW`?Xk6<52WJ#y{1^qoP6Nws<7?v gjey?51pj6Jv1J)6f9+aaX9$WfPgg&ebxsLQ003>qIRF3v literal 0 HcmV?d00001 diff --git a/tests/ref/box-width-fr.png b/tests/ref/box-fr-width.png similarity index 100% rename from tests/ref/box-width-fr.png rename to tests/ref/box-fr-width.png diff --git a/tests/ref/colbreak-weak.png b/tests/ref/colbreak-weak.png new file mode 100644 index 0000000000000000000000000000000000000000..e2ce5b96a7dc9db7177b413c43ec65bb3c8813c3 GIT binary patch literal 197 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%0Dpgf zcgF`j0008=Nkl|$;2MJT%aLtH`?t%b!2+%VR0s;Y~%@+#CCkTb(qM_a*U|?!8luKD`AL{Jjbrbo(;PUw4 z*%07Z0cRW}1mfoaB!q-E^$WeYUFOBB0BaxATe@=DlJH5qZXzNgNt3;Tb|!hlUl9C? zVgCV$2;Nr!Ym-EHZ$8{<65&00z-o!GUN09mZZJW)FoOX>nfz45FWC|y;_kr&DSfFHmRkuUra!mm6Lag3E?tQ=$I7%LISSUJYZF;EL~VnN&@~XT-Y68#NMG9x5y_4GyBE@iEj_v&5_aX7 zQXygI4vP~qp7lb44b_8?koEA<08fmx;b?Y1g0r;-A}qDl*^p3Pkr59G<|kPxkYKB- zOyz}rac-VaSg#igk8JFL$bS)y$n4{F`=h5N zZRR8hC@#a+4hVQticOmtRw4bt{qQXi;0EC4aTC$M?=P>#Dm9e>-75e9002ovPDHLk FV1g#r)yMz< literal 0 HcmV?d00001 diff --git a/tests/ref/footnote-break-across-pages-nested.png b/tests/ref/footnote-break-across-pages-nested.png new file mode 100644 index 0000000000000000000000000000000000000000..490618446f1ad3e925ae42352acf8a6569eea9f3 GIT binary patch literal 1320 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 kwXc3*^%|G+b%scPcRvaW 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�f|-vW?Z%B8gfwkoA))wcm!?M^<1;SfWob9 zSCxYNtlPmyKY=jt^7P#7YL8+Cn)Bgly-FMtuByamzPu55@Cm;Um9~H~y<+K;R3eMT zP`zR$zTsM~q*`M;Aw9(&f0I*E{<(~j$%|cSE@1a}0P78pilTC+#}LlTyDl%dQRLJT vM195KGM07$U5}pwy!_T}C!OXhb42%7NxtHMeq;sMgi`2e>T8s%KMMN~6l8us diff --git a/tests/ref/footnote-float-priority.png b/tests/ref/footnote-float-priority.png new file mode 100644 index 0000000000000000000000000000000000000000..267973311fa135a1d8083ebfe4ddc20113ade87a GIT binary patch literal 1393 zcmV-%1&;cOP)rPWZB_$>M`~3I!_d7c~BO@c~>+gkyh1=WP zu&}UgZEc8%h<<*4FfcHpqN1Upp>S|;`T6=;Sy}V*_FY|FXJ=U{k*WaI^sZmo~*Vy2_zsJMh$lKlJgoch}W^SXTtlZt?ij0)GyTe*sWZ&TC_V)L!t*_3{)0&%~XK8VOf{L}Z zwb0Pe`}_OD!^1p0Jm~4|I5;?ukB^3ihIDjvVq#+O@biN?(W#w*nxq8Jv}}7`T3NTl;Ggtm6ers zb#>|K=_x5G!NI}G%F1hNYo?~AARr**a4p$G=Hgn^CSt=Qe&-Q7)F*ntAlu+#>-`&T(~>BSC=hu+P{2c<2z` zHqpOp>Ko?y3<>>U`vM7eM`Z;hynk0#9s~h)pr#fAegNO-hmIUQ1|n8w;JkNe66OI@UnXn}Rh&BD;-})KG zXABV$QFqbe6*`W~EB-O01JleoKoHb3rp`t16p8SPmB3_)u%-dSyIXkak~&@)5z*gz z2;nbJL>%vPywCAI$NNMa?{mD*@jl1%vPywCAI$NNM?L|uiiSLirCUh$719o$$v z4RMbfB=%gXD9R0ma`9yTCjFUCo|1Wud+E$+U}AqL zXH1!jgh>)%O*52OWx_4R*`Xm4VZc1b0ucsYHxY5X&+$GHbp&^N=-G98+Ro|GG3-Ix zxfy1dVTKuIm|=z)W|(1y8D^Mah8gZ8VW+$8oH~Z#X%D`SBqE}&ttlqz=B;K3hyk*4 zEKtu5pgc?{Tu>$yb_YT^#}i<|{CQCBQDJpck{_>|NC!!muUxHwgxf$RB-k;_0twN9 zv*U!mv7Z3~4ADSDB-FcmC$C^6=gmY!MC$zpGOFbhoq?Ch00000NkvXXu0mjf73SqV literal 0 HcmV?d00001 diff --git a/tests/ref/footnote-in-list.png b/tests/ref/footnote-in-list.png new file mode 100644 index 0000000000000000000000000000000000000000..504c357881ceaaf3c6e30f81f524492f718bfd22 GIT binary patch literal 2513 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 literal 0 HcmV?d00001 diff --git a/tests/ref/footnote-in-place.png b/tests/ref/footnote-in-place.png new file mode 100644 index 0000000000000000000000000000000000000000..d41316dbe659e90b6dc90b85a2b5393b434e8af5 GIT binary patch literal 1127 zcmV-t1ep7YP)FMe6^775i%{n?dJUl$r)zx`< zdCSYo$jHdS!NF^5YwPRl@$vC$YHE^_lKA-edwY9^hKBq5{QCO(FfcIf?d|OB>~(c@ zlarH4NlEDF=-k}g)6>)8;o&wmHi3bGLPA2KqN0O?gSfc3uCA^`MowyMbj!`v#mCRL zxxt5rhpDNlqobp%tE-=%pUlk6j*gD<^7ZlY^q!ucm6et7@bfe@G(J8)H8nMsmX_@8 z@!{g;q@<+U+S<3bw<{|vl$4Z$f`U#?PU`FLd3%FoWovoTQkUpL=|QuCTb~=H^^nTv1U`goK1k zOG~Y-t+TVUczAena&rFu{#8{~S65eBT3TLSUi0(wK|w)OR8+;q#a&%pZffg!IZRAUb8~ZTZEa;`W#Hi8;^N}&?(XmJ@BIAy zjEs!e*Vp#;_K}g1rKP2di;F)$KYxFJXJ==|#>TO+v3GZO$;ru=mzSNLo!;KwNJvPC zh=`k;n|^+N&(F`<+1a6?q0-XQ=jZ3+gwtyCMMtC-`(Ba{r&yh+uMqY ziuLvN0002b9VN~H00Iw5L_t(|+U?hMPg`LWfbj?Jr7cikxX%a7$K2iB-N)SB$2R7; z6&sFdiOcTD-sZL--1G@br$)^WH@An#3k+cxMNwp>2qE7-}qP!gQ!>7(bS7(;_VO6Eu_0^qkK{(^lW1c;2 z!d#Qn)2DkwM7-Z+!Y8bHtg-3xm8%^C!7yf1F4pkb^DThc4jjBhpr@nLlFb$H41SDS z5q{A~-3LsTVzc?4H@I44yoC@#h!D+;oLCm_tht{Jr_W%nk*Q60ontcv2%JCBl zI{O)egs& zb5j1y=qmswL3t)@KYGN77yEe_>AQRHK81CxYT&g-LD~G^-*Akgs=9}7^a2kwzuV=c zXdWxed83iD9AY-Z;15H>;p^8b(Arvk{qF-B#S3y~IojHB?%%fpVDFyY40#~MW=p{s t8>;Znzyce~yl-={!fY)n>r{w;%O97cx$SX^)_VW|002ovPDHLkV1kEQYkU9z literal 0 HcmV?d00001 diff --git a/tests/ref/footnote-in-table.png b/tests/ref/footnote-in-table.png index 3f8f50ca1027d75a74d7e7d2040dd9abae710710..7aa2bbf4633d10ef509c08c9a6ecc22c942d04cf 100644 GIT binary patch literal 12423 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 literal 12311 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 Rp#}di3NotFRgxw_{|_jmotppv diff --git a/tests/ref/footnote-multiple-in-one-line.png b/tests/ref/footnote-multiple-in-one-line.png new file mode 100644 index 0000000000000000000000000000000000000000..6f60b609cd8ea9ae72581a5f611fe4695aa23815 GIT binary patch literal 693 zcmV;m0!safP)E z^LcrBdwYA{-rk0YklWka%+A(DM^9>ObVEf=#>mj0pP!+jp}xMpfPjEbPENG6v_L>W zS65e=nx34VroO<)n3J*x1;}$jESTa4;}1etv%V z`1qEVmetkO%F4=XYiq&5!8J8C;Nala*495iKO-X}Sy@@Iu&~tB)MR92cXxNOv9a~_ z_2=m9pP{K;UuTz@p>cD2O;1;|v$Mp+#LLUeYHDgoNJyQXonm5QR8&+^QBg@rNri=l zK0ZF?=j-X|>GbsWZf+7tntdf$Fk&%(=>gtJ!iIkL-*epY8;A_u%gC65QP(5PeDZzW@eQrWwKO zP<6fyoT~1+cXR~+0000000000;Gb)ea!Q)ykDH{_*w2%vX z+Sv9f{27rJ=Yv1tR7#s$K7|t>NzKl68)Kw&V?%XUCSXi}^#TBeB$Jo^PBt$-2EQOM zHYz8~#JHnybD2WZlMcexDl{|YCfpMqa1yTX?dyM)3ty^aya5!fB1d?60000000000 z00000037dgybovIMh4Rra=;7^say^e*Olop7I$+)|DdN!C*g8=7f!-8)wMyn5UgUl b0TirnCBRSqM#TU=00000NkvXXu0mjfuqk2! literal 0 HcmV?d00001 diff --git a/tests/ref/footnote-nested-same-frame.png b/tests/ref/footnote-nested-same-frame.png deleted file mode 100644 index b22276d5cc95d72ca1dcc3a18a968a7f114aa3b5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 743 zcmV?P)_{rmg;`}_O(`TF_!`Sgwv~=;-F>?dIp}=jZ3<=H}()<>KPv;Nalh-{;@o-`?Kd-QC^V+TGaN;@aBU z+1c6H*x1+C*VEPA)z#I|(%RC}($CP=&d}J<(9q7$)z8n*&Cb-!&C$)x&C1Nw%*@Qo z%gxBi&B@8h$jHdX#>T_N&BevV!NI}1y~Ve=!MM1(w6?ylvAMCav97SVtgg1MuCA-C zwWzAHr>U{0r>CT*uBD}=pP;0mprD?grJSCooSmVYo12)Jn3b2GmzS58mX?#1osW=~ zkB^Uzj*g3sm5hvxii(Pch>(GUi++B7dVGX=d3kkre|L9xaB_NYad~iXaBgmHZEbCA zY;0#|XJ27yUS3{ZU0qsQT2)tHR8&+_Qc_DzRZL7wMMqCMIyyNyIXE~tX#&<&0003x zNklYaTnXF%c6?Ozc20QLzJkecymT9N6pV`Mtn5J3G4= zhG8T_eHzeHiL}9wCg=^^TL{xvO<*qt#lxL%bI|{MAwRL!U~B!a!ez})gq2bacX>*= zkXJuyzL{YdKM_ud)X7cAEaoq?py%&@WA8o_@*g4QOdm|ZhGZ6uv* zdKj|ZrAR9oKf`y}8a_anI-TJa7!sd2I92|H1C-X;Uk-(dgsAf%%w<{NUG|v!HVosJ Z`Ub*UGr%U~q1gZc002ovPDHLkV1nb#qD=q* diff --git a/tests/ref/footnote-nested.png b/tests/ref/footnote-nested.png index fecf2e8de7006869ed378001e51e0685f860ec02..50cc2637feed0d776896b1971e41eb6d7597e7b9 100644 GIT binary patch literal 2581 zcmV+w3hMQVP)MB?ll_qvz6#*3yPyqoc3J4OwhAhf}6b00! zE&|fKqJV(9`-?YuL8D}KW;7Ge$z*cgo5vgG_wKvzoO91T_qTsVeu(@g2n4i%77@?_ zT0n~kXaOxEpiLC?<;#}`3>a|s>{(k|TRS_u($Z2ZE30mfudAyQrr+73`}gm!l>7JZ z^WNUx-p#=yMvM@q-wArqph3WmjErn)X}NguA}yNDO`A4dx^!vz^5w0qtv)_J=ocC-1TIC#T`4b9EX1qB5wR;*y>$&)Ad@8AFW^=qI{oH!BO zSy@@^A2Vi*F#S%@g9i`h{oT8FycZW2E6{FkZqP?TP=*g5e)sNO0sZ};;oGA}4{b9y zH+SK}g(!}Wj&u&N&>{j_K#Tlb z&^b9d*n%kdIJlbK9NE~|_UmZnZfD8^Jl!vb>+mt_6=_48cVeNVd?t?`JsLW6 zDCco;^W6Q1ZE9MUNrL7#6XifjN=m}vM$uE;-QBNTxl&oxaNg^(1{z}=UMTCrg4Uf%KG&i%^m+d ztw3wCWy_Yt#6%Qu6HDQXwr$%+=ru1dZ`G<*++J6&UM0+O@ZdqNL~U&?`tjq(M~)oH zc~-32aKhO=HZI;&`{?NC=yBu55#k_zMf65DsD!Ku^c=2McD_4+tpUrCp;; znly>%EBDRJnKRks5)!}d+I6TCnks0TAx|*X70{4@NN=E?^)%a_J$n*6rDxzSo;Pov zij=Ndvxas@i*|ByVv}bY_e6Mj`0CZG)p=n3HPB{eW?U5%-qjrdWjY+yqF=pw#hD?l z4kq?pSy{=y0rA$>x26p`A|e7({{H^JQhY5eEFk#s;X^iQ-L!NfO|-A7swx^UeKhgX z&dyFS5q^TgjvYG=A3n?t&WHhx6l(0RsH}wuulDv1?Yp>;8#iv`O3*3OM{yXBU;b+E z-o2@*scYA+CBj7m=VGXDr|mQTz!fD{o{;dDI$?doE3u=+VG$9~0$M~s|C*q2bDlqc zj-m2fnnX#xF(ybioRm4Afug4d9ckbsVRLW3pXz=(ME zN5Q|qsl$cgCLr?3r*Gc8!Euj?iQ%LC{QSzQI{(11Z5EaS`g7^sySJa8-~9RW@wUN7 z{EVH;mMtUV5g#AFd-rZO4;(mvmxwZC$Pjjt{y`*!a1wg}O`0UZe!>^Daawe0`cooG zC<6KmEt)t3ZZugkL@ZTo24l5 zD$%ZCO5!LnPi0pS*9SU@OC&Tg3NvfgEJmtSLte6E31J`}p=6Pjl$4x5f1VB}H@84V zf5hq#5zqo!L_iB@0WBh+1+<8OHVx1|MiT-3iIf^m1hj~N7SJNb0^Qm9E+HYYsHj-m zL;B&TgWnzb`Gt9T1%g;F(1h*1E(AnG-qZ%X$(=1LE7J}rE~z9fJm5;WAl4VOpa0dM zkVtJ}05dRDp01M2tQ`A8PP_!MzM!LH;&0zgQk&$NGcTOs73~1VP!bYTIY1EW3tA2R zp);?7xg<=mAXl2K1P!!0KoA=YG_!itfGm_5LS%*jJ&AxenAO3o2wp`+MT~#*uBON_ z6iW&Roi$d6IzSK`%Z?_2kuh%}D-Mf*7SJL;4K#U1NxF1bHxFj|9XWD@geGPZGMiQV z)cMp|U2=x-)faSiO~Zcc!_4PK-@0`x35;0NS~bmv#%7#vcMl)oYXCd?f$q_cT~_G3 zcI_f(mr34g(knvlb6`+JaA=h9HKfA==zI6>F~gf=M(tDaRtyiCk|m__qfpI+0G@z0 z3TT8pb7~CIdmKG_RCU<`+UVvTK@SQ@2v)J91+;(``N^OuR>G{!uBFz5rcta82Pe1W zhgs-_g@pv5sR_mi0M@iHHN1Iu;GiQq^})1)(aew)riKJf=^~;Tx|B?+ECE_L8r@-0 z^EHK|(Lt*kYRth`gILU^6{dy+&8RD^c`0v@k_SwZl@40$XaOytMFjM(1iGcA&DSq5 zBs84CS*n9l#Dl^p)D-P%Xn*{)MiA?3-X$eJc0KKdPHhybDPd6C*w|3#M;+jOF_6L8 zwDfF2tQY9I`j=+we)shDS7U~1D2pPyTB#rWMjyYB;E)JGtZ$3X%`eR_Eb9synK5Gq zPB{v`3-}l|&!|!bNyVkSUUi+t7*`H!R34uThv`nA{ zTA*bDEzmN7{)>WkadBzizWs?4C$?&vn11JoZr84zX+%aw z@?BL`^_9*&d-fF5?*!ekV@Kfn`ue_q|K7>Ti4o1^;>C+ET(~fO`t*v53TJ0$+UL)o zfBg7y+qP}FtgNgQ*+zqI)vDFit5+8-TJ-MSyQHL~nKNf{^T2@v8#ZhxD=P!~ph1Jc z9TOA7{XTvAi0OBN?$oIh-yc7IG-6|8pFMk)ot-^l!UQh6cklk;!-r3wK8bAOK)buU+u7N9dwZKp zM@PpsYt}Fqj?%;wk(Zap;AM_RFRiJm;TJ-F z;>TFIa^=pQJJIu4G1#_k8`CuRb8-t{&8MoWTA*bDEzmN77HEO~f| z?~4~N;su-GJYc|px*c$z5j<0CM9m;8j638j z1q1}p=+dRj?c29W{5W>(SYTivclb#FO`QESCLty+NuV1C+Dsi885ziRG`MfD_U_%= zbRIc!WL8#|W$FO5`3Q8w##DfIVf=@F{rVY?V(QeXG?+RNnrU?F)(t_!h)O8YH0YU| zn=8=Y3L2FeO?lqDdE_Fo+JZ`#nwm=b=#(i_n4J-w*RNlX*gtC2D6}W@8%&-&nK^pt z(xscwdBG3(M9qe9Vp^xQ0U)hpX`^}b|oQ!`>13iSK8G-)R%|ZzY2}X$~4kS=SMKY>hNJt2oG^EAQ0hx*2 zS}c%zdU}$TFl5LO7SYSg%gF{Iqm1MmLUV!sLu=Wx<)urP#*Q6J3I}8{Zn8B9{Rqk^ z)F%Cd)B~~%I(P02Tv(G{Lp;eNK$BY&A0JOtxoXuafo@<#v-cQ|Y{`Nzmke0)VD8}>Vh^?2Sc8CU7-KaTDNX(X_K*NfHGH*QiIk(?vjy~$N{6lhC^m^ z8c&}-<&l}AhYlS|gF%g8BG6wpJdLhhyIQutk^03rMcv=Of4@;pVMpQ;=}R~#OvEHV znz;pml&P78F5`6|tMX5P@PtGq&@zD*Xqi9@v_Q)QTA*bD{Z|5g&1NRh|6}nsGl7-~ zv_Q*j1-ho@V{GiNnVDIZJN1fhl$XB`2neKu5H|^QNom>Pqi3)C2ANBfJNo+dYs+P9 zTtax{!+j3Ng}AAp-93D-dH-TA!@|PYl1~Pyr9)bJ&K~T(QG?ROiUD6 z1uOzB(6S!|nh3k%oTKx3r%UW4Bu*hCiK1+lC_FDO-}$1OuU~-pTEV>|!gh4JOq>0L zBrviI5og2l^s1+y!@-l{+f>j6g{2D?Ejw}Q0&S9UDIrMGG&R@E)6}$A7hODgD!$eX zSO85HBohcVJuFWZt=I&#ah%7D8ADFLK-&Z~MH{JiK;|y#(reeQ<jc{R?j52X zLI>MhESIEI3#-ljXh5q@l>!!l7U-{+wR`*atq9r{bbdjJ>t*kQhfmYyxGavyViPBY z!q`kGrdD?E9zBWwyWLva^XJbuUI(v1lIq0NnxJ#@3Z0zY1A`;j=0^E{akP2B!byA7 zUlYeaBLwG21vVs`sRIp9Ol`6_``>mY`@)f=c@q*PimCi)ffi_)K>y#o43kqbSbil{ zmvYinj;1_m-HP46K2PxQ^vTYaB;7R7Ma6Gj+`Rs=-5ye*RG*YF6X?KU}JiGP`n+))=a zM%5Y4)1r}>oJApYRWr89;w)y$Ct>EPKwAwox*cV?Df7?zHw7OlVMX~QiqQ+S)plj! zc<`a_C@(-V&pAz!lrW=su+zdku|f`WA=h`PBO{S>^2ll z+eU_f#))QYg=pFqG^KVaG;cO^3aIs8hUNq;UWPMf%pg;VV;2xT1XS*wKnt`?paohc j&;l*cGJzIo*_Z7<;UJ-9D}!be00000NkvXXu0mjf(ml%l diff --git a/tests/ref/issue-2213-align-fr.png b/tests/ref/issue-2213-align-fr.png new file mode 100644 index 0000000000000000000000000000000000000000..66f56d76f4401668defcb335a8f0bd1351cb1aca GIT binary patch literal 291 zcmeAS@N?(olHy`uVBq!ia0vp^6+j%o0VEi{e|Q%Pq+WTtIEGZ*dV6Irmm{OZvBKR) zGt3kwA5~cDbEn~??1m1bB1WsuBmpnUDU+`*%TZ!WHHbLT^mM}Kz~^6fF<*XZ)BJpA zNiL{)N;46xSVH?ZkfS`MN1VwEZ*WZM_|Jy%Wtn3BGz}!C_G-h?Z2c_#n> literal 0 HcmV?d00001 diff --git a/tests/ref/issue-3481-cite-location.png b/tests/ref/issue-3481-cite-location.png index cfc13db5126fca92603d5cc969aca992c6be1292..63388bd250bd6abd0b0d222a6824080628ac82d8 100644 GIT binary patch delta 479 zcmV<50U-X`1o#7xBYyzlP)t-s|NsB_`T2&1hKGlTdwYA0jg6d~oc8wisi~=>?3I<3ZEbD*{QT|h?K?X=r>CdN%+y6kPu||%{{H^h+TyIPwo6S_ z(9+u5-Q`0?O|!MV+}`F+P*_t{T{bvC&d}K7_{rmg;`}_O)`ug|x`SI@ayaC>g(_3=IG_+<>27r-QVZm-{0Qe-rL>f+T7&W+Tz*S+0@qF z*4Eb3)!ot4+tAY5&CSos%+$%s&dJHi#l^+J!^^(F$hy45w12j~wzjshv9YeOxUaCZ zudlDHuC}YKwW_SNsjIWAtF5c6tEH!}q^7Q>rlzH(rJ$gqpP!$co~E3fo|&4SnwpxJ znVplBosyKClarN?k(rQ@m4t?lb9H@mbaZfXdTwuXX=`(9Yinp~a%pOAW@vC_XK!L; zY-D6)VPk7!V}D~)Rb5L>RYyrtMMqCZM@L3RMnOYMJ3BiwGcy1H002AYyZ`_I{z*hZ zRCwC$)K_i-Q4EFQGnsHhic_6QNUx;#GU>gO-YNTE2Z9A49>5o&^KIalY}p_pA|fIp zA|fIpA|fIpsz$*RPM(NrA_`h&J7916H?=YU;$TkiQ-6A~;2}Kuy}tK&;~}hN3>%QL zkZ*;EsDbTQg!&n&h>K|^;nYC5ec9)n>p4CMVMnf3Br~cLw?nEQVj14ulFYECv#R=P z8s1zih4srktm-%XGj1GqBJQDEX0BI7o*iu69d-gp#_uqeNCp7V*2=e5Pwi(X0Cr$b z(nmno-#Z+v^}d}0ar6Xhq6&P*`Gk+~(il#cJ%pqGL9FHa;YGmvCZYya1evW((BL~U QegFUf07*qoM6N<$g1VzcR{#J2 diff --git a/tests/ref/issue-3641-float-loop.png b/tests/ref/issue-3641-float-loop.png index c898d54e9df8501d290750c990bb48e02c71ebfc..c294c1e471abd59a4653c4fbda85a75d2aea88c8 100644 GIT binary patch delta 646 zcmV;10(t$w1*Qd%7a36q00000JghzN0007QNklk@OaKWaOjC zdO{w!-Q{w;x&_2XtEp4k zP=O!tJ#q;sRsv1r3%1rnQxKOKPUaRN)$O`xEz}jDC2PPh3QyFI$oZ-lNExt)^e<1{ zk7S?35$Zq_mCQCSaBMHa?KUL9^L3}tyv(HE8YRUS8K?r&6ZFIYH zTDj?XLyqufCsBV*_RwtGa6W1Am#tG)Hk#ys#gG_|Kng}8K>*&b0N?`_lR(UkKos$i z7>cbfi_wS}6q~P`O^tLsW*#@jK=)hdKPO!-9_uJgvRT1|A5gnZW)t6ZLZ};DKUSZ*TT8fWvw~+$LonzGCp< zrS1+NAROlK-@|uzTMbhzV3fnRiwRsM4Vjq0rGq_Mk;#sM)(0%$EkN5C1K8GTwmsql ggu@)>Fo%EJclrm3pizeu-2eap07*qoM6N<$f_T^|F#rGn delta 672 zcmV;R0$=^61;7Q67a4j800000QOC#y0007qNkljo_jUQYaY`Gw$>DIkkHgBj2BX1^v4b7DJ|NUu z+qadu7AtrU+I1yEMZ6h!YUs)CvVPVE-eWSE&d1?P>EYL`2U3444rN5qp<9E*T^M5h zh-GHYM+<4yMq9H4yF&sK-(;UDAJg(yFR=pP0PA0!c`T=%#R+ah8J~+*v3Z!;RbYD#U}P&e zc_#*F0>3oyykUQQImJ^ihZf4&=A(W{8BL9Rw|!Q>?Q~U|^kFYEOn35B%V;L6^A>E< z7gzUc0V@F|C`0wjvLAr^CjfYWK5B65`$)wNfLGA!P zr7a~Lo=%gjBh8aYI*gU^7jyI;6HG9{1QSd!!SumKXH0xB!2~A}mONtM*_NAP-~pde z2<*KuRk>W;gvV+`a?=0000EaktaqI1!yWW!%C5{*V z=i=@S&Dv@;!Ov=gf{K;EONRgz53YsETR9HLiOgzU;d;2JIJ-0Vo$rK&WfM5qj5xX$ z`6+pFM45P)E1BHtzCAC#eeZ>XdGFd|Blo|z|M2ip-HsavcbrlPkxV?)!YQogGs6HY zW4qp+2@O$`O_iI&zdRI|T^z86tzr%T?d$zc8*6WRgh~6Ax$&=cSUWd3{nD)se}76} zH(inHx^Kfu&gFVcGn-jgOj*xjqdn_GY&6q(Js+o~1=EY)*obo^9@>AnQQ7Wx@QSDF zi@)%`JhZ~?k%7o+re|9dvmZWW5ZV05(@XlE=)PMM8>7VHPq+O$;_!OQrz4rp((4#* zi7l8C$mk%>w{@4Zae(#>sfSF8K0o+Btg8L}o8f{{MU{g=W``IamXq!jK; zXxqxHDP?VoF9s?G2F=6&64Sc!L1AS4;NPOERI5Aj`Pz%#75#}h@alE!h1Oh+MHQ-b znf&{%bRMVw~Y$bPBu}%I>jG5Ng<+)_U*9vronGE85h4hp9kxLf#LR525nKtZxkU1P-ytd^AgGQH#^=IB@mArD(<*@vmj9=RF37+6@+qnv;deVNB;j6|9xE7$whE+Hmyx zkI;-qJI)-r>J-1VzPg3G1(q->v=4m!>v|y89cH#ywsAT-G@yGywp6 C^;uE? literal 0 HcmV?d00001 diff --git a/tests/ref/issue-footnotes-skip-first-page.png b/tests/ref/issue-footnotes-skip-first-page.png index d24387e3bad35ec8d7db6891723f70e3c97ad26a..fd973af71f0a6ee7db7451d1e27fc3d7e9fb85ee 100644 GIT binary patch delta 487 zcmVu9aD04x^z`(zv$K_zm9Vg|zrVlO*x1z6)SR50)_>O4wzjt4-`_??M!LMj zl9Zf;hK^}#bDEl(Gcz;#`uef4v7DZ!nVO!MnVr7C$fTyOuCTbm!^e`7mvC}=*xKS^ zWNb@KRjjVIhKP`8XlUi-<#2FtJ3Bk*=;-R}@AUNcla-zO`~3I!`Q6{=?(g#e005@6 zi#Y%Q0M1E7L4Q|B7i(Fh?%ih*@`SRv=D06*?{xP54ZQ8xF#q_UXwn dj{yLfGzN)?L80{4lEDA~002ovPDHLkV1k)-5e)zU delta 547 zcmV+;0^I$81iJ)~BYy!4P)t-s|NsB~{{H>_{rmg;`T6g(_7>gwp|=;r3;t}($dn*&C$)x&C1Nw$;rvc$jHXV#>K_O!GFWc!NI}4z{tP9zq-7{ zv$M0Zva+$Uv97SVu&}VNudl4Gwyv(OtF5)9rmm)@rktLpoSmVZoSd1Oo|u`Pm6xBB zm7SH9m6DX4i;b0yjg5+miiC!agoK2Cetvv>e0h0!cXxMia(ZxZaA|9EW@vC|XlP<& zY+hboU0q#NRDV=UO;t-vOGQUdJ3Bi%IywLV0B6m>*8l(j>q$gGRCwC$*=J5dK@^7J z5AoXY+P#VmRP4QAuUHW~b^j|NVF5QGfisl%*}$8blQVxt0MH0Ck)Bjc_EwAW^yZf* zbHPVEc-VCiPUKI{KA&BjxQ4I+zb4AF0-%jNg;MkMhks%)oy5Cbmb4;se)>@AEic-K zA8)18wRih)VnNETm+iwrpwz+i>z0FX>~}~T|9!F*^}NyKxMq_hY&ZaPxr!s)JOBUy z0MunTl$s|Kiou`(25zn=Gv|AQvgU7J=zD(YP1eSK?v&fPI(G6Ik~_Npa}%CZVah>x lqV%$#l^hchwi`gpssc{_PXS!+kh}l@002ovPDHLkV1mJ@K;-}c diff --git a/tests/ref/issue-multiple-footnote-in-one-line.png b/tests/ref/issue-multiple-footnote-in-one-line.png deleted file mode 100644 index cdb83af204902f3fea498fd5d19b764976fab52e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 704 zcmV;x0zdtUP)P)t-s|NsBl z+1aM1rl_c>$;rv<>+9y{>rGErPft%(RaHz(Oi4*eNJvO(YHG{N%iG)ALq$!-$k4O1 zv&6*2g@uKWk(s5Zud1xHw6?yftFx}KxZvR8&CSpI`~39u_L!NSzQD+wo~D_ap5x== zdwY9%d3kzzdVha^fPjEjR#s6_QKhA&>gwu}l9H^ftXWxE;Nala*4Ce&pQ)*-`T6<% z{r$$q#_#X%jg5`n-QBdbw7|f?V`F3T^77u^-d$g3R8&-AVq%@0opW<@_V)IXk&%>? zl!=Ln)YR0lu&{1!Zfb0FMMqD}&eq-E=kD+G_xJhn@$tdI!PV8(%F4=XYind=WOsLW zv9YoB_4ViI?4O~jwYR^QnW1rWd+O`&Vq|QSm7SrXp>S|;$jHd}`1qEVmVSPIFfcGR zH8nmyK0iM{BO@aK008A_j(7k70WnELK~#9!?bO9iLs1lk;e`};cPmbD3dN=L7CooM zrBK{?|0x5~DWm2dlK$Td{K-!C5&!@I000000000000000006n-+Yv4gAe*YCjJ!4Z zqaq~L))*q_I4*HtT9$Th@2e1yk^3lroMa%Z%3u)xA1Q`9@q4zPvO{5NKcOs)>UH||90000006=!-|4CU^z`=7-XGgo7u#Ihw!cDDOm=8M$H@7IVIP4&7 zjFy*1oP<|b0;BR>!dev+j)nMdWL;NxU$9IL*q&aM%7OIVy~-!X-7MFr8tR>dOXbx$ m3750pn^E6U&i0000bU$O zz~HrkNyP4!Yi5Fh&KsqA)2quXRvfsT!hipK;`{SYf8L9$`C0cSn{K zhWng757;jI*E88XUh#RM&c;RowHUKGdHc+6O%-o=DCrg*!X$SkOn;3=%l29G4mU)* z`<>TnN!Pu$-`eff1g9Ma|87Qnsra}w>OqgO`8xj$?HmP=+j{112>I9pWaO~Dx#c^P zV|UKEOQ)Zi2$n7SQY47%@`BbwX5lYWjRo6^&5w#Rf;|?axZ$4lt=nHb8OnT_Iei)I zm>68YY99T1m_cD;(4Fm^3{09w8LcNw+7UD3m2~xaHm4aIG&Nm+ulT*#!z-}4&Q5Dm z5X&ZZ)u1k^hP{>(rzml;s|0m1CY(^87?dN;@3$G~#T=$N`-&`YY*J%bY~Ap43CkO9 zCBD`V=f7$2>^ORRsZd{J-~Zbm+q^8^&omb6H(lWVDD1X=?_DlX=zxN~?K>#^cgR@W zz3^gvq-ev%?<_CZ3vJNfAli83zG2PUg?STqPS)p$O?mg_gm%el>-V}~ONC@+a%@#+ zd5~V0usEJ!+bf2mSI_-s|87jG_|*PsXK>;9@&~1B58OF3? zzoHvtAuuA}?U*atc=2r`kIjmet@XR_pFCiA+ECJ>Ts=)^Pu)!8b0+g17YipIdUl|4 zezG6Su}6U?4rHreJ@o5D6OT>u##0Awf7`(EXaBTUE)SeuJzc^v=k=ch(>zfUo+#gc Y#`vFK7q}=$v4Ap#r>mdKI;Vst0Ahr6PXGV_ literal 0 HcmV?d00001 diff --git a/tests/ref/place-float-clearance-empty.png b/tests/ref/place-float-clearance-empty.png new file mode 100644 index 0000000000000000000000000000000000000000..f170df34e113098bdae697ed6f5701f8b63c4815 GIT binary patch literal 1235 zcmV;^1T6cBP)JUl!oC@3Bt9{&FR{r&xwm6fKZruzE&rKP2zp`rHn_E1nzR#sN5tgNlA ztFMdQv9YeMu7reyYHDhShlfZ=NQsGwI5;?Yd3oB}+CoA?O-)V4 z#>PxcOp%e1udlCDQ&Ydczv1EG9UUD(K|xzvTUc0F$;ruba&l~JY=A|mGI z=5cXxoSdBH<>f;|L!h9b85tS-`}-dsAE&3MVq#)DJ3I69^HEVz@9*zNM@Qn~;+mS8 z0|NsvFfe|8e%aaC-rn9LBO^66HRR;vDJdybR8(YSWIjGVTwGkZxVW~qwk9SfOG`_P zjEv08%yo5j7Z(@c;Na-!=q@fUVPRo(baZWPZT0o_b8~Z&l9JZe)|r`^pP!$6e0+O* zd#S0ZN=i!8)6*m*Bqb#!qobpUh=`MulWS{h>+9>Vu&~I;$d;Ct!NI}R)zy!WkHW&j z78Vxr^74(1jijWc%F4?4`1tDT>Z+=${QUf1Utf@rkU&5{U|?V?D=XC0)BpegfPjG9 zIQ``S00Os3L_t(|+U?p^Q(I9Oh2fPJVi3XIUFz=c?(Xi2y9Nq`7)eU=V|&?)T+lsa zvXJ%Mf4sBd%*RP)LqtSGL_|bHL_|bHL_}7n&Z$L1Gv*W1NcML2x1m|{iGCzYSg?dt zOenk}!ueU8aP!s{PB?$jO9@B($Y@}M!wJXXI!5>#aPB-IyvY;7WPP0$h{6glKiubc zIo*tKRslA87-3{$oD(jq*-=3{v8{ZP5q<-lIZFsT@7~Ei?k0q7uxoRz*>F6TFcdSN zh$C5a!D1+GKJiOm2Z%@t{2|eAUPMwL8c!ICn@>cMEFQBMikVL==!JfLhc}0ee$0md zv}Cxry@M0>H@u*P$H%4~KVXFYfob1;LfHKi2;Q@&KfK2h!eo^z&EbC(R(K7Chp%0* zlrX}0git9ZoM~@qqlC+x+uDPa69e8bBU}Kyd_@Rnd-JnfwLlbBI3?lF{*x!_2w}Hp z4z_)SaF-82H6i?cg7i0T8zF_CJZ%Q{+Xzo}=LcW+0WA=P6;44o`)k+e1S4#-0^#SR z_i8+QO$tYQN#V*-LU<5}^nXf!c>arTcw|5eL}7(f5Oy|2w^T5~moCCy3C=%>hY?EH z+I=fXI`M7JiHCr|c0#zOw$V{qqy?g|!U`))4eK6=F7NB|zKDn}?~8~o@9XluK1r;e zuxn_vw}%nNATUD-*B4FvpoA}9xjIJ)XCeYdIPwvogq>qKS|AE5oTBi}j=pyZLiqZP zIUr$Af4E`h{7qHD70o6vYJm6 xB3Vh!Mnk#g6T9@Cv51I>h=_=Yh=|CF_7^$Uh*KBc9>o9v002ovPDHLkV1g?EQ}6%) literal 0 HcmV?d00001 diff --git a/tests/ref/place-float-column-align-auto.png b/tests/ref/place-float-column-align-auto.png new file mode 100644 index 0000000000000000000000000000000000000000..a1dc9203a43b631ade5cdc392e78ce5ebcd5a71a GIT binary patch literal 932 zcmV;V16%xwP)cAC#BC}mWf4ej ziurD0npVydD@iM@G^bN%nb~x{Y`-o7Us&^;vvam*$n(Aa|M|c!p8W^g210G4Foh|+ z^RCQ$vCk?Vb6s}qoNGLVAI){WfWZaAreM@K7_$&ivcPI zAQo1sgu)1#MJ7nVnVOLbBJg;qdPzK-l_eO)nGA@A4QXix!EncS`hZZ_4zBm{!sIw^%8)G`UeO5hd5vY56JsB`p-5J@VlLFn=iK#fFTE#4-ZQLN0xIIxenYT7tL9OX(Oy*+RFTY@OW~LYs zz`DYFIs<*VC`@4rQ<%bz;Rzmz-?4{leRpx@TjR_Q{SQ5bHw-kfrLAl;X4uF4;aav6 zW4YC8wo3#I|1sOauR!W7f7r5n4oF^n84fXUh#8t}Earhf4CQ_NEJpu%vGA`TR16D& zhmYazj07+)wX+ZcOtzQ#Q;Kgod$V|^{MNdp2I<;4vc}D; z8-7hc(kQ;%^501=xqAkOHm}ufRhBFGm!}`L!TqI{>iTCwd+G|lOKr|8xGu%a5Wnp1 zo>wQ|?Gdh6Y1S@mal}5HewGH^b*+g+^@!@vh?4NDPfAAJEthGaF{`XLLs$oVs0l z|Ngo+4C|J0+&Fyn?13{H4qr4b*d(&>|9*GydHI1)t=GbB^533Z;lRSJ$9*HT!NGEs^zrIndnT-pM7> zKkhJnD-iu+@A-A+Vds4Y)BpUNz~zGKad-%Dmzm%5Z&knB^IbY)-}{t3|K}Mi=N7y0)&p-dZ2lfj45A|nXKisf8$Q=Vpe4eg;F6*2UngFRl BGlT#D literal 0 HcmV?d00001 diff --git a/tests/ref/place-float-delta.png b/tests/ref/place-float-delta.png new file mode 100644 index 0000000000000000000000000000000000000000..578d930118a7cbd3714d6fcffeb42ef24be7a615 GIT binary patch literal 317 zcmV-D0mA-?P)k``}I6I zpLfsx+CDu25-I=~FyOz3H*HrNMmW@HIW>cv@ROWgfc*YxrAA%Qir3Du}(eg^#m`&Op>U!1? zfGP4l2{`^az$|d=a}uzT*7dZ30RikOJWnK*5;G>4%&n4%XAdx7z(TzNo8fOCeXfxr P00000NkvXXu0mjfE6s|d literal 0 HcmV?d00001 diff --git a/tests/ref/place-float-flow-size-alone.png b/tests/ref/place-float-flow-size-alone.png new file mode 100644 index 0000000000000000000000000000000000000000..e1b6adac5caec9ae59df941c226a5c396c7ae1da GIT binary patch literal 125 zcmeAS@N?(olHy`uVBq!ia0vp^(m<@h0VEiXF~vLwQZAk@jv*Ddl7I9|8}yuVY&@<# zr)c?RwTDw)|IKLkepmX<@$zhoCsqEjbHmzW-$j4?|KFbHU((ds5&vKP?`&#a{Ly^M Z6NZlpL1`p=fS?83{1ORZdGEV>i literal 0 HcmV?d00001 diff --git a/tests/ref/place-float-flow-size.png b/tests/ref/place-float-flow-size.png new file mode 100644 index 0000000000000000000000000000000000000000..60bbc7cd445a5db962e8fcd3eeec74e88628a200 GIT binary patch literal 347 zcmV-h0i^zkP)IIMagKrRo7B|0}CK!DhSEFM$&*9|j5 zxio#1xRF9IdY-c*PxVxh9ZA#jjvM*=r0G2~vIsuP`4xuIYNYRJ{xpuK`S$RICL-wc tBoH9CglN7!+=d=Pp-NcwK!98$asUq1`_`r!jZFXm002ovPDHLkV1oP4k8l70 literal 0 HcmV?d00001 diff --git a/tests/ref/place-float-fr.png b/tests/ref/place-float-fr.png new file mode 100644 index 0000000000000000000000000000000000000000..83d310540b9bf8f17716183bd068669d173778f2 GIT binary patch literal 507 zcmV3BNlgNZzJ`Z?$LMxS@z{ndB*_DzCNNrVX#{)6yP$f03>;JLz}Lj^Isw$VA# z8t?|LghLf?;G-%Fs270y#{k#@aMLUR2*7;--{Ae~>crRZMZqR)!}bHlzyQ96aoQ6Z zzl1OF;NVC2U4*@XySilHL`O$L1b&I|--6>>xFH!c x0JdR24=>kmhD$>pRH_am=t+F`2oomkeNUFyxfUvNRcQbK002ovPDHLkV1jEX=63)9 literal 0 HcmV?d00001 diff --git a/tests/ref/place-float-in-column-align-auto.png b/tests/ref/place-float-in-column-align-auto.png deleted file mode 100644 index 58ba9741500bb994f9a2d9ebdb52272d1cc7f90a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 843 zcmV-R1GM~!P)q<+C@4TLDD5?1uC?uP|R)4u|H7LvQ5d- zx@1}r((vN4^bm-kNcgAOX6B#%sXfmp=r9zt-=00sv)_{Mn;pMDUi>b<=l6^!1)NGD zm|%kU9&CysHDHt0$V_WQw9!Vdp-vIl2u9mx!si6w5(n6R4}i!4zVZ|RIKVA+s=>1r z6?41KtQLHL_(W6#hQon>%q&?vzBQ-g(q?z|oQo_8-6Fw6Al3 zaS#|eC2)W>04_dM0PauEW|sbZJM5}e07fK+upMsla=3Hl0ZXMziHEx?J$}z3U2h2{ zcqjO6MmDpIV07HCv)^h}1U6ldFuP$ywEaLQz~v$Fev=EFd%x@#ryVbgs=z}7fxy7f z?lT8~zHv3+P}=NlT4>i9f3eH07u0~E0W_=`g8yrHEHxNR9peB`jZaRFPbJ@*L}#`0 zrjG|K?1R|G>ByW|R|YQcW>&rh&h0q8stCNOKhNw;Wb2M2n*g6T3Gje@`Mq?@B$(jj zg0HUefhTnj_`t36rCi`|4UrR{c))i%dJHxWuy_qvKE?qy*Mu>wYo`N*V1nhtUDX}d z86L2hhv_07Ff^FB%mvQ#owbU5c8rrRxa=6H>E-};9`XnXo^~=_4ccVd3YGjlVx3!Ejh#GFDbLTA~jp7>3sZrxO-05)HRm;c~&+ zMzHE|b1fPhP-jjw9CEvZZuhX;9io$K55a~7W(GCjcUd{ivPS66MKHkx?*uQzP&{i` za9?H)v&?5Q*2sNGNMA#6a>6fd?Sr;g_-cDB0gl)ln|RdvMGlNZ^J4xqU|5j=!(Iyv z`5_BF`W>ETthYc4yiy2Qf6IoK3(@mY5{wIPHXM)*qbgv7G&oSzJ0lZ5vIYzm^+|+L z&;)t#>vRi%VnHf=yI&?OLIfZ{DlE{+mCUf##7t|9V+{+2F^1w7 Ve$oZ9&qDwJ002ovPDHLkV1g-od{+Pf diff --git a/tests/ref/place-float-rel-sizing.png b/tests/ref/place-float-rel-sizing.png new file mode 100644 index 0000000000000000000000000000000000000000..1b4e44b2da7dd1864b95cbc3d606000ca04c4287 GIT binary patch literal 335 zcmV-V0kHmwP))2n8YZi0}UdAN&F6iRRzOc6QkgVFbbk(F)gjFY-reEVNsjHiP>r#K)|NqrUL^5B_$;&C@3c< zCo3x})YR1Q@bFAbOtZ7Id2Pg@uK? zG%O=wsn^9!xK<;qDO|0Z30swl#R5WjfUA7;z7*)aOK6`;@FWKyQq%sVFZhTle}oAW zCQO(x;o-u82ookun6T+^@MQ@TCQO(xVZww76DF)B%$FYkvG#2UcOJmVNI4#MhkoN5 zc*OFd>tUxHeRxScln9Sq_|3lRwog0TYfa5zxxJRlGuc`f-Qk;#T%6112I0uVFVAv! zFS9e+XZ7n-_wk`KS8cL@5GG9c|KY`B4=2%wS4u}7+E&EjzY!)pn&Ci%wT2V^2ooke zx#5H_!h{JECOj5lz7_$9)V>lD+Ft8GfmWH{>ktlqN-mj7C36PC*-Sc}$r=dD!tWrg zBRt<9MrL)fP$-(1)p^PH2P;>c!?vSkIlTJ^I^VO){f@tw*ZUuM?b`2meRhq(q{e7i z!y4AGG4Q9oNDan*l)LHzQr7SUr#jpmw->SMB;1z}TUm!XoH6(zVzuiW<;cZ0s>6Np z4Kx0d;W_!6f&gb+ZO{HrhS%fu7!JEqz%$K<(9d!BHX;^Ps|;sadbzy*sPtEF+i-CE zA*8G++nstqG_2u*_m5bbkQz)}n7ynSsotb{w^U(5>79vKRU(c?#LmQ^42Stnq!VZ| z&mdQ~Rvn&vY;&APh9hzkg8+xYlF~pYb_4;Q2uo{`s#pVo;CN-YXh}}daij*f#I}Q6 z`3_U=vHf`3eN$j}jayHqhW`V;mvP|hJj87I1*r`PS(j~go{s%f33kmNVq_sw*Al>; zid6Kuksta|f^)Nh2S_cw0X#%1wmzc|HF#YGftuuo%MJY^{9Y7_aC+deo7M^~AubVa zOx~>s-+kjg)Quo?Bm&%1ky?J<`|(LNn2wFvhwmU(l2lZ99wF=Eubf?xje0;d{G7u@ zD{`|ckQ%bAvvFXBbWN=ZTb-)#$g5BqgVc!_aK9$c`RS*6SF@-Fp|goV81iI39lYlo z5f0zKe}8b`omnQFcnWwUyAV1R2i_@2t!&_Ctr}eTc6PyzHiU)}=Ge%2NBpZJ686k) z(*vSm4Qp7#8rJYAhwmAZI(%~61OpmZr3|N8FrYMxhDS5py?ggvQ()55F$dnaPs7g$ z&dM?e?uq!MM-PaGp9T(3GoWyF7>gn4$DW2Ytl=QQW~INehBd6=Ai!yc1nRF$ivfk} z2fc<-hf6~X3PMXwfC+&R%z(onOvBFvp5XIMFas`&FD{NR(*vSmb+~5icmo<+tqjv= zNcyp-VGTbYa8I=Zu>FdVqo$-Asas`Lrz%UFO7L)GCJ<)Df8DG4AH^0T^!?dOFL$I^ zm0)(g3l|pO#(&GNfID;m|2@V#74H)@_;B<^O1CuQzYrHN_X1MBw*WQx&~H)p)}5#E z-=uzEL8HO2H=pS_i>XSq7;daFk*Y`YBu&8_&)b+xI!2Qho|RUR~}ZS}<$ z+i-nZf&YdrpWj?mja1JoRk#u>m0&)2^AVYk5voa{G#RlOkKw02H8_3mXJk4=IJs-> zwIIMO+Zq6lT)m0FaLyxD*gbug45u*bHe&PaunQ%)CCY<+yafAxoa<SYZAs zgl0SGG9Lam4>*v7P~P-1pYd>F9#Falsq4{mjE9S(fz)DYtM9-xhv6_$hrKPyK7{7p z>xssAn5JW`h&2_Qd!m<1!=ngxI(mmqflFTaW&0L0U|xrbPt1VBu+PK{cyf965ayg1 w%zzy)dCY;!a`N?ZX;{M=*06>(tl?+Tzd+;&JHgH$Hvj+t07*qoM6N<$f?axag#Z8m literal 0 HcmV?d00001 diff --git a/tests/ref/place-float-twocolumn-align-auto.png b/tests/ref/place-float-twocolumn-align-auto.png new file mode 100644 index 0000000000000000000000000000000000000000..5d9932a351667bb56c56377a6b2f598f24958b3c GIT binary patch literal 719 zcmV;=0xuf`|}GSP;=64;95OLD)qorJ#$@L9C#%C>i+M!Gj8eOj}7rna#t{ z(pDo4v>irn7UjgHdC+o!O*?3{mbTxgW9Mhj)4r`f?|<5B6%Zy&_#cMd zMpB2}PRl?}OC7dgAUEN>hUcrQ5+X3#+8_gWbP&!F+|nWgCtR)s2ZV4IaIk2jfr_3e z!&o#DAA5ue|2J?H%1v}XThWHUJ4T_HyEi?wR~z;{Is>(yUjz`)hFh^`4NAFsO`LJK z_WGd!l&bGC0XG1#<4`Ji1i-Sb2M?S`;p3Jtl)7@^RqsRS!STtjNW`b1)>8Bfmy7UU z3&+Ny2`CnO8F^>x!sEqnTN8=(hBLc_0rK&?nfhG~Sw5OpiE%L=c zwO<0>44ix_0Q;(M_#SXT2op8}FL-WUzAXT!Dpz116@WXv06q)AXZwZDxT#qPPVD#u zjM%w{5x?ZU^PU4jn6LrZSJT zyLo~GLYOdN!h{JECQO(xVZww76DCZUFk!;K`vr5#p1&s_GBE%E002ovPDHLkV1i4M BLPP)n literal 0 HcmV?d00001 diff --git a/tests/ref/place-float-twocolumn-fits-not.png b/tests/ref/place-float-twocolumn-fits-not.png new file mode 100644 index 0000000000000000000000000000000000000000..e533daf91a645e5a00d12d28ddfa992dcc4b7310 GIT binary patch literal 1043 zcmV+u1nm2XP)W|2Sw@jHx)?G`CcAz2>gO&$^xk{*p?E+4fd`+B?~9EupHWcMD1ikQ zSm5};DFYz(y11@5hYjD!JPumE6+xgd%!N%K=7Cm?DIOp7`>bozpMcc1O2Bi%#e_|s zp0a%)bu0_OqHZ2MvKFJSxZDnkco z*=IQ6PT3P;!5E8yWDrXPu=*Z&uui*h5X5u<(z==OzcqU@1Jn~+t=6erSO=szRh)PX zklqSfV*sr5@L)$;xuYJm9BF6a*>oTz10t}%pTloWw%w;ds(QJT509++i0E3-N&p|O zC<8D^o=iTR-2eaz;DZHX;3|gC-z=oj%klGAVnOEWjm*gsfkCZBa#{rS0FNm;<4_7CZ*$W1^y1WW>-n!Sru^jZ)itUz|i4&L}? zu*D+qPl5{z)xh4|T(1m>z;qbRqyLOUqnpSu-p3{Ru_v&=0{>9B|5&jT#2S`tOn%3O zFT8%T9>m(h$O9L~jk!G_*X88H5%*+}+ZE))z_bq_Hna_FxTUdmC#W55X`)`EuhsGbe`%^>GKj5UK?xHo-o zABa_)#MTZj91Qf%0I`8o$4vfK1OEjB+aO$J0@fdv*=V1WfD^a+5B^^nCK_WA$- N002ovPDHLkV1nhB=+6KE literal 0 HcmV?d00001 diff --git a/tests/ref/place-float-twocolumn-fits.png b/tests/ref/place-float-twocolumn-fits.png new file mode 100644 index 0000000000000000000000000000000000000000..07e4c25ff9f81bd65f763b71b4af8fbbca4bdcb8 GIT binary patch literal 1001 zcmVw%1)kT(JQ<-Kp5`#{Gn2t@^6poHo zh~?NpqrytVj?zV1T3VK4j8>GAGxO7#`E*;+%`?w155@ET2VVU6^8I{gCPAo43M;Iz z!m)wp4}(@!Vo6~xAHJVi24ac8H9lNC@-K+}ZqyrW_|2A^b9zAQm(yzlY`FMdmt#L@ znGgz(GhzP@AUG=wN}->DiXJ9hY8l3z>?%+?wYB`nHXRU!75*A_r?@P)L905+o?pa; zF%84XAT||fV#B6zqZPzVxaDWVzn;p=41(4a*RoOm7Xuk}??NeJ}ChEmyRNmE&x|J0K5}`vphm)Y_$o&-am$b>yh}w zc)ZI|`4Y7Ha%Apt_|hyH7)Vt(PVm!&0U5acW(OOtU0-4MNDN413Q)IE0EP*`vL+$; zaJ~?H$SwdU0-=Td@rP?>I^4ELpmpALM&us1Ix%fb0tSpZkbw(2W#F>zk#PyQeNDp# z5%_8S+4K6}NnwQ*R@fJl=L9A;@=YnoF49pk-F<)zHgbP3a7)%AVp!p%V z@RL&KD^RN$)B#ag;cvlX9U#{UpyfIf&a#16xa>E;-B{Jgf*X$5KrRJHniyKdfnV1) z(_y3n(-E4%fr|>)&+m!`{s&`e92jV_f!v?K_WlP&EZDz4?*oXrGCcU;bOv=m6jt~P zxaCSS$PEC1mj#E{o*={FvgttYx?C3QJ>~_y6acMzcyMWUc2ti9+ITR4spAsx+kjyL zxtRd2?qtE;IS1~7n(IFg-fmyjqXVL_!e7CIAom`?a6b=zW$vXv5`O{JHx61ku#!5lD*yIi z(~9K>bU+kV_;YyFH}ot&iG=R5WE#Ka9_R^160=C}AMxO-SvHP@D>N@FiDXg%<3M;Iz!tv-6 X0Cxrs_F~0500000NkvXXu0mjfXE@MW literal 0 HcmV?d00001 diff --git a/tests/ref/place-float-twocolumn-queued.png b/tests/ref/place-float-twocolumn-queued.png new file mode 100644 index 0000000000000000000000000000000000000000..e5fa387d63764ac8ce7fae4f3a22bc031a261f18 GIT binary patch literal 862 zcmV-k1EKthP)gu7Pq4M(br>Cd+`T3NTl#h>(A0HoUYik!57fnq~%gf7+jg79Z zt|cWUlarHxfPi{>dM++5@9*!Dl9KD|>#wh`Wo2b>aB#@T$e^I0j*gBaBO{fSm6@5D zI5;?rjEt$NsURRAR#sL}P*5;1FsrMpM@L6vV`FM+YJPrxw6wI=*4BuKh*42dhK7ca zkdU37ous6s`}_ON&CNMEIk>pEBqSs!Cnxdo@jgC2H8nNa+1Y=8e{*wl_V)IXk&%0Q zdwhI+LPA0zAtCVa@JmZe=;-L5pP%OD=5}^=9v&XT!os_|yJBKuu&}Vs&d!2@g6-|? zii(O@SXf?OUM40ca&mGaA|iQtc|1Hkn3$M@gM)N*beo%-i;Ih%o}QwjqNb*%tgNik z($ehg?DO;Ut*xy{NJvvtQ`6Ja%F4>s)z$U&_5A$&!NI|$rKR}z`0noRmX?y5Yxj z#RcJpLrv%qzUL3TsY>cpau5+Y$Tlo+Rm0vWtgylgE3B}>3M;Iz!XhGvz`BJ#3569_ zSYd?~R#;($6&4YZU0H}>6MCoe&v4dovkjvrTA$)9_wp4+ecvCZadj+Pc>M;Ofxo$) zRUGA(S1Px#@u0VeoA>Xabk)bguY<(`Za%z?ak4#*MKU{FENB{th)9(kiHL4qL=K}G z3oTIr#M#fPa5~=;-Jd7Z4-b2Ldq_w~FfcH>y1KQswVTK<>e$KB$Jbq;Naj95fQYsw2+XH zc6N4XXlVHO_|wzV(b3VqzP@j7Z$Cdj_xJZkMn+OnQuX!qG&D3hIXUL$=G)ub*4EbC z+}zL4&j$ww$;rtwGBUWhxX{qhJv}`_LPFTs*rKAMfq{YI;^IwBO>l5<$jHbyH#b~d zT%n<%PEJnm@9&zLnsak=cXxN2o0~^RM>8`sb#-;=>FFscDTaoIn3$Lz9v)#~VeReh zF)=ZOgoG?CEKpETqobpXi;LIS*JER2pP!%k`T6JP=L-u9x3{;$!^3B1XTQI{RaI4y zk&(Q-ykcTvxw*NBh=}3g;Zsvn_V)IfnVIhH?v9R*baZs_@$oG!Ey2OT{QUfiii%xb zU8JO>s;a7FWMuI0@M>ymI5;?MZEcp8mM$(X-rn9yN=hIgAUZlawnw-0002?e3%FT00P@dL_t(| z+U?v|Qxj1b#_>l25t7|dLhrqU(y=S{-g~Dgs7S8@N@#%;*3kU8&Tz@i&e^lsCGh-a zZ{PeUxp?QyV}gua2-`IUPjW=Z$@0>Uj_g!JGkPK`yE?L*MrQQGf1%@~)4&YF zy5YXwsj1#Rk#Mr2R4d9kB{x|r+}-2%_jHSdJ>?aZwY8NMtE?akVn%!bUP4mv^6vR&2w9B4 z1b6Zh$5jY!X?^7T^pop==zz5Ed4VgUGcQJp zB9Nr5V!2Lq<~cAo4@sN2SRp83>nUN`GTCoHY=hDT16;AblHR3W4dB zVo3S*#+RmQf-_6LcY{^j2t-6gB<7z@_{$SfOgkaS$PB{_Gt4l<3^ROG!V;vzdx98d zm|=$Z!2pICW|-jv;gl~)I?_|35{|4T>S%JB7#6xw&TursTKzCkS+7|h9&H$!Mp5H} zL__MU1RW1~ z6`JT+i8lp=m6jx!pv1}7>o6=j@~^ArIv@kWxNM)!DD$?g9gCE%@xyOSDXo4;$b*U!IK;j@$x literal 0 HcmV?d00001 diff --git a/tests/ref/query-running-header.png b/tests/ref/query-running-header.png index 210c7810365a5912aaa8e9159e3e13a7dd5fe10c..1dd044181d3853791929bf10acd9731fd4bbcdec 100644 GIT binary patch delta 6138 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 diff --git a/tests/skip.txt b/tests/skip.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/src/collect.rs b/tests/src/collect.rs index 4dae0b70a..cc3ff7360 100644 --- a/tests/src/collect.rs +++ b/tests/src/collect.rs @@ -5,6 +5,7 @@ use std::path::{Path, PathBuf}; use std::str::FromStr; use ecow::{eco_format, EcoString}; +use once_cell::sync::Lazy; use typst::syntax::package::PackageVersion; use typst::syntax::{is_id_continue, is_ident, is_newline, FileId, Source, VirtualPath}; use unscanny::Scanner; @@ -389,6 +390,18 @@ impl<'a> Parser<'a> { /// Whether a test is within the selected set to run. fn selected(name: &str, abs: PathBuf) -> bool { + static SKIPPED: Lazy> = Lazy::new(|| { + String::leak(std::fs::read_to_string(crate::SKIP_PATH).unwrap()) + .lines() + .map(|line| line.trim()) + .filter(|line| !line.is_empty() && !line.starts_with("//")) + .collect() + }); + + if SKIPPED.contains(name) { + return false; + } + let paths = &crate::ARGS.path; if !paths.is_empty() && !paths.iter().any(|path| abs.starts_with(path)) { return false; diff --git a/tests/src/tests.rs b/tests/src/tests.rs index a2d85fecc..58bd7cf7e 100644 --- a/tests/src/tests.rs +++ b/tests/src/tests.rs @@ -30,6 +30,9 @@ const STORE_PATH: &str = "tests/store"; /// The directory where the reference images are stored. const REF_PATH: &str = "tests/ref"; +/// The file where the skipped tests are stored. +const SKIP_PATH: &str = "tests/skip.txt"; + /// The maximum size of reference images that aren't marked as `// LARGE`. const REF_LIMIT: usize = 20 * 1024; diff --git a/tests/suite/introspection/query.typ b/tests/suite/introspection/query.typ index b078863e4..ddb518f58 100644 --- a/tests/suite/introspection/query.typ +++ b/tests/suite/introspection/query.typ @@ -25,10 +25,10 @@ #outline() = Introduction -#v(1cm) +#lines(1) = Background -#v(2cm) +#lines(2) = Approach diff --git a/tests/suite/layout/align.typ b/tests/suite/layout/align.typ index 61b799757..c4ed9ab95 100644 --- a/tests/suite/layout/align.typ +++ b/tests/suite/layout/align.typ @@ -140,3 +140,11 @@ To the right! Where the sunlight peeks behind the mountain. // Test right-aligning a line and a rectangle. #align(right, line(length: 30%)) #align(right, rect()) + +--- issue-2213-align-fr --- +// Test a mix of alignment and fr units (fr wins). +#set page(height: 80pt) +A +#v(1fr) +B +#align(bottom + right)[C] diff --git a/tests/suite/layout/columns.typ b/tests/suite/layout/columns.typ index 87a9f773a..b86b798b6 100644 --- a/tests/suite/layout/columns.typ +++ b/tests/suite/layout/columns.typ @@ -122,3 +122,10 @@ Hallo = B Text ] + +--- colbreak-weak --- +#set page(columns: 2) +#colbreak(weak: true) +A +#colbreak(weak: true) +B diff --git a/tests/suite/layout/container.typ b/tests/suite/layout/container.typ index 9ce3dc7d9..508f1a368 100644 --- a/tests/suite/layout/container.typ +++ b/tests/suite/layout/container.typ @@ -18,10 +18,41 @@ Apart #block(width: 50%, height: 60%, fill: blue) ] ---- box-width-fr --- +--- box-fr-width --- // Test fr box. Hello #box(width: 1fr, rect(height: 0.7em, width: 100%)) World +--- block-fr-height --- +#set page(height: 100pt) +#rect(height: 10pt, width: 100%) +#align(center, block(height: 1fr, width: 20pt, stroke: 1pt)) +#rect(height: 10pt, width: 100%) + +--- block-fr-height-auto-width --- +// Test that the fr block can also expand its parent. +#set page(height: 100pt) +#set align(center) +#block(inset: 5pt, stroke: green)[ + #rect(height: 10pt) + #block(height: 1fr, stroke: 1pt, inset: 5pt)[ + #set align(center + horizon) + I am the widest + ] + #rect(height: 10pt) +] + +--- block-fr-height-first-child --- +// Test that block spacing is not trimmed if only an fr block precedes it. +#set page(height: 100pt) +#rect(height: 1fr) +#rect() + +--- block-fr-height-multiple --- +#set page(height: 100pt) +#rect(height: 1fr) +#rect() +#block(height: 1fr, line(length: 100%, angle: 90deg)) + --- block-multiple-pages --- // Test block over multiple pages. #set page(height: 60pt) @@ -121,6 +152,34 @@ Paragraph #show bibliography: none #bibliography("/assets/bib/works.bib") +--- block-sticky --- +#set page(height: 100pt) +#lines(3) +#block(sticky: true)[D] +#block(sticky: true)[E] +F + +--- block-sticky-alone --- +#set page(height: 50pt) +#block(sticky: true)[A] + +--- block-sticky-many --- +#set page(height: 80pt) +#set block(sticky: true) +#block[A] +#block[B] +#block[C] +#block[D] +E +#block[F] +#block[G] + +--- block-sticky-colbreak --- +A +#block(sticky: true)[B] +#colbreak() +C + --- box-clip-rect --- // Test box clipping with a rectangle Hello #box(width: 1em, height: 1em, clip: false)[#rect(width: 3em, height: 3em, fill: red)] diff --git a/tests/suite/layout/flow/flow.typ b/tests/suite/layout/flow/flow.typ index 88075c5b9..fcbc005b6 100644 --- a/tests/suite/layout/flow/flow.typ +++ b/tests/suite/layout/flow/flow.typ @@ -55,9 +55,6 @@ --- issue-3641-float-loop --- // Flow layout should terminate! -// -// This is not yet ideal: The heading should not move to the second page, but -// that's a separate bug and not a regression. #set page(height: 40pt) = Heading @@ -69,3 +66,13 @@ #metadata(none) #v(10pt, weak: true) Hi + +--- issue-3866-block-migration --- +#set page(height: 120pt) +#set text(costs: (widow: 0%, orphan: 0%)) +#v(50pt) +#columns(2)[ + #lines(6) + #block(rect(width: 80%, height: 80pt), breakable: false) + #lines(6) +] diff --git a/tests/suite/model/footnote.typ b/tests/suite/layout/flow/footnote.typ similarity index 74% rename from tests/suite/model/footnote.typ rename to tests/suite/layout/flow/footnote.typ index 410912288..f7722e156 100644 --- a/tests/suite/model/footnote.typ +++ b/tests/suite/layout/flow/footnote.typ @@ -9,16 +9,12 @@ A#footnote[A] \ A #footnote[A] --- footnote-nested --- -// Test nested footnotes. -First \ -Second #footnote[A, #footnote[B, #footnote[C]]] \ -Third #footnote[D, #footnote[E]] \ -Fourth - ---- footnote-nested-same-frame --- // Currently, numbers a bit out of order if a nested footnote ends up in the // same frame as another one. :( -#footnote[A, #footnote[B]], #footnote[C] +First \ +Second #footnote[A, #footnote[B, #footnote[C]]] +Third #footnote[D, #footnote[E]] \ +Fourth #footnote[F] --- footnote-entry --- // Test customization. @@ -48,18 +44,94 @@ Beautiful footnotes. #footnote[Wonderful, aren't they?] #lines(6) #footnote[V] // 5 ---- footnote-in-columns --- -// Test footnotes in columns, even those that are not enabled via `set page`. -#set page(height: 120pt) -#align(center, strong[Title]) +--- footnote-break-across-pages-block --- +#set page(height: 100pt) +#block[ + #lines(3) #footnote(lines(6, "1")) + #footnote[Y] + #footnote[Z] +] + +--- footnote-break-across-pages-float --- +#set page(height: 180pt) + +#lines(5) + +#place( + bottom, + float: true, + rect(height: 50pt, width: 100%, { + footnote(lines(6, "1")) + footnote(lines(2, "I")) + }) +) + +#lines(5) + +--- footnote-break-across-pages-nested --- +#set page(height: 120pt) +#block[ + #lines(4) + #footnote[ + #lines(6, "1") + #footnote(lines(3, "I")) + ] +] + +--- footnote-in-columns --- +#set page(height: 120pt, columns: 2) + +#place( + top + center, + float: true, + scope: "page", + clearance: 12pt, + strong[Title], +) -#show: columns.with(2) #lines(3) #footnote(lines(4, "1")) #lines(2) #footnote(lines(2, "1")) +--- footnote-in-list --- +#set page(height: 120pt) + +- A #footnote[a] +- B #footnote[b] +- C #footnote[c] +- D #footnote[d] +- E #footnote[e] +- F #footnote[f] +- G #footnote[g] + +--- footnote-block-at-end --- +#set page(height: 50pt) +A +#block(footnote[hello]) + +--- footnote-float-priority --- +#set page(height: 100pt) + +#lines(3) + +#place( + top, + float: true, + rect(height: 40pt) +) + +#block[ + V + #footnote[1] + #footnote[2] + #footnote[3] + #footnote[4] +] + +#lines(5) + --- footnote-in-caption --- // Test footnote in caption. Read the docs #footnote[https://typst.app/docs]! @@ -71,6 +143,15 @@ Read the docs #footnote[https://typst.app/docs]! ) More #footnote[just for ...] footnotes #footnote[... testing. :)] +--- footnote-in-place --- +A +#place(top + right, footnote[A]) +#figure( + placement: bottom, + caption: footnote[B], + rect(), +) + --- footnote-duplicate --- // Test duplicate footnotes. #let lang = footnote[Languages.] @@ -105,6 +186,10 @@ A #footnote(lines(6, "1")) A footnote #footnote[Hi] \ A reference to it @fn +--- footnote-self-ref --- +// Error: 2-16 footnote cannot reference itself +#footnote() + --- footnote-ref-multiple --- // Multiple footnotes are refs First #footnote[A] \ @@ -163,10 +248,7 @@ Ref @fn .map(v => upper(v) + footnote(v)) ) ---- issue-multiple-footnote-in-one-line --- -// Test that the logic that keeps footnote entry together with -// their markers also works for multiple footnotes in a single -// line. +--- footnote-multiple-in-one-line --- #set page(height: 100pt) #v(50pt) A #footnote[a] diff --git a/tests/suite/layout/flow/place-float.typ b/tests/suite/layout/flow/place-float.typ deleted file mode 100644 index 50a8a1129..000000000 --- a/tests/suite/layout/flow/place-float.typ +++ /dev/null @@ -1,83 +0,0 @@ ---- place-float-flow-around --- -#set page(height: 80pt) -#set place(float: true) -#place(bottom + center, rect(height: 20pt)) -#lines(4) - ---- place-float-queued --- -#set page(height: 180pt) -#set figure(placement: auto) - -#figure(rect(height: 60pt), caption: [I]) -#figure(rect(height: 40pt), caption: [II]) -#figure(rect(), caption: [III]) -#figure(rect(), caption: [IV]) -A - ---- place-float-align-auto --- -#set page(height: 140pt) -#set place(clearance: 5pt) -#set place(auto, float: true) - -#place(rect[A]) -#place(rect[B]) -1 \ 2 -#place(rect[C]) -#place(rect[D]) - ---- place-float-in-column-align-auto --- -#set page(height: 150pt, columns: 2) -#set place(auto, float: true, clearance: 10pt) -#set rect(width: 75%) - -#place(rect[I]) -#place(rect[II]) -#place(rect[III]) -#place(rect[IV]) - -#lines(6) - -#place(rect[V]) - ---- place-float-in-column-queued --- -#set page(height: 100pt, columns: 2) -#set place(float: true, clearance: 10pt) -#set rect(width: 75%) -#set text(costs: (widow: 0%, orphan: 0%)) - -#lines(3) - -#place(top, rect[I]) -#place(top, rect[II]) -#place(bottom, rect[III]) - -#lines(3) - ---- place-float-missing --- -// Error: 2-20 automatic positioning is only available for floating placement -// Hint: 2-20 you can enable floating placement with `place(float: true, ..)` -#place(auto)[Hello] - ---- place-float-center-horizon --- -// Error: 2-45 floating placement must be `auto`, `top`, or `bottom` -#place(center + horizon, float: true)[Hello] - ---- place-float-horizon --- -// Error: 2-36 floating placement must be `auto`, `top`, or `bottom` -#place(horizon, float: true)[Hello] - ---- place-float-default --- -// Error: 2-27 floating placement must be `auto`, `top`, or `bottom` -#place(float: true)[Hello] - ---- place-float-right --- -// Error: 2-34 floating placement must be `auto`, `top`, or `bottom` -#place(right, float: true)[Hello] - ---- issue-2595-float-overlap --- -#set page(height: 80pt) - -1 -#place(auto, float: true, block(height: 100%, width: 100%, fill: aqua)) -#place(auto, float: true, block(height: 100%, width: 100%, fill: red)) -#lines(7) diff --git a/tests/suite/layout/flow/place-flush.typ b/tests/suite/layout/flow/place-flush.typ deleted file mode 100644 index 8f55a6fd4..000000000 --- a/tests/suite/layout/flow/place-flush.typ +++ /dev/null @@ -1,29 +0,0 @@ ---- place-flush --- -#set page(height: 120pt) -#let floater(align, height) = place( - align, - float: true, - rect(width: 100%, height: height), -) - -#floater(top, 30pt) -A - -#floater(bottom, 50pt) -#place.flush() -B // Should be on the second page. - ---- place-flush-figure --- -#set page(height: 120pt) -#let floater(align, height, caption) = figure( - placement: align, - caption: caption, - rect(width: 100%, height: height), -) - -#floater(top, 30pt)[I] -A - -#floater(bottom, 50pt)[II] -#place.flush() -B // Should be on the second page. diff --git a/tests/suite/layout/flow/place.typ b/tests/suite/layout/flow/place.typ index f3231735b..dc655ec59 100644 --- a/tests/suite/layout/flow/place.typ +++ b/tests/suite/layout/flow/place.typ @@ -70,6 +70,288 @@ Second #line(length: 50pt) ] +--- place-float-flow-around --- +#set page(height: 80pt) +#set place(float: true) +#place(bottom + center, rect(height: 20pt)) +#lines(4) + +--- place-float-queued --- +#set page(height: 180pt) +#set figure(placement: auto) + +#figure(rect(height: 60pt), caption: [I]) +#figure(rect(height: 40pt), caption: [II]) +#figure(rect(), caption: [III]) +A +#figure(rect(), caption: [IV]) + +--- place-float-align-auto --- +#set page(height: 140pt) +#set place(auto, float: true, clearance: 5pt) + +#place(rect[A]) +#place(rect[B]) +1 \ 2 +#place(rect[C]) +#place(rect[D]) + +--- place-float-delta --- +#place(top + center, float: true, dx: 10pt, rect[I]) +A +#place(bottom + center, float: true, dx: -10pt, rect[II]) + +--- place-float-flow-size --- +#set page(width: auto, height: auto) +#set place(float: true, clearance: 5pt) + +#place(bottom, rect(width: 80pt, height: 10pt)) +#place(top + center, rect(height: 20pt)) +#align(center)[A] +#pagebreak() +#align(center)[B] +#place(bottom, scope: "page", rect(height: 10pt)) + +--- place-float-flow-size-alone --- +#set page(width: auto, height: auto) +#set place(float: true, clearance: 5pt) +#place(auto)[A] + +--- place-float-fr --- +#set page(height: 120pt, columns: 2) +#set place(float: true, clearance: 10pt) +#set rect(width: 70%) + +#place(top + center, rect[I]) +#place(bottom + center, scope: "page", rect[II]) + +A +#v(1fr) +B +#colbreak() +C +#align(bottom)[D] + +--- place-float-rel-sizing --- +#set page(height: 100pt, columns: 2) +#set place(float: true, clearance: 10pt) +#set rect(width: 70%) + +#place(top + center, scope: "page", rect[I]) +#place(top + center, rect[II]) + +// This test result is not ideal: The first column takes 30% of the full page, +// while the second takes 30% of the remaining space since there is no concept +// of `full` for followup pages. +#set align(bottom) +#rect(width: 100%, height: 30%) +#rect(width: 100%, height: 30%) + +--- place-float-block-backlog --- +#set page(height: 100pt) +#v(60pt) +#place(top, float: true, rect()) +#list(.."ABCDEFGHIJ".clusters()) + +--- place-float-clearance-empty --- +// Check that we don't require space for clearance if there is no content. +#set page(height: 100pt) +#v(1fr) +#table( + columns: (1fr, 1fr), + lines(2), + [], + lines(8), + place(auto, float: true, block(width: 100%, height: 100%, fill: aqua)) +) + + +--- place-float-column-align-auto --- +#set page(height: 150pt, columns: 2) +#set place(auto, float: true, clearance: 10pt) +#set rect(width: 75%) + +#place(rect[I]) +#place(rect[II]) +#place(rect[III]) +#place(rect[IV]) + +#lines(6) + +#place(rect[V]) +#place(rect[VI]) + +--- place-float-column-queued --- +#set page(height: 100pt, columns: 2) +#set place(float: true, clearance: 10pt) +#set rect(width: 75%) +#set text(costs: (widow: 0%, orphan: 0%)) + +#lines(3) + +#place(top, rect[I]) +#place(top, rect[II]) +#place(bottom, rect[III]) + +#lines(3) + +--- place-float-twocolumn --- +#set page(height: 100pt, columns: 2) +#set place(float: true, clearance: 10pt) +#set rect(width: 70%) + +#place(top + center, scope: "page", rect[I]) +#place(top + center, rect[II]) +#lines(4) +#place(top + center, rect[III]) +#block(width: 100%, height: 70pt, fill: conifer) +#place(bottom + center, scope: "page", rect[IV]) +#place(bottom + center, rect[V]) +#v(1pt, weak: true) +#block(width: 100%, height: 60pt, fill: aqua) + +--- place-float-twocolumn-queued --- +#set page(height: 100pt, columns: 2) +#set place(float: true, scope: "page", clearance: 10pt) +#let t(align, fill) = place(top + align, rect(fill: fill, height: 25pt)) + +#t(left, aqua) +#t(center, forest) +#t(right, conifer) +#lines(7) + +--- place-float-twocolumn-align-auto --- +#set page(height: 100pt, columns: 2) +#set place(float: true, clearance: 10pt) +#set rect(width: 70%) + +#place(auto, scope: "page", rect[I]) // Should end up `top` +#lines(4) +#place(auto, scope: "page", rect[II]) // Should end up `bottom` +#lines(4) + +--- place-float-twocolumn-fits --- +#set page(height: 100pt, columns: 2) +#set place(float: true, clearance: 10pt) +#set rect(width: 70%) + +#lines(6) +#place(auto, scope: "page", rect[I]) +#lines(12, "1") + +--- place-float-twocolumn-fits-not --- +#set page(height: 100pt, columns: 2) +#set place(float: true, clearance: 10pt) +#set rect(width: 70%) + +#lines(10) +#place(auto, scope: "page", rect[I]) +#lines(10, "1") + +--- place-float-threecolumn --- +#set page(height: 100pt, columns: 3) +#set place(float: true, clearance: 10pt) +#set rect(width: 70%) + +#place(bottom + center, scope: "page", rect[I]) +#lines(21) +#place(top + center, scope: "page", rect[II]) + +--- place-float-threecolumn-block-backlog --- +#set page(height: 100pt, columns: 3) +#set place(float: true, clearance: 10pt) +#set rect(width: 70%) + +// The most important part of this test is that we get the backlog of the +// conifer (green) block right. +#place(top + center, scope: "page", rect[I]) +#block(fill: aqua, width: 100%, height: 70pt) +#block(fill: conifer, width: 100%, height: 160pt) +#place(bottom + center, scope: "page", rect[II]) +#place(top, rect(height: 40%)[III]) +#block(fill: yellow, width: 100%, height: 60pt) + +--- place-float-counter --- +#let c = counter("c") +#let cd = context c.display() + +#set page( + height: 100pt, + margin: (y: 20pt), + header: [H: #cd], + footer: [F: #cd], + columns: 2, +) + +#let t(align, scope: "column", n) = place( + align, + float: true, + scope: scope, + clearance: 10pt, + line(length: 100%) + c.update(n), +) + +#t(bottom, 6) +#cd +#t(top, 3) +#colbreak() +#cd +#t(scope: "page", bottom, 11) +#colbreak() +#cd +#t(top, 12) + +--- place-float-missing --- +// Error: 2-20 automatic positioning is only available for floating placement +// Hint: 2-20 you can enable floating placement with `place(float: true, ..)` +#place(auto)[Hello] + +--- place-float-center-horizon --- +// Error: 2-45 vertical floating placement must be `auto`, `top`, or `bottom` +#place(center + horizon, float: true)[Hello] + +--- place-float-horizon --- +// Error: 2-36 vertical floating placement must be `auto`, `top`, or `bottom` +#place(horizon, float: true)[Hello] + +--- place-float-default --- +// Error: 2-27 vertical floating placement must be `auto`, `top`, or `bottom` +#place(float: true)[Hello] + +--- place-float-right --- +// Error: 2-34 vertical floating placement must be `auto`, `top`, or `bottom` +#place(right, float: true)[Hello] + +--- place-flush --- +#set page(height: 120pt) +#let floater(align, height) = place( + align, + float: true, + rect(width: 100%, height: height), +) + +#floater(top, 30pt) +A + +#floater(bottom, 50pt) +#place.flush() +B // Should be on the second page. + +--- place-flush-figure --- +#set page(height: 120pt) +#let floater(align, height, caption) = figure( + placement: align, + caption: caption, + rect(width: 100%, height: height), +) + +#floater(top, 30pt)[I] +A + +#floater(bottom, 50pt)[II] +#place.flush() +B // Should be on the second page. + --- issue-place-base --- // Test that placement is relative to container and not itself. #set page(height: 80pt, margin: 0pt) @@ -98,3 +380,11 @@ Paragraph after float. Paragraph before place. #place(rect()) Paragraph after place. + +--- issue-2595-float-overlap --- +#set page(height: 80pt) + +1 +#place(auto, float: true, block(height: 100%, width: 100%, fill: aqua)) +#place(auto, float: true, block(height: 100%, width: 100%, fill: red)) +#lines(7) diff --git a/tests/suite/model/cite.typ b/tests/suite/model/cite.typ index ffbd3b52f..f69fe9f41 100644 --- a/tests/suite/model/cite.typ +++ b/tests/suite/model/cite.typ @@ -102,8 +102,7 @@ B #cite() #cite(). // Everything moves to the second page because we want to keep the line and // its footnotes together. -#footnote[@netwok] -#footnote[A] +#footnote[@netwok \ A] #show bibliography: none #bibliography("/assets/bib/works.bib") diff --git a/tests/suite/model/figure.typ b/tests/suite/model/figure.typ index 13e944811..d71d92e3e 100644 --- a/tests/suite/model/figure.typ +++ b/tests/suite/model/figure.typ @@ -41,6 +41,42 @@ We can clearly see that @fig-cylinder and caption: "A table containing images." ) +--- figure-placement --- +#set page(height: 160pt, columns: 2) +#set place(clearance: 10pt) + +#lines(4) + +#figure( + placement: auto, + scope: "page", + caption: [I], + rect(height: 15pt, width: 80%), +) + +#figure( + placement: bottom, + caption: [II], + rect(height: 15pt, width: 80%), +) + +#lines(2) + +#figure( + placement: bottom, + caption: [III], + rect(height: 25pt, width: 80%), +) + +#figure( + placement: auto, + scope: "page", + caption: [IV], + rect(width: 80%), +) + +#lines(15) + --- figure-theorem --- // Testing show rules with figures with a simple theorem display #show figure.where(kind: "theorem"): it => {