summaryrefslogtreecommitdiff
path: root/src/eval/template.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/eval/template.rs')
-rw-r--r--src/eval/template.rs434
1 files changed, 134 insertions, 300 deletions
diff --git a/src/eval/template.rs b/src/eval/template.rs
index b15215ce..a6693c84 100644
--- a/src/eval/template.rs
+++ b/src/eval/template.rs
@@ -1,11 +1,12 @@
-use std::convert::TryFrom;
use std::fmt::Debug;
use std::hash::Hash;
use std::iter::Sum;
use std::mem;
use std::ops::{Add, AddAssign};
-use super::{Property, StyleMap, Styled};
+use typed_arena::Arena;
+
+use super::{CollapsingBuilder, Interruption, Property, StyleMap, StyleVecBuilder};
use crate::diag::StrResult;
use crate::layout::{Layout, PackedNode};
use crate::library::prelude::*;
@@ -50,16 +51,16 @@ pub enum Template {
Horizontal(SpacingKind),
/// Plain text.
Text(EcoString),
- /// An inline node.
+ /// An inline-level node.
Inline(PackedNode),
/// A paragraph break.
Parbreak,
+ /// A column break.
+ Colbreak,
/// Vertical spacing.
Vertical(SpacingKind),
- /// A block node.
+ /// A block-level node.
Block(PackedNode),
- /// A column break.
- Colbreak,
/// A page break.
Pagebreak,
/// A page node.
@@ -95,12 +96,11 @@ impl Template {
/// Layout this template into a collection of pages.
pub fn layout(&self, ctx: &mut Context) -> Vec<Arc<Frame>> {
let (mut ctx, styles) = LayoutContext::new(ctx);
- let mut packer = Packer::new(true);
- packer.walk(self.clone(), StyleMap::new());
- packer
- .into_root()
+ let (pages, shared) = Builder::build_pages(self);
+ let styles = shared.chain(&styles);
+ pages
.iter()
- .flat_map(|styled| styled.item.layout(&mut ctx, styled.map.chain(&styles)))
+ .flat_map(|(page, map)| page.layout(&mut ctx, map.chain(&styles)))
.collect()
}
@@ -159,13 +159,13 @@ impl Debug for Template {
f.write_str(")")
}
Self::Parbreak => f.pad("Parbreak"),
+ Self::Colbreak => f.pad("Colbreak"),
Self::Vertical(kind) => write!(f, "Vertical({kind:?})"),
Self::Block(node) => {
f.write_str("Block(")?;
node.fmt(f)?;
f.write_str(")")
}
- Self::Colbreak => f.pad("Colbreak"),
Self::Pagebreak => f.pad("Pagebreak"),
Self::Page(page) => page.fmt(f),
Self::Styled(sub, map) => {
@@ -220,338 +220,172 @@ impl Layout for Template {
regions: &Regions,
styles: StyleChain,
) -> Vec<Constrained<Arc<Frame>>> {
- let mut packer = Packer::new(false);
- packer.walk(self.clone(), StyleMap::new());
- packer.into_block().layout(ctx, regions, styles)
+ let (flow, shared) = Builder::build_flow(self);
+ flow.layout(ctx, regions, shared.chain(&styles))
}
fn pack(self) -> PackedNode {
- if let Template::Block(packed) = self {
- packed
- } else {
- PackedNode::new(self)
+ match self {
+ Template::Block(node) => node,
+ other => PackedNode::new(other),
}
}
}
-/// Packs a [`Template`] into a flow or root node.
-struct Packer {
- /// Whether this packer produces a root node.
- top: bool,
- /// The accumulated page nodes.
- pages: Vec<Styled<PageNode>>,
- /// The accumulated flow children.
- flow: Builder<Styled<FlowChild>>,
- /// The accumulated paragraph children.
- par: Builder<Styled<ParChild>>,
+/// Builds a flow or page nodes from a template.
+struct Builder<'a> {
+ /// An arena where intermediate style chains are stored.
+ arena: &'a Arena<StyleChain<'a>>,
+ /// The already built page runs.
+ pages: Option<StyleVecBuilder<'a, PageNode>>,
+ /// The currently built flow.
+ flow: CollapsingBuilder<'a, FlowChild>,
+ /// The currently built paragraph.
+ par: CollapsingBuilder<'a, ParChild>,
+ /// Whether to keep the next page even if it is empty.
+ keep_next: bool,
}
-impl Packer {
- /// Start a new template-packing session.
- fn new(top: bool) -> Self {
- Self {
- top,
- pages: vec![],
- flow: Builder::default(),
- par: Builder::default(),
- }
+impl<'a> Builder<'a> {
+ /// Build page runs from a template.
+ fn build_pages(template: &Template) -> (StyleVec<PageNode>, StyleMap) {
+ let arena = Arena::new();
+
+ let mut builder = Builder::prepare(&arena, true);
+ builder.process(template, StyleChain::default());
+ builder.finish_page(true, false, StyleChain::default());
+
+ let (pages, shared) = builder.pages.unwrap().finish();
+ (pages, shared.to_map())
}
- /// Finish up and return the resulting flow.
- fn into_block(mut self) -> PackedNode {
- self.parbreak(None, false);
- FlowNode(self.flow.children).pack()
+ /// Build a subflow from a template.
+ fn build_flow(template: &Template) -> (FlowNode, StyleMap) {
+ let arena = Arena::new();
+
+ let mut builder = Builder::prepare(&arena, false);
+ builder.process(template, StyleChain::default());
+ builder.finish_par();
+
+ let (flow, shared) = builder.flow.finish();
+ (FlowNode(flow), shared.to_map())
}
- /// Finish up and return the resulting root node.
- fn into_root(mut self) -> Vec<Styled<PageNode>> {
- self.pagebreak();
- self.pages
+ /// Prepare the builder.
+ fn prepare(arena: &'a Arena<StyleChain<'a>>, top: bool) -> Self {
+ Self {
+ arena,
+ pages: top.then(|| StyleVecBuilder::new()),
+ flow: CollapsingBuilder::new(),
+ par: CollapsingBuilder::new(),
+ keep_next: true,
+ }
}
- /// Consider a template with the given styles.
- fn walk(&mut self, template: Template, styles: StyleMap) {
+ /// Process a template.
+ fn process(&mut self, template: &'a Template, styles: StyleChain<'a>) {
match template {
Template::Space => {
- // A text space is "soft", meaning that it can be eaten up by
- // adjacent line breaks or explicit spacings.
- self.par.last.soft(Styled::new(ParChild::text(' '), styles), false);
+ self.par.weak(ParChild::Text(' '.into()), styles);
}
Template::Linebreak => {
- // A line break eats up surrounding text spaces.
- self.par.last.hard();
- self.push_inline(Styled::new(ParChild::text('\n'), styles));
- self.par.last.hard();
+ self.par.destructive(ParChild::Text('\n'.into()), styles);
}
- Template::Parbreak => {
- // An explicit paragraph break is styled according to the active
- // styles (`Some(_)`) whereas paragraph breaks forced by
- // incompatibility take their styles from the preceding
- // paragraph.
- self.parbreak(Some(styles), true);
- }
- Template::Colbreak => {
- // Explicit column breaks end the current paragraph and then
- // discards the paragraph break.
- self.parbreak(None, false);
- self.make_flow_compatible(&styles);
- self.flow.children.push(Styled::new(FlowChild::Skip, styles));
- self.flow.last.hard();
- }
- Template::Pagebreak => {
- // We must set the flow styles after the page break such that an
- // empty page created by two page breaks in a row has styles at
- // all.
- self.pagebreak();
- self.flow.styles = styles;
+ Template::Horizontal(kind) => {
+ let child = ParChild::Spacing(*kind);
+ if kind.is_fractional() {
+ self.par.destructive(child, styles);
+ } else {
+ self.par.ignorant(child, styles);
+ }
}
Template::Text(text) => {
- self.push_inline(Styled::new(ParChild::text(text), styles));
+ self.par.supportive(ParChild::Text(text.clone()), styles);
}
- Template::Horizontal(kind) => {
- // Just like a line break, explicit horizontal spacing eats up
- // surrounding text spaces.
- self.par.last.hard();
- self.push_inline(Styled::new(ParChild::Spacing(kind), styles));
- self.par.last.hard();
+ Template::Inline(node) => {
+ self.par.supportive(ParChild::Node(node.clone()), styles);
}
- Template::Vertical(kind) => {
- // Explicit vertical spacing ends the current paragraph and then
- // discards the paragraph break.
- self.parbreak(None, false);
- self.make_flow_compatible(&styles);
- self.flow.children.push(Styled::new(FlowChild::Spacing(kind), styles));
- self.flow.last.hard();
+ Template::Parbreak => {
+ self.finish_par();
+ self.flow.weak(FlowChild::Parbreak, styles);
}
- Template::Inline(inline) => {
- self.push_inline(Styled::new(ParChild::Node(inline), styles));
+ Template::Colbreak => {
+ self.finish_par();
+ self.flow.destructive(FlowChild::Colbreak, styles);
}
- Template::Block(block) => {
- self.push_block(Styled::new(block, styles));
+ Template::Vertical(kind) => {
+ self.finish_par();
+ let child = FlowChild::Spacing(*kind);
+ if kind.is_fractional() {
+ self.flow.destructive(child, styles);
+ } else {
+ self.flow.ignorant(child, styles);
+ }
}
- Template::Page(page) => {
- if self.top {
- self.pagebreak();
- self.pages.push(Styled::new(page, styles));
+ Template::Block(node) => {
+ self.finish_par();
+ let child = FlowChild::Node(node.clone());
+ if node.is::<PlaceNode>() {
+ self.flow.ignorant(child, styles);
} else {
- self.push_block(Styled::new(page.0, styles));
+ self.flow.supportive(child, styles);
}
}
- Template::Styled(template, mut map) => {
- map.apply(&styles);
- self.walk(*template, map);
+ Template::Pagebreak => {
+ self.finish_page(true, true, styles);
}
- Template::Sequence(seq) => {
- // For a list of templates, we apply the list's styles to each
- // templates individually.
- for item in seq {
- self.walk(item, styles.clone());
+ Template::Page(page) => {
+ self.finish_page(false, false, styles);
+ if let Some(pages) = &mut self.pages {
+ pages.push(page.clone(), styles);
}
}
- }
- }
-
- /// Insert an inline-level element into the current paragraph.
- fn push_inline(&mut self, child: Styled<ParChild>) {
- // The child's map must be both compatible with the current page and the
- // current paragraph.
- self.make_flow_compatible(&child.map);
- self.make_par_compatible(&child.map);
-
- if let Some(styled) = self.par.last.any() {
- self.push_coalescing(styled);
- }
-
- self.push_coalescing(child);
- self.par.last.any();
- }
-
- /// Push a paragraph child, coalescing text nodes with compatible styles.
- fn push_coalescing(&mut self, child: Styled<ParChild>) {
- if let ParChild::Text(right) = &child.item {
- if let Some(Styled { item: ParChild::Text(left), map }) =
- self.par.children.last_mut()
- {
- if child.map.compatible::<TextNode>(map) {
- left.0.push_str(&right.0);
- return;
+ Template::Styled(sub, map) => {
+ let interruption = map.interruption();
+ match interruption {
+ Some(Interruption::Page) => self.finish_page(false, true, styles),
+ Some(Interruption::Par) => self.finish_par(),
+ None => {}
}
- }
- }
- self.par.children.push(child);
- }
-
- /// Insert a block-level element into the current flow.
- fn push_block(&mut self, node: Styled<PackedNode>) {
- let placed = node.item.is::<PlaceNode>();
-
- self.parbreak(Some(node.map.clone()), false);
- self.make_flow_compatible(&node.map);
- self.flow.children.extend(self.flow.last.any());
- self.flow.children.push(node.map(FlowChild::Node));
- self.parbreak(None, false);
-
- // Prevent paragraph spacing between the placed node and the paragraph
- // below it.
- if placed {
- self.flow.last.hard();
- }
- }
+ let outer = self.arena.alloc(styles);
+ let styles = map.chain(outer);
+ self.process(sub, styles);
- /// Advance to the next paragraph.
- fn parbreak(&mut self, break_styles: Option<StyleMap>, important: bool) {
- // Erase any styles that will be inherited anyway.
- let Builder { mut children, styles, .. } = mem::take(&mut self.par);
- for Styled { map, .. } in &mut children {
- map.erase(&styles);
- }
-
- // We don't want empty paragraphs.
- if !children.is_empty() {
- // The paragraph's children are all compatible with the page, so the
- // paragraph is too, meaning we don't need to check or intersect
- // anything here.
- let par = ParNode(children).pack();
- self.flow.children.extend(self.flow.last.any());
- self.flow.children.push(Styled::new(FlowChild::Node(par), styles));
- }
-
- // Actually styled breaks have precedence over whatever was before.
- if break_styles.is_some() {
- if let Last::Soft(_, false) = self.flow.last {
- self.flow.last = Last::Any;
+ match interruption {
+ Some(Interruption::Page) => self.finish_page(true, false, styles),
+ Some(Interruption::Par) => self.finish_par(),
+ None => {}
+ }
}
- }
-
- // For explicit paragraph breaks, `break_styles` is already `Some(_)`.
- // For page breaks due to incompatibility, we fall back to the styles
- // of the preceding thing.
- let break_styles = break_styles
- .or_else(|| self.flow.children.last().map(|styled| styled.map.clone()))
- .unwrap_or_default();
-
- // Insert paragraph spacing.
- self.flow
- .last
- .soft(Styled::new(FlowChild::Break, break_styles), important);
- }
-
- /// Advance to the next page.
- fn pagebreak(&mut self) {
- if self.top {
- self.parbreak(None, false);
-
- // Take the flow and erase any styles that will be inherited anyway.
- let Builder { mut children, styles, .. } = mem::take(&mut self.flow);
- for Styled { map, .. } in &mut children {
- map.erase(&styles);
+ Template::Sequence(seq) => {
+ for sub in seq {
+ self.process(sub, styles);
+ }
}
-
- let flow = FlowNode(children).pack();
- self.pages.push(Styled::new(PageNode(flow), styles));
- }
- }
-
- /// Break to a new paragraph if the `styles` contain paragraph styles that
- /// are incompatible with the current paragraph.
- fn make_par_compatible(&mut self, styles: &StyleMap) {
- if self.par.children.is_empty() {
- self.par.styles = styles.clone();
- return;
- }
-
- if !self.par.styles.compatible::<ParNode>(styles) {
- self.parbreak(Some(styles.clone()), false);
- self.par.styles = styles.clone();
- return;
}
-
- self.par.styles.intersect(styles);
}
- /// Break to a new page if the `styles` contain page styles that are
- /// incompatible with the current flow.
- fn make_flow_compatible(&mut self, styles: &StyleMap) {
- if self.flow.children.is_empty() && self.par.children.is_empty() {
- self.flow.styles = styles.clone();
- return;
- }
-
- if self.top && !self.flow.styles.compatible::<PageNode>(styles) {
- self.pagebreak();
- self.flow.styles = styles.clone();
- return;
+ /// Finish the currently built paragraph.
+ fn finish_par(&mut self) {
+ let (par, shared) = mem::take(&mut self.par).finish();
+ if !par.is_empty() {
+ let node = ParNode(par).pack();
+ self.flow.supportive(FlowChild::Node(node), shared);
}
-
- self.flow.styles.intersect(styles);
}
-}
-/// Container for building a flow or paragraph.
-struct Builder<T> {
- /// The intersection of the style properties of all `children`.
- styles: StyleMap,
- /// The accumulated flow or paragraph children.
- children: Vec<T>,
- /// The kind of thing that was last added.
- last: Last<T>,
-}
-
-impl<T> Default for Builder<T> {
- fn default() -> Self {
- Self {
- styles: StyleMap::new(),
- children: vec![],
- last: Last::None,
- }
- }
-}
-
-/// The kind of child that was last added to a flow or paragraph. A small finite
-/// state machine used to coalesce spaces.
-///
-/// Soft children can only exist when surrounded by `Any` children. Not at the
-/// start, end or next to hard children. This way, spaces at start and end of
-/// paragraphs and next to `#h(..)` goes away.
-enum Last<N> {
- /// Start state, nothing there.
- None,
- /// Text or a block node or something.
- Any,
- /// Hard children: Linebreaks and explicit spacing.
- Hard,
- /// Soft children: Word spaces and paragraph breaks. These are saved here
- /// temporarily and then applied once an `Any` child appears. The boolean
- /// says whether this soft child is "important" and preferrable to other soft
- /// nodes (that is the case for explicit paragraph breaks).
- Soft(N, bool),
-}
-
-impl<N> Last<N> {
- /// Transition into the `Any` state and return a soft child to really add
- /// now if currently in `Soft` state.
- fn any(&mut self) -> Option<N> {
- match mem::replace(self, Self::Any) {
- Self::Soft(soft, _) => Some(soft),
- _ => None,
- }
- }
-
- /// Transition into the `Soft` state, but only if in `Any`. Otherwise, the
- /// soft child is discarded.
- fn soft(&mut self, soft: N, important: bool) {
- if matches!(
- (&self, important),
- (Self::Any, _) | (Self::Soft(_, false), true)
- ) {
- *self = Self::Soft(soft, important);
+ /// Finish the currently built page run.
+ fn finish_page(&mut self, keep_last: bool, keep_next: bool, styles: StyleChain<'a>) {
+ self.finish_par();
+ if let Some(pages) = &mut self.pages {
+ let (flow, shared) = mem::take(&mut self.flow).finish();
+ if !flow.is_empty() || (keep_last && self.keep_next) {
+ let styles = if flow.is_empty() { styles } else { shared };
+ let node = PageNode(FlowNode(flow).pack());
+ pages.push(node, styles);
+ }
}
- }
-
- /// Transition into the `Hard` state, discarding a possibly existing soft
- /// child and preventing further soft nodes from being added.
- fn hard(&mut self) {
- *self = Self::Hard;
+ self.keep_next = keep_next;
}
}