mirror of
https://github.com/typst/typst
synced 2025-05-13 20:46:23 +08:00
Flow collection phase (#4931)
This commit is contained in:
parent
40fcd97d58
commit
a82256c585
@ -33,7 +33,7 @@ arrayvec = "0.7.4"
|
||||
az = "1.2"
|
||||
base64 = "0.22"
|
||||
bitflags = { version = "2", features = ["serde"] }
|
||||
bumpalo = { version = "3", features = ["collections"] }
|
||||
bumpalo = { version = "3", features = ["boxed", "collections"] }
|
||||
bytemuck = "1"
|
||||
chinese-number = { version = "0.7.2", default-features = false, features = ["number-to-chinese"] }
|
||||
chrono = { version = "0.4.24", default-features = false, features = ["clock", "std"] }
|
||||
|
@ -470,7 +470,7 @@ pub struct BlockElem {
|
||||
/// The contents of the block.
|
||||
#[positional]
|
||||
#[borrowed]
|
||||
pub body: Option<BlockChild>,
|
||||
pub body: Option<BlockBody>,
|
||||
}
|
||||
|
||||
impl BlockElem {
|
||||
@ -490,7 +490,7 @@ impl BlockElem {
|
||||
) -> Self {
|
||||
Self::new()
|
||||
.with_breakable(false)
|
||||
.with_body(Some(BlockChild::SingleLayouter(
|
||||
.with_body(Some(BlockBody::SingleLayouter(
|
||||
callbacks::BlockSingleCallback::new(captured, f),
|
||||
)))
|
||||
}
|
||||
@ -506,7 +506,7 @@ impl BlockElem {
|
||||
regions: Regions,
|
||||
) -> SourceResult<Fragment>,
|
||||
) -> Self {
|
||||
Self::new().with_body(Some(BlockChild::MultiLayouter(
|
||||
Self::new().with_body(Some(BlockBody::MultiLayouter(
|
||||
callbacks::BlockMultiCallback::new(captured, f),
|
||||
)))
|
||||
}
|
||||
@ -555,7 +555,7 @@ impl Packed<BlockElem> {
|
||||
}
|
||||
|
||||
// If we have content as our body, just layout it.
|
||||
Some(BlockChild::Content(body)) => {
|
||||
Some(BlockBody::Content(body)) => {
|
||||
let mut fragment =
|
||||
layout_fragment(engine, body, locator.relayout(), styles, pod)?;
|
||||
|
||||
@ -586,7 +586,7 @@ impl Packed<BlockElem> {
|
||||
|
||||
// If we have a child that wants to layout with just access to the
|
||||
// base region, give it that.
|
||||
Some(BlockChild::SingleLayouter(callback)) => {
|
||||
Some(BlockBody::SingleLayouter(callback)) => {
|
||||
let pod = Region::new(pod.base(), pod.expand);
|
||||
callback.call(engine, locator, styles, pod).map(Fragment::frame)?
|
||||
}
|
||||
@ -597,7 +597,7 @@ impl Packed<BlockElem> {
|
||||
// For auto-sized multi-layouters, we propagate the outer expansion
|
||||
// so that they can decide for themselves. We also ensure again to
|
||||
// only expand if the size is finite.
|
||||
Some(BlockChild::MultiLayouter(callback)) => {
|
||||
Some(BlockBody::MultiLayouter(callback)) => {
|
||||
let expand = (pod.expand | regions.expand) & pod.size.map(Abs::is_finite);
|
||||
let pod = Regions { expand, ..pod };
|
||||
callback.call(engine, locator, styles, pod)?
|
||||
@ -619,7 +619,7 @@ impl Packed<BlockElem> {
|
||||
let clip = self.clip(styles);
|
||||
let has_fill_or_stroke = fill.is_some() || stroke.iter().any(Option::is_some);
|
||||
let has_inset = !inset.is_zero();
|
||||
let is_explicit = matches!(body, None | Some(BlockChild::Content(_)));
|
||||
let is_explicit = matches!(body, None | Some(BlockBody::Content(_)));
|
||||
|
||||
// Skip filling/stroking the first frame if it is empty and a non-empty
|
||||
// one follows.
|
||||
@ -787,7 +787,7 @@ impl Packed<BlockElem> {
|
||||
|
||||
/// The contents of a block.
|
||||
#[derive(Debug, Clone, PartialEq, Hash)]
|
||||
pub enum BlockChild {
|
||||
pub enum BlockBody {
|
||||
/// The block contains normal content.
|
||||
Content(Content),
|
||||
/// The block contains a layout callback that needs access to just one
|
||||
@ -798,14 +798,14 @@ pub enum BlockChild {
|
||||
MultiLayouter(callbacks::BlockMultiCallback),
|
||||
}
|
||||
|
||||
impl Default for BlockChild {
|
||||
impl Default for BlockBody {
|
||||
fn default() -> Self {
|
||||
Self::Content(Content::default())
|
||||
}
|
||||
}
|
||||
|
||||
cast! {
|
||||
BlockChild,
|
||||
BlockBody,
|
||||
self => match self {
|
||||
Self::Content(content) => content.into_value(),
|
||||
_ => Value::Auto,
|
||||
|
264
crates/typst/src/layout/flow/collect.rs
Normal file
264
crates/typst/src/layout/flow/collect.rs
Normal file
@ -0,0 +1,264 @@
|
||||
use bumpalo::boxed::Box as BumpBox;
|
||||
use bumpalo::Bump;
|
||||
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::layout::{
|
||||
layout_frame, Abs, AlignElem, Alignment, Axes, BlockElem, ColbreakElem,
|
||||
FixedAlignment, FlushElem, Fr, Fragment, Frame, PagebreakElem, PlaceElem, Ratio,
|
||||
Region, Regions, Rel, Size, 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<Abs>, 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.
|
||||
#[typst_macros::time]
|
||||
pub fn collect<'a>(
|
||||
engine: &mut Engine,
|
||||
bump: &'a Bump,
|
||||
children: &[Pair<'a>],
|
||||
locator: Locator<'a>,
|
||||
base: Size,
|
||||
expand: bool,
|
||||
) -> SourceResult<Vec<Child<'a>>> {
|
||||
let mut locator = locator.split();
|
||||
let mut output = Vec::with_capacity(children.len());
|
||||
let mut last_was_par = false;
|
||||
|
||||
for &(child, styles) in children {
|
||||
if let Some(elem) = child.to_packed::<TagElem>() {
|
||||
output.push(Child::Tag(&elem.tag));
|
||||
} else if let Some(elem) = child.to_packed::<VElem>() {
|
||||
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::<ColbreakElem>() {
|
||||
output.push(Child::Break(elem.weak(styles)));
|
||||
} else if let Some(elem) = child.to_packed::<ParElem>() {
|
||||
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);
|
||||
|
||||
let lines = crate::layout::layout_inline(
|
||||
engine,
|
||||
&elem.children,
|
||||
locator.next(&elem.span()),
|
||||
styles,
|
||||
last_was_par,
|
||||
base,
|
||||
expand,
|
||||
)?
|
||||
.into_frames();
|
||||
|
||||
output.push(Child::Rel(spacing.into(), 4));
|
||||
|
||||
// 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 {
|
||||
output.push(Child::Rel(leading.into(), 5));
|
||||
}
|
||||
|
||||
// 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()
|
||||
};
|
||||
|
||||
let child = LineChild { frame, align, need };
|
||||
output.push(Child::Line(BumpBox::new_in(child, bump)));
|
||||
}
|
||||
|
||||
output.push(Child::Rel(spacing.into(), 4));
|
||||
last_was_par = true;
|
||||
} else if let Some(elem) = child.to_packed::<BlockElem>() {
|
||||
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),
|
||||
};
|
||||
|
||||
output.push(spacing(elem.above(styles)));
|
||||
|
||||
let child = BlockChild { align, sticky, rootable, elem, styles, locator };
|
||||
output.push(Child::Block(BumpBox::new_in(child, bump)));
|
||||
|
||||
output.push(spacing(elem.below(styles)));
|
||||
last_was_par = false;
|
||||
} else if let Some(elem) = child.to_packed::<PlaceElem>() {
|
||||
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);
|
||||
|
||||
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)));
|
||||
|
||||
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 child = PlacedChild {
|
||||
float,
|
||||
clearance,
|
||||
delta,
|
||||
align_x,
|
||||
align_y,
|
||||
elem,
|
||||
styles,
|
||||
locator,
|
||||
alignment,
|
||||
};
|
||||
output.push(Child::Placed(BumpBox::new_in(child, bump)));
|
||||
} else if child.is::<FlushElem>() {
|
||||
output.push(Child::Flush);
|
||||
} else if child.is::<PagebreakElem>() {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
/// A child that encapsulates a paragraph line.
|
||||
pub struct LineChild {
|
||||
pub frame: Frame,
|
||||
pub align: Axes<FixedAlignment>,
|
||||
pub need: Abs,
|
||||
}
|
||||
|
||||
/// A child that encapsulates a prepared block.
|
||||
pub struct BlockChild<'a> {
|
||||
pub align: Axes<FixedAlignment>,
|
||||
pub sticky: bool,
|
||||
pub rootable: bool,
|
||||
elem: &'a Packed<BlockElem>,
|
||||
styles: StyleChain<'a>,
|
||||
locator: Locator<'a>,
|
||||
}
|
||||
|
||||
impl BlockChild<'_> {
|
||||
/// Build the child's frames given regions.
|
||||
pub fn layout(
|
||||
&self,
|
||||
engine: &mut Engine,
|
||||
regions: Regions,
|
||||
) -> SourceResult<Fragment> {
|
||||
let mut fragment =
|
||||
self.elem
|
||||
.layout(engine, self.locator.relayout(), self.styles, regions)?;
|
||||
|
||||
for frame in &mut fragment {
|
||||
frame.post_process(self.styles);
|
||||
}
|
||||
|
||||
Ok(fragment)
|
||||
}
|
||||
}
|
||||
|
||||
/// A child that encapsulates a prepared placed element.
|
||||
pub struct PlacedChild<'a> {
|
||||
pub float: bool,
|
||||
pub clearance: Abs,
|
||||
pub delta: Axes<Rel<Abs>>,
|
||||
pub align_x: FixedAlignment,
|
||||
pub align_y: Smart<Option<FixedAlignment>>,
|
||||
elem: &'a Packed<PlaceElem>,
|
||||
styles: StyleChain<'a>,
|
||||
locator: Locator<'a>,
|
||||
alignment: Smart<Alignment>,
|
||||
}
|
||||
|
||||
impl PlacedChild<'_> {
|
||||
/// Build the child's frame given the region's base size.
|
||||
pub fn layout(&self, engine: &mut Engine, base: Size) -> SourceResult<Frame> {
|
||||
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)
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
566
crates/typst/src/layout/flow/pages.rs
Normal file
566
crates/typst/src/layout/flow/pages.rs
Normal file
@ -0,0 +1,566 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use comemo::{Track, Tracked, TrackedMut};
|
||||
|
||||
use super::layout_flow;
|
||||
use crate::diag::SourceResult;
|
||||
use crate::engine::{Engine, Route, Sink, Traced};
|
||||
use crate::foundations::{Content, NativeElement, Resolve, Smart, StyleChain, Styles};
|
||||
use crate::introspection::{
|
||||
Counter, CounterDisplayElem, CounterKey, Introspector, Locator, LocatorLink,
|
||||
ManualPageCounter, SplitLocator, Tag, TagElem, TagKind,
|
||||
};
|
||||
use crate::layout::{
|
||||
layout_frame, Abs, AlignElem, Alignment, Axes, Binding, ColumnsElem, Dir, Frame,
|
||||
FrameItem, HAlignment, Length, OuterVAlignment, Page, PageElem, PagebreakElem, Paper,
|
||||
Parity, Point, Region, Regions, Rel, Sides, Size, VAlignment,
|
||||
};
|
||||
use crate::model::Numbering;
|
||||
use crate::realize::Pair;
|
||||
use crate::syntax::Span;
|
||||
use crate::text::TextElem;
|
||||
use crate::utils::Numeric;
|
||||
use crate::visualize::Paint;
|
||||
use crate::World;
|
||||
|
||||
/// An item in page layout.
|
||||
enum PageItem<'a> {
|
||||
/// A page run containing content. All runs will be layouted in parallel.
|
||||
Run(&'a [Pair<'a>], StyleChain<'a>, Locator<'a>),
|
||||
/// Tags in between pages. These will be prepended to the first start of
|
||||
/// the next page, or appended at the very end of the final page if there is
|
||||
/// no next page.
|
||||
Tags(&'a [Pair<'a>]),
|
||||
/// An instruction to possibly add a page to bring the page number parity to
|
||||
/// the desired state. Can only be done at the end, sequentially, because it
|
||||
/// requires knowledge of the concrete page number.
|
||||
Parity(Parity, StyleChain<'a>, Locator<'a>),
|
||||
}
|
||||
|
||||
/// A mostly finished layout for one page. Needs only knowledge of its exact
|
||||
/// page number to be finalized into a `Page`. (Because the margins can depend
|
||||
/// on the page number.)
|
||||
#[derive(Clone)]
|
||||
struct LayoutedPage {
|
||||
inner: Frame,
|
||||
margin: Sides<Abs>,
|
||||
binding: Binding,
|
||||
two_sided: bool,
|
||||
header: Option<Frame>,
|
||||
footer: Option<Frame>,
|
||||
background: Option<Frame>,
|
||||
foreground: Option<Frame>,
|
||||
fill: Smart<Option<Paint>>,
|
||||
numbering: Option<Numbering>,
|
||||
}
|
||||
|
||||
/// Layouts the document's pages.
|
||||
pub fn layout_pages<'a>(
|
||||
engine: &mut Engine,
|
||||
children: &'a mut [Pair<'a>],
|
||||
locator: SplitLocator<'a>,
|
||||
styles: StyleChain<'a>,
|
||||
) -> SourceResult<Vec<Page>> {
|
||||
// Slice up the children into logical parts.
|
||||
let items = collect_page_items(children, locator, styles);
|
||||
|
||||
// Layout the page runs in parallel.
|
||||
let mut runs = engine.parallelize(
|
||||
items.iter().filter_map(|item| match item {
|
||||
PageItem::Run(children, initial, locator) => {
|
||||
Some((children, initial, locator.relayout()))
|
||||
}
|
||||
_ => None,
|
||||
}),
|
||||
|engine, (children, initial, locator)| {
|
||||
layout_page_run(engine, children, locator, *initial)
|
||||
},
|
||||
);
|
||||
|
||||
let mut pages = vec![];
|
||||
let mut tags = vec![];
|
||||
let mut counter = ManualPageCounter::new();
|
||||
|
||||
// Collect and finalize the runs, handling things like page parity and tags
|
||||
// between pages.
|
||||
for item in &items {
|
||||
match item {
|
||||
PageItem::Run(..) => {
|
||||
let layouted = runs.next().unwrap()?;
|
||||
for layouted in layouted {
|
||||
let page = finalize_page(engine, &mut counter, &mut tags, layouted)?;
|
||||
pages.push(page);
|
||||
}
|
||||
}
|
||||
PageItem::Parity(parity, initial, locator) => {
|
||||
if !parity.matches(pages.len()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let layouted = layout_blank_page(engine, locator.relayout(), *initial)?;
|
||||
let page = finalize_page(engine, &mut counter, &mut tags, layouted)?;
|
||||
pages.push(page);
|
||||
}
|
||||
PageItem::Tags(items) => {
|
||||
tags.extend(
|
||||
items
|
||||
.iter()
|
||||
.filter_map(|(c, _)| c.to_packed::<TagElem>())
|
||||
.map(|elem| elem.tag.clone()),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add the remaining tags to the very end of the last page.
|
||||
if !tags.is_empty() {
|
||||
let last = pages.last_mut().unwrap();
|
||||
let pos = Point::with_y(last.frame.height());
|
||||
last.frame
|
||||
.push_multiple(tags.into_iter().map(|tag| (pos, FrameItem::Tag(tag))));
|
||||
}
|
||||
|
||||
Ok(pages)
|
||||
}
|
||||
|
||||
/// Slices up the children into logical parts, processing styles and handling
|
||||
/// things like tags and weak pagebreaks.
|
||||
fn collect_page_items<'a>(
|
||||
mut children: &'a mut [Pair<'a>],
|
||||
mut locator: SplitLocator<'a>,
|
||||
mut initial: StyleChain<'a>,
|
||||
) -> Vec<PageItem<'a>> {
|
||||
// The collected page-level items.
|
||||
let mut items: Vec<PageItem<'a>> = vec![];
|
||||
// When this is true, an empty page should be added to `pages` at the end.
|
||||
let mut staged_empty_page = true;
|
||||
|
||||
// The `children` are a flat list of flow-level items and pagebreaks. This
|
||||
// loops splits it up into pagebreaks and consecutive slices of
|
||||
// non-pagebreaks. From these pieces, we build page items that we can then
|
||||
// layout in parallel.
|
||||
while let Some(&(elem, styles)) = children.first() {
|
||||
if let Some(pagebreak) = elem.to_packed::<PagebreakElem>() {
|
||||
// Add a blank page if we encounter a strong pagebreak and there was
|
||||
// a staged empty page.
|
||||
let strong = !pagebreak.weak(styles);
|
||||
if strong && staged_empty_page {
|
||||
let locator = locator.next(&elem.span());
|
||||
items.push(PageItem::Run(&[], initial, locator));
|
||||
}
|
||||
|
||||
// Add an instruction to adjust the page parity if requested.
|
||||
if let Some(parity) = pagebreak.to(styles) {
|
||||
let locator = locator.next(&elem.span());
|
||||
items.push(PageItem::Parity(parity, styles, locator));
|
||||
}
|
||||
|
||||
// The initial styles for the next page are ours unless this is a
|
||||
// "boundary" pagebreak. Such a pagebreak is generated at the end of
|
||||
// the scope of a page set rule to ensure a page boundary. It's
|
||||
// styles correspond to the styles _before_ the page set rule, so we
|
||||
// don't want to apply it to a potential empty page.
|
||||
if !pagebreak.boundary(styles) {
|
||||
initial = styles;
|
||||
}
|
||||
|
||||
// Stage an empty page after a strong pagebreak.
|
||||
staged_empty_page |= strong;
|
||||
|
||||
// Advance to the next child.
|
||||
children = &mut children[1..];
|
||||
} else {
|
||||
// Find the end of the consecutive non-pagebreak run.
|
||||
let end =
|
||||
children.iter().take_while(|(c, _)| !c.is::<PagebreakElem>()).count();
|
||||
|
||||
// Migrate start tags without accompanying end tags from before a
|
||||
// pagebreak to after it.
|
||||
let end = migrate_unterminated_tags(children, end);
|
||||
if end == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Advance to the rest of the children.
|
||||
let (group, rest) = children.split_at_mut(end);
|
||||
children = rest;
|
||||
|
||||
// If all that is left now are tags, then we don't want to add a
|
||||
// page just for them (since no group would have been detected in a
|
||||
// tagless layout and tags should never affect the layout). For this
|
||||
// reason, we remember them in a `PageItem::Tags` and later insert
|
||||
// them at the _very start_ of the next page, even before the
|
||||
// header.
|
||||
//
|
||||
// We don't do this if all that's left is end boundary pagebreaks
|
||||
// and if an empty page is still staged, since then we can just
|
||||
// conceptually replace that final page with us.
|
||||
if group.iter().all(|(c, _)| c.is::<TagElem>())
|
||||
&& !(staged_empty_page
|
||||
&& children.iter().all(|&(c, s)| {
|
||||
c.to_packed::<PagebreakElem>().is_some_and(|c| c.boundary(s))
|
||||
}))
|
||||
{
|
||||
items.push(PageItem::Tags(group));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Record a page run and then disregard a staged empty page because
|
||||
// we have real content now.
|
||||
let locator = locator.next(&elem.span());
|
||||
items.push(PageItem::Run(group, initial, locator));
|
||||
staged_empty_page = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Flush a staged empty page.
|
||||
if staged_empty_page {
|
||||
items.push(PageItem::Run(&[], initial, locator.next(&())));
|
||||
}
|
||||
|
||||
items
|
||||
}
|
||||
|
||||
/// Migrates trailing start tags without accompanying end tags tags from before
|
||||
/// a pagebreak to after it. Returns the position right after the last
|
||||
/// non-migrated tag.
|
||||
///
|
||||
/// This is important because we want the positions of introspectible elements
|
||||
/// that technically started before a pagebreak, but have no visible content
|
||||
/// yet, to be after the pagebreak. A typical case where this happens is `show
|
||||
/// heading: it => pagebreak() + it`.
|
||||
fn migrate_unterminated_tags(children: &mut [Pair], mid: usize) -> usize {
|
||||
// Compute the range from before the first trailing tag to after the last
|
||||
// following pagebreak.
|
||||
let (before, after) = children.split_at(mid);
|
||||
let start = mid - before.iter().rev().take_while(|&(c, _)| c.is::<TagElem>()).count();
|
||||
let end = mid + after.iter().take_while(|&(c, _)| c.is::<PagebreakElem>()).count();
|
||||
|
||||
// Determine the set of tag locations which we won't migrate (because they
|
||||
// are terminated).
|
||||
let excluded: HashSet<_> = children[start..mid]
|
||||
.iter()
|
||||
.filter_map(|(c, _)| c.to_packed::<TagElem>())
|
||||
.filter(|elem| elem.tag.kind() == TagKind::End)
|
||||
.map(|elem| elem.tag.location())
|
||||
.collect();
|
||||
|
||||
// A key function that partitions the area of interest into three groups:
|
||||
// Excluded tags (-1) | Pagebreaks (0) | Migrated tags (1).
|
||||
let key = |(c, _): &Pair| match c.to_packed::<TagElem>() {
|
||||
Some(elem) => {
|
||||
if excluded.contains(&elem.tag.location()) {
|
||||
-1
|
||||
} else {
|
||||
1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
|
||||
// Partition the children using a *stable* sort. While it would be possible
|
||||
// to write a more efficient direct algorithm for this, the sort version is
|
||||
// less likely to have bugs and this is absolutely not on a hot path.
|
||||
children[start..end].sort_by_key(key);
|
||||
|
||||
// Compute the new end index, right before the pagebreaks.
|
||||
start + children[start..end].iter().take_while(|pair| key(pair) == -1).count()
|
||||
}
|
||||
|
||||
/// Layout a page run with uniform properties.
|
||||
#[typst_macros::time(name = "page run")]
|
||||
fn layout_page_run(
|
||||
engine: &mut Engine,
|
||||
children: &[Pair],
|
||||
locator: Locator,
|
||||
initial: StyleChain,
|
||||
) -> SourceResult<Vec<LayoutedPage>> {
|
||||
layout_page_run_impl(
|
||||
engine.world,
|
||||
engine.introspector,
|
||||
engine.traced,
|
||||
TrackedMut::reborrow_mut(&mut engine.sink),
|
||||
engine.route.track(),
|
||||
children,
|
||||
locator.track(),
|
||||
initial,
|
||||
)
|
||||
}
|
||||
|
||||
/// Layout a single page suitable for parity adjustment.
|
||||
fn layout_blank_page(
|
||||
engine: &mut Engine,
|
||||
locator: Locator,
|
||||
initial: StyleChain,
|
||||
) -> SourceResult<LayoutedPage> {
|
||||
let layouted = layout_page_run(engine, &[], locator, initial)?;
|
||||
Ok(layouted.into_iter().next().unwrap())
|
||||
}
|
||||
|
||||
/// The internal implementation of `layout_page_run`.
|
||||
#[comemo::memoize]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_page_run_impl(
|
||||
world: Tracked<dyn World + '_>,
|
||||
introspector: Tracked<Introspector>,
|
||||
traced: Tracked<Traced>,
|
||||
sink: TrackedMut<Sink>,
|
||||
route: Tracked<Route>,
|
||||
children: &[Pair],
|
||||
locator: Tracked<Locator>,
|
||||
initial: StyleChain,
|
||||
) -> SourceResult<Vec<LayoutedPage>> {
|
||||
let link = LocatorLink::new(locator);
|
||||
let mut locator = Locator::link(&link).split();
|
||||
let mut engine = Engine {
|
||||
world,
|
||||
introspector,
|
||||
traced,
|
||||
sink,
|
||||
route: Route::extend(route),
|
||||
};
|
||||
|
||||
// Determine the page-wide styles.
|
||||
let styles = determine_page_styles(children, initial);
|
||||
let styles = StyleChain::new(&styles);
|
||||
|
||||
// When one of the lengths is infinite the page fits its content along
|
||||
// that axis.
|
||||
let width = PageElem::width_in(styles).unwrap_or(Abs::inf());
|
||||
let height = PageElem::height_in(styles).unwrap_or(Abs::inf());
|
||||
let mut size = Size::new(width, height);
|
||||
if PageElem::flipped_in(styles) {
|
||||
std::mem::swap(&mut size.x, &mut size.y);
|
||||
}
|
||||
|
||||
let mut min = width.min(height);
|
||||
if !min.is_finite() {
|
||||
min = Paper::A4.width();
|
||||
}
|
||||
|
||||
// Determine the margins.
|
||||
let default = Rel::<Length>::from((2.5 / 21.0) * min);
|
||||
let margin = PageElem::margin_in(styles);
|
||||
let two_sided = margin.two_sided.unwrap_or(false);
|
||||
let margin = margin
|
||||
.sides
|
||||
.map(|side| side.and_then(Smart::custom).unwrap_or(default))
|
||||
.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);
|
||||
let header_ascent = PageElem::header_ascent_in(styles).relative_to(margin.top);
|
||||
let footer_descent = PageElem::footer_descent_in(styles).relative_to(margin.bottom);
|
||||
let numbering = PageElem::numbering_in(styles);
|
||||
let number_align = PageElem::number_align_in(styles);
|
||||
let binding =
|
||||
PageElem::binding_in(styles).unwrap_or_else(|| match TextElem::dir_in(styles) {
|
||||
Dir::LTR => Binding::Left,
|
||||
_ => Binding::Right,
|
||||
});
|
||||
|
||||
// Construct the numbering (for header or footer).
|
||||
let numbering_marginal = numbering.as_ref().map(|numbering| {
|
||||
let both = match numbering {
|
||||
Numbering::Pattern(pattern) => pattern.pieces() >= 2,
|
||||
Numbering::Func(_) => true,
|
||||
};
|
||||
|
||||
let mut counter = CounterDisplayElem::new(
|
||||
Counter::new(CounterKey::Page),
|
||||
Smart::Custom(numbering.clone()),
|
||||
both,
|
||||
)
|
||||
.pack();
|
||||
|
||||
// We interpret the Y alignment as selecting header or footer
|
||||
// and then ignore it for aligning the actual number.
|
||||
if let Some(x) = number_align.x() {
|
||||
counter = counter.aligned(x.into());
|
||||
}
|
||||
|
||||
counter
|
||||
});
|
||||
|
||||
let header = PageElem::header_in(styles);
|
||||
let footer = PageElem::footer_in(styles);
|
||||
let (header, footer) = if matches!(number_align.y(), Some(OuterVAlignment::Top)) {
|
||||
(header.as_ref().unwrap_or(&numbering_marginal), footer.as_ref().unwrap_or(&None))
|
||||
} else {
|
||||
(header.as_ref().unwrap_or(&None), footer.as_ref().unwrap_or(&numbering_marginal))
|
||||
};
|
||||
|
||||
// Layout the children.
|
||||
let bump = bumpalo::Bump::new();
|
||||
let fragment = layout_flow(
|
||||
&mut engine,
|
||||
&bump,
|
||||
children,
|
||||
&mut locator,
|
||||
styles,
|
||||
regions,
|
||||
PageElem::columns_in(styles),
|
||||
ColumnsElem::gutter_in(styles),
|
||||
Span::detached(),
|
||||
)?;
|
||||
|
||||
// Layouts a single marginal.
|
||||
let mut layout_marginal = |content: &Option<Content>, area, align| {
|
||||
let Some(content) = content else { return Ok(None) };
|
||||
let aligned = content.clone().styled(AlignElem::set_alignment(align));
|
||||
layout_frame(
|
||||
&mut engine,
|
||||
&aligned,
|
||||
locator.next(&content.span()),
|
||||
styles,
|
||||
Region::new(area, Axes::splat(true)),
|
||||
)
|
||||
.map(Some)
|
||||
};
|
||||
|
||||
// Layout marginals.
|
||||
let mut layouted = Vec::with_capacity(fragment.len());
|
||||
for inner in fragment {
|
||||
let header_size = Size::new(inner.width(), margin.top - header_ascent);
|
||||
let footer_size = Size::new(inner.width(), margin.bottom - footer_descent);
|
||||
let full_size = inner.size() + margin.sum_by_axis();
|
||||
let mid = HAlignment::Center + VAlignment::Horizon;
|
||||
layouted.push(LayoutedPage {
|
||||
inner,
|
||||
fill: fill.clone(),
|
||||
numbering: numbering.clone(),
|
||||
header: layout_marginal(header, header_size, Alignment::BOTTOM)?,
|
||||
footer: layout_marginal(footer, footer_size, Alignment::TOP)?,
|
||||
background: layout_marginal(background, full_size, mid)?,
|
||||
foreground: layout_marginal(foreground, full_size, mid)?,
|
||||
margin,
|
||||
binding,
|
||||
two_sided,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(layouted)
|
||||
}
|
||||
|
||||
/// Determines the styles used for a page run itself and page-level content like
|
||||
/// marginals and footnotes.
|
||||
///
|
||||
/// As a base, we collect the styles that are shared by all elements on the page
|
||||
/// run. As a fallback if there are no elements, we use the styles active at the
|
||||
/// pagebreak that introduced the page (at the very start, we use the default
|
||||
/// styles). Then, to produce our page styles, we filter this list of styles
|
||||
/// according to a few rules:
|
||||
///
|
||||
/// - Other styles are only kept if they are `outside && (initial || liftable)`.
|
||||
/// - "Outside" means they were not produced within a show rule or that the
|
||||
/// show rule "broke free" to the page level by emitting page styles.
|
||||
/// - "Initial" means they were active at the pagebreak that introduced the
|
||||
/// page. Since these are intuitively already active, they should be kept even
|
||||
/// if not liftable. (E.g. `text(red, page(..)`) makes the footer red.)
|
||||
/// - "Liftable" means they can be lifted to the page-level even though they
|
||||
/// weren't yet active at the very beginning. Set rule styles are liftable as
|
||||
/// opposed to direct constructor calls:
|
||||
/// - For `set page(..); set text(red)` the red text is kept even though it
|
||||
/// comes after the weak pagebreak from set page.
|
||||
/// - For `set page(..); text(red)[..]` the red isn't kept because the
|
||||
/// constructor styles are not liftable.
|
||||
fn determine_page_styles(children: &[Pair], initial: StyleChain) -> Styles {
|
||||
// Determine the shared styles (excluding tags).
|
||||
let tagless = children.iter().filter(|(c, _)| !c.is::<TagElem>()).map(|&(_, s)| s);
|
||||
let base = StyleChain::trunk(tagless).unwrap_or(initial).to_map();
|
||||
|
||||
// Determine the initial styles that are also shared by everything. We can't
|
||||
// use `StyleChain::trunk` because it currently doesn't deal with partially
|
||||
// shared links (where a subslice matches).
|
||||
let trunk_len = initial
|
||||
.to_map()
|
||||
.as_slice()
|
||||
.iter()
|
||||
.zip(base.as_slice())
|
||||
.take_while(|&(a, b)| a == b)
|
||||
.count();
|
||||
|
||||
// Filter the base styles according to our rules.
|
||||
base.into_iter()
|
||||
.enumerate()
|
||||
.filter(|(i, style)| {
|
||||
let initial = *i < trunk_len;
|
||||
style.outside() && (initial || style.liftable())
|
||||
})
|
||||
.map(|(_, style)| style)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Piece together the inner page frame and the marginals. We can only do this
|
||||
/// at the very end because inside/outside margins require knowledge of the
|
||||
/// physical page number, which is unknown during parallel layout.
|
||||
fn finalize_page(
|
||||
engine: &mut Engine,
|
||||
counter: &mut ManualPageCounter,
|
||||
tags: &mut Vec<Tag>,
|
||||
LayoutedPage {
|
||||
inner,
|
||||
mut margin,
|
||||
binding,
|
||||
two_sided,
|
||||
header,
|
||||
footer,
|
||||
background,
|
||||
foreground,
|
||||
fill,
|
||||
numbering,
|
||||
}: LayoutedPage,
|
||||
) -> SourceResult<Page> {
|
||||
// If two sided, left becomes inside and right becomes outside.
|
||||
// Thus, for left-bound pages, we want to swap on even pages and
|
||||
// for right-bound pages, we want to swap on odd pages.
|
||||
if two_sided && binding.swap(counter.physical()) {
|
||||
std::mem::swap(&mut margin.left, &mut margin.right);
|
||||
}
|
||||
|
||||
// Create a frame for the full page.
|
||||
let mut frame = Frame::hard(inner.size() + margin.sum_by_axis());
|
||||
|
||||
// Add tags.
|
||||
for tag in tags.drain(..) {
|
||||
frame.push(Point::zero(), FrameItem::Tag(tag));
|
||||
}
|
||||
|
||||
// Add the "before" marginals. The order in which we push things here is
|
||||
// important as it affects the relative ordering of introspectible elements
|
||||
// and thus how counters resolve.
|
||||
if let Some(background) = background {
|
||||
frame.push_frame(Point::zero(), background);
|
||||
}
|
||||
if let Some(header) = header {
|
||||
frame.push_frame(Point::with_x(margin.left), header);
|
||||
}
|
||||
|
||||
// Add the inner contents.
|
||||
frame.push_frame(Point::new(margin.left, margin.top), inner);
|
||||
|
||||
// Add the "after" marginals.
|
||||
if let Some(footer) = footer {
|
||||
let y = frame.height() - footer.height();
|
||||
frame.push_frame(Point::new(margin.left, y), footer);
|
||||
}
|
||||
if let Some(foreground) = foreground {
|
||||
frame.push_frame(Point::zero(), foreground);
|
||||
}
|
||||
|
||||
// Apply counter updates from within the page to the manual page counter.
|
||||
counter.visit(engine, &frame)?;
|
||||
|
||||
// Get this page's number and then bump the counter for the next page.
|
||||
let number = counter.logical();
|
||||
counter.step();
|
||||
|
||||
Ok(Page { frame, fill, numbering, number })
|
||||
}
|
@ -1,10 +1,5 @@
|
||||
use crate::diag::{bail, At, Hint, SourceResult};
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{elem, scope, Content, Packed, Smart, StyleChain};
|
||||
use crate::introspection::Locator;
|
||||
use crate::layout::{
|
||||
layout_frame, Alignment, Axes, Em, Frame, Length, Region, Rel, Size, VAlignment,
|
||||
};
|
||||
use crate::foundations::{elem, scope, Content, Smart};
|
||||
use crate::layout::{Alignment, Em, Length, Rel};
|
||||
|
||||
/// Places content at an absolute position.
|
||||
///
|
||||
@ -103,42 +98,6 @@ impl PlaceElem {
|
||||
type FlushElem;
|
||||
}
|
||||
|
||||
impl Packed<PlaceElem> {
|
||||
#[typst_macros::time(name = "place", span = self.span())]
|
||||
pub fn layout(
|
||||
&self,
|
||||
engine: &mut Engine,
|
||||
locator: Locator,
|
||||
styles: StyleChain,
|
||||
base: Size,
|
||||
) -> SourceResult<Frame> {
|
||||
// The pod is the base area of the region because for absolute
|
||||
// placement we don't really care about the already used area.
|
||||
let float = self.float(styles);
|
||||
let alignment = self.alignment(styles);
|
||||
|
||||
if float
|
||||
&& alignment.is_custom_and(|align| {
|
||||
matches!(align.y(), None | Some(VAlignment::Horizon))
|
||||
})
|
||||
{
|
||||
bail!(self.span(), "floating placement must be `auto`, `top`, or `bottom`");
|
||||
} else if !float && alignment.is_auto() {
|
||||
return Err("automatic positioning is only available for floating placement")
|
||||
.hint("you can enable floating placement with `place(float: true, ..)`")
|
||||
.at(self.span());
|
||||
}
|
||||
|
||||
let child = self
|
||||
.body()
|
||||
.clone()
|
||||
.aligned(alignment.unwrap_or_else(|| Alignment::CENTER));
|
||||
|
||||
let pod = Region::new(base, Axes::splat(false));
|
||||
layout_frame(engine, &child, locator, styles, pod)
|
||||
}
|
||||
}
|
||||
|
||||
/// Asks the layout algorithm to place pending floating elements before
|
||||
/// continuing with the content.
|
||||
///
|
||||
|
@ -29,7 +29,7 @@ use crate::foundations::{
|
||||
};
|
||||
use crate::introspection::{Introspector, Locatable, Location};
|
||||
use crate::layout::{
|
||||
BlockChild, BlockElem, Em, GridCell, GridChild, GridElem, GridItem, HElem, PadElem,
|
||||
BlockBody, BlockElem, Em, GridCell, GridChild, GridElem, GridItem, HElem, PadElem,
|
||||
Sizing, TrackSizings, VElem,
|
||||
};
|
||||
use crate::model::{
|
||||
@ -932,7 +932,7 @@ impl ElemRenderer<'_> {
|
||||
match elem.display {
|
||||
Some(Display::Block) => {
|
||||
content = BlockElem::new()
|
||||
.with_body(Some(BlockChild::Content(content)))
|
||||
.with_body(Some(BlockBody::Content(content)))
|
||||
.pack()
|
||||
.spanned(self.span);
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ use crate::introspection::{
|
||||
Count, Counter, CounterKey, CounterUpdate, Locatable, Location,
|
||||
};
|
||||
use crate::layout::{
|
||||
AlignElem, Alignment, BlockChild, BlockElem, Em, HAlignment, Length, OuterVAlignment,
|
||||
AlignElem, Alignment, BlockBody, BlockElem, Em, HAlignment, Length, OuterVAlignment,
|
||||
PlaceElem, VAlignment, VElem,
|
||||
};
|
||||
use crate::model::{Numbering, NumberingPattern, Outlinable, Refable, Supplement};
|
||||
@ -318,7 +318,7 @@ impl Show for Packed<FigureElem> {
|
||||
|
||||
// Wrap the contents in a block.
|
||||
realized = BlockElem::new()
|
||||
.with_body(Some(BlockChild::Content(realized)))
|
||||
.with_body(Some(BlockBody::Content(realized)))
|
||||
.pack()
|
||||
.spanned(self.span());
|
||||
|
||||
|
@ -10,7 +10,7 @@ use crate::introspection::{
|
||||
Count, Counter, CounterUpdate, Locatable, Locator, LocatorLink,
|
||||
};
|
||||
use crate::layout::{
|
||||
layout_frame, Abs, Axes, BlockChild, BlockElem, Em, HElem, Length, Region,
|
||||
layout_frame, Abs, Axes, BlockBody, BlockElem, Em, HElem, Length, Region,
|
||||
};
|
||||
use crate::model::{Numbering, Outlinable, ParElem, Refable, Supplement};
|
||||
use crate::text::{FontWeight, LocalName, SpaceElem, TextElem, TextSize};
|
||||
@ -257,7 +257,7 @@ impl Show for Packed<HeadingElem> {
|
||||
}
|
||||
|
||||
Ok(BlockElem::new()
|
||||
.with_body(Some(BlockChild::Content(realized)))
|
||||
.with_body(Some(BlockBody::Content(realized)))
|
||||
.pack()
|
||||
.spanned(span))
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ use crate::foundations::{
|
||||
};
|
||||
use crate::introspection::Locatable;
|
||||
use crate::layout::{
|
||||
Alignment, BlockChild, BlockElem, Em, HElem, PadElem, Spacing, VElem,
|
||||
Alignment, BlockBody, BlockElem, Em, HElem, PadElem, Spacing, VElem,
|
||||
};
|
||||
use crate::model::{CitationForm, CiteElem};
|
||||
use crate::text::{SmartQuoteElem, SmartQuotes, SpaceElem, TextElem};
|
||||
@ -185,7 +185,7 @@ impl Show for Packed<QuoteElem> {
|
||||
|
||||
if block {
|
||||
realized = BlockElem::new()
|
||||
.with_body(Some(BlockChild::Content(realized)))
|
||||
.with_body(Some(BlockBody::Content(realized)))
|
||||
.pack()
|
||||
.spanned(self.span());
|
||||
|
||||
|
@ -82,11 +82,11 @@ pub enum RealizationKind<'a> {
|
||||
#[derive(Default)]
|
||||
pub struct Arenas {
|
||||
/// A typed arena for owned content.
|
||||
content: typed_arena::Arena<Content>,
|
||||
pub content: typed_arena::Arena<Content>,
|
||||
/// A typed arena for owned styles.
|
||||
styles: typed_arena::Arena<Styles>,
|
||||
pub styles: typed_arena::Arena<Styles>,
|
||||
/// An untyped arena for everything that is `Copy`.
|
||||
bump: bumpalo::Bump,
|
||||
pub bump: bumpalo::Bump,
|
||||
}
|
||||
|
||||
/// Mutable state for realization.
|
||||
|
@ -16,7 +16,7 @@ use crate::foundations::{
|
||||
cast, elem, scope, Args, Array, Bytes, Content, Fold, NativeElement, Packed,
|
||||
PlainText, Show, ShowSet, Smart, StyleChain, Styles, Synthesize, Value,
|
||||
};
|
||||
use crate::layout::{BlockChild, BlockElem, Em, HAlignment};
|
||||
use crate::layout::{BlockBody, BlockElem, Em, HAlignment};
|
||||
use crate::model::{Figurable, ParElem};
|
||||
use crate::syntax::{split_newlines, LinkedNode, Span, Spanned};
|
||||
use crate::text::{
|
||||
@ -450,7 +450,7 @@ impl Show for Packed<RawElem> {
|
||||
// Align the text before inserting it into the block.
|
||||
realized = realized.aligned(self.align(styles).into());
|
||||
realized = BlockElem::new()
|
||||
.with_body(Some(BlockChild::Content(realized)))
|
||||
.with_body(Some(BlockBody::Content(realized)))
|
||||
.pack()
|
||||
.spanned(self.span());
|
||||
}
|
||||
|
@ -47,6 +47,7 @@ First
|
||||
--- pagebreak-in-container ---
|
||||
#box[
|
||||
// Error: 4-15 pagebreaks are not allowed inside of containers
|
||||
// Hint: 4-15 try using a `#colbreak()` instead
|
||||
#pagebreak()
|
||||
]
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user