From ae38be9097bbb32142ef776e77e627ac12379000 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Wed, 15 Dec 2021 11:11:57 +0100 Subject: [PATCH] Set Rules Episode IV: A New Fold --- src/eval/mod.rs | 11 +- src/eval/node.rs | 295 ++++++++++++++++++++++++---------------- src/eval/styles.rs | 161 +++++++++++++++------- src/eval/value.rs | 10 +- src/font.rs | 8 +- src/frame.rs | 7 +- src/geom/em.rs | 2 +- src/geom/linear.rs | 15 +- src/layout/mod.rs | 46 +++++-- src/library/align.rs | 4 +- src/library/deco.rs | 133 ------------------ src/library/document.rs | 9 +- src/library/flow.rs | 117 +++++++++------- src/library/image.rs | 6 + src/library/link.rs | 29 ++++ src/library/mod.rs | 7 +- src/library/page.rs | 57 +++++--- src/library/par.rs | 147 ++++++++++---------- src/library/shape.rs | 24 ++-- src/library/spacing.rs | 22 ++- src/library/stack.rs | 68 +++++---- src/library/text.rs | 209 +++++++++++++++++++++------- tests/typeset.rs | 24 ++-- 23 files changed, 829 insertions(+), 582 deletions(-) delete mode 100644 src/library/deco.rs create mode 100644 src/library/link.rs diff --git a/src/eval/mod.rs b/src/eval/mod.rs index d5b332806..6dcff900f 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -181,11 +181,11 @@ impl Eval for MarkupNode { Self::Linebreak => Node::Linebreak, Self::Parbreak => Node::Parbreak, Self::Strong => { - ctx.styles.set(TextNode::STRONG, !ctx.styles.get(TextNode::STRONG)); + ctx.styles.toggle(TextNode::STRONG); Node::new() } Self::Emph => { - ctx.styles.set(TextNode::EMPH, !ctx.styles.get(TextNode::EMPH)); + ctx.styles.toggle(TextNode::EMPH); Node::new() } Self::Text(text) => Node::Text(text.clone()), @@ -216,7 +216,7 @@ impl Eval for MathNode { type Output = Node; fn eval(&self, _: &mut EvalContext) -> TypResult { - let text = Node::Text(self.formula.clone()).monospaced(); + let text = Node::Text(self.formula.trim().into()).monospaced(); Ok(if self.display { Node::Block(text.into_block()) } else { @@ -229,11 +229,10 @@ impl Eval for HeadingNode { type Output = Node; fn eval(&self, ctx: &mut EvalContext) -> TypResult { - // TODO(set): Relative font size. let upscale = (1.6 - 0.1 * self.level() as f64).max(0.75); let mut styles = Styles::new(); styles.set(TextNode::STRONG, true); - styles.set(TextNode::SIZE, upscale * Length::pt(11.0)); + styles.set(TextNode::SIZE, Relative::new(upscale).into()); Ok(Node::Block( self.body().eval(ctx)?.into_block().styled(styles), )) @@ -266,7 +265,7 @@ fn labelled(_: &mut EvalContext, label: EcoString, body: Node) -> TypResult Self { - // TODO(set): Actually decorate. - self - } - /// Lift to a type-erased block-level node. pub fn into_block(self) -> PackedNode { if let Node::Block(packed) = self { packed } else { - let mut packer = NodePacker::new(); + let mut packer = NodePacker::new(true); packer.walk(self, Styles::new()); packer.into_block() } @@ -96,7 +89,7 @@ impl Node { /// Lift to a document node, the root of the layout tree. pub fn into_document(self) -> DocumentNode { - let mut packer = NodePacker::new(); + let mut packer = NodePacker::new(false); packer.walk(self, Styles::new()); packer.into_document() } @@ -117,13 +110,6 @@ impl Default for Node { } } -impl PartialEq for Node { - fn eq(&self, _: &Self) -> bool { - // TODO(set): Figure out what to do here. - false - } -} - impl Add for Node { type Output = Self; @@ -141,92 +127,89 @@ impl AddAssign for Node { /// Packs a [`Node`] into a flow or whole document. struct NodePacker { + /// Whether packing should produce a block-level node. + block: bool, /// The accumulated page nodes. - document: Vec, - /// The common style properties of all items on the current page. - page_styles: Styles, + pages: Vec, /// The accumulated flow children. flow: Vec, + /// The common style properties of all items on the current flow. + flow_styles: Styles, + /// The kind of thing that was last added to the current flow. + flow_last: Last, /// The accumulated paragraph children. par: Vec, /// The common style properties of all items in the current paragraph. par_styles: Styles, /// The kind of thing that was last added to the current paragraph. - last: Last, -} - -/// The type of the last thing that was pushed into the paragraph. -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -enum Last { - None, - Spacing, - Newline, - Space, - Other, + par_last: Last, } impl NodePacker { /// Start a new node-packing session. - fn new() -> Self { + fn new(block: bool) -> Self { Self { - document: vec![], - page_styles: Styles::new(), + block, + pages: vec![], flow: vec![], + flow_styles: Styles::new(), + flow_last: Last::None, par: vec![], par_styles: Styles::new(), - last: Last::None, + par_last: Last::None, } } /// Finish up and return the resulting flow. fn into_block(mut self) -> PackedNode { - self.parbreak(); + self.finish_par(); FlowNode(self.flow).pack() } /// Finish up and return the resulting document. fn into_document(mut self) -> DocumentNode { self.pagebreak(true); - DocumentNode(self.document) + DocumentNode(self.pages) } /// Consider a node with the given styles. fn walk(&mut self, node: Node, styles: Styles) { match node { Node::Space => { - // Only insert a space if the previous thing was actual content. - if self.last == Last::Other { - self.push_text(' '.into(), styles); - self.last = Last::Space; + if self.is_flow_compatible(&styles) && self.is_par_compatible(&styles) { + self.par_last.soft(ParChild::text(' ', styles)); } } Node::Linebreak => { - self.trim(); - self.push_text('\n'.into(), styles); - self.last = Last::Newline; + self.par_last.hard(); + self.push_inline(ParChild::text('\n', styles)); + self.par_last.hard(); } Node::Parbreak => { - self.parbreak(); + self.parbreak(Some(styles)); } Node::Pagebreak => { self.pagebreak(true); - self.page_styles = styles; + self.flow_styles = styles; } Node::Text(text) => { - self.push_text(text, styles); + self.push_inline(ParChild::text(text, styles)); } - Node::Spacing(SpecAxis::Horizontal, amount) => { - self.push_inline(ParChild::Spacing(amount), styles); - self.last = Last::Spacing; + Node::Spacing(SpecAxis::Horizontal, kind) => { + self.par_last.hard(); + self.push_inline(ParChild::Spacing(SpacingNode { kind, styles })); + self.par_last.hard(); } - Node::Spacing(SpecAxis::Vertical, amount) => { - self.push_block(FlowChild::Spacing(amount), styles); + Node::Spacing(SpecAxis::Vertical, kind) => { + self.finish_par(); + self.flow.push(FlowChild::Spacing(SpacingNode { kind, styles })); + self.flow_last.hard(); } Node::Inline(inline) => { - self.push_inline(ParChild::Node(inline), styles); + self.push_inline(ParChild::Node(inline.styled(styles))); } Node::Block(block) => { - self.push_block(FlowChild::Node(block), styles); + self.push_block(block.styled(styles)); } Node::Sequence(list) => { for (node, mut inner) in list { @@ -237,82 +220,120 @@ impl NodePacker { } } - /// Remove a trailing space. - fn trim(&mut self) { - if self.last == Last::Space { - self.par.pop(); - self.last = Last::Other; + /// Insert an inline-level element into the current paragraph. + fn push_inline(&mut self, child: ParChild) { + if let Some(child) = self.par_last.any() { + self.push_inline_impl(child); + } + + // The node must be both compatible with the current page and the + // current paragraph. + self.make_flow_compatible(child.styles()); + self.make_par_compatible(child.styles()); + self.push_inline_impl(child); + self.par_last = Last::Any; + } + + /// Push a paragraph child, coalescing text nodes with compatible styles. + fn push_inline_impl(&mut self, child: ParChild) { + if let ParChild::Text(right) = &child { + if let Some(ParChild::Text(left)) = self.par.last_mut() { + if left.styles.compatible(&right.styles, TextNode::has_property) { + left.text.push_str(&right.text); + return; + } + } + } + + self.par.push(child); + } + + /// Insert a block-level element into the current flow. + fn push_block(&mut self, node: PackedNode) { + let mut is_placed = false; + if let Some(placed) = node.downcast::() { + is_placed = true; + + // This prevents paragraph spacing after the placed node if it + // is completely out-of-flow. + if placed.out_of_flow() { + self.flow_last = Last::None; + } + } + + self.parbreak(None); + self.make_flow_compatible(&node.styles); + + if let Some(child) = self.flow_last.any() { + self.flow.push(child); + } + + self.flow.push(FlowChild::Node(node)); + self.parbreak(None); + + // This prevents paragraph spacing between the placed node and + // the paragraph below it. + if is_placed { + self.flow_last = Last::None; } } /// Advance to the next paragraph. - fn parbreak(&mut self) { - self.trim(); + fn parbreak(&mut self, break_styles: Option) { + self.finish_par(); - let children = mem::take(&mut self.par); + // Insert paragraph spacing. + self.flow_last + .soft(FlowChild::Parbreak(break_styles.unwrap_or_default())); + } + + fn finish_par(&mut self) { + let mut children = mem::take(&mut self.par); let styles = mem::take(&mut self.par_styles); + self.par_last = Last::None; + + // No empty paragraphs. if !children.is_empty() { + // Erase any styles that will be inherited anyway. + for child in &mut children { + child.styles_mut().erase(&styles); + } + + if let Some(child) = self.flow_last.any() { + self.flow.push(child); + } + // 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 node = ParNode(children).pack().styled(styles); self.flow.push(FlowChild::Node(node)); } - - self.last = Last::None; } /// Advance to the next page. fn pagebreak(&mut self, keep: bool) { - self.parbreak(); - let children = mem::take(&mut self.flow); - let styles = mem::take(&mut self.page_styles); + if self.block { + return; + } + + self.finish_par(); + + let styles = mem::take(&mut self.flow_styles); + let mut children = mem::take(&mut self.flow); + self.flow_last = Last::None; + if keep || !children.is_empty() { + // Erase any styles that will be inherited anyway. + for child in &mut children { + child.styles_mut().erase(&styles); + } + let node = PageNode { node: FlowNode(children).pack(), styles }; - self.document.push(node); + self.pages.push(node); } } - /// Insert text into the current paragraph. - fn push_text(&mut self, text: EcoString, styles: Styles) { - // TODO(set): Join compatible text nodes. Take care with space - // coalescing. - let node = TextNode { text, styles: Styles::new() }; - self.push_inline(ParChild::Text(node), styles); - } - - /// Insert an inline-level element into the current paragraph. - fn push_inline(&mut self, mut child: ParChild, styles: Styles) { - match &mut child { - ParChild::Spacing(_) => {} - ParChild::Text(node) => node.styles.apply(&styles), - ParChild::Node(node) => node.styles.apply(&styles), - ParChild::Decorate(_) => {} - ParChild::Undecorate => {} - } - - // The node must be both compatible with the current page and the - // current paragraph. - self.make_page_compatible(&styles); - self.make_par_compatible(&styles); - self.par.push(child); - self.last = Last::Other; - } - - /// Insert a block-level element into the current flow. - fn push_block(&mut self, mut child: FlowChild, styles: Styles) { - self.parbreak(); - - match &mut child { - FlowChild::Spacing(_) => {} - FlowChild::Node(node) => node.styles.apply(&styles), - } - - // The node must be compatible with the current page. - self.make_page_compatible(&styles); - self.flow.push(child); - } - /// 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: &Styles) { @@ -321,8 +342,8 @@ impl NodePacker { return; } - if !self.par_styles.compatible(&styles, ParNode::has_property) { - self.parbreak(); + if !self.is_par_compatible(styles) { + self.parbreak(None); self.par_styles = styles.clone(); return; } @@ -331,19 +352,55 @@ impl NodePacker { } /// Break to a new page if the `styles` contain page styles that are - /// incompatible with the current page. - fn make_page_compatible(&mut self, styles: &Styles) { + /// incompatible with the current flow. + fn make_flow_compatible(&mut self, styles: &Styles) { if self.flow.is_empty() && self.par.is_empty() { - self.page_styles = styles.clone(); + self.flow_styles = styles.clone(); return; } - if !self.page_styles.compatible(&styles, PageNode::has_property) { + if !self.is_flow_compatible(styles) { self.pagebreak(false); - self.page_styles = styles.clone(); + self.flow_styles = styles.clone(); return; } - self.page_styles.intersect(styles); + self.flow_styles.intersect(styles); + } + + /// Whether the given styles are compatible with the current page. + fn is_par_compatible(&self, styles: &Styles) -> bool { + self.par_styles.compatible(&styles, ParNode::has_property) + } + + /// Whether the given styles are compatible with the current flow. + fn is_flow_compatible(&self, styles: &Styles) -> bool { + self.block || self.flow_styles.compatible(&styles, PageNode::has_property) + } +} + +/// Finite state machine for spacing coalescing. +enum Last { + None, + Any, + Soft(N), +} + +impl Last { + fn any(&mut self) -> Option { + match mem::replace(self, Self::Any) { + Self::Soft(soft) => Some(soft), + _ => None, + } + } + + fn soft(&mut self, soft: N) { + if let Self::Any = self { + *self = Self::Soft(soft); + } + } + + fn hard(&mut self) { + *self = Self::None; } } diff --git a/src/eval/styles.rs b/src/eval/styles.rs index 9d2048436..555c2a619 100644 --- a/src/eval/styles.rs +++ b/src/eval/styles.rs @@ -11,7 +11,7 @@ use std::rc::Rc; /// A map of style properties. #[derive(Default, Clone, Hash)] pub struct Styles { - pub(crate) map: Vec<(StyleId, Entry)>, + map: Vec<(StyleId, Entry)>, } impl Styles { @@ -21,10 +21,7 @@ impl Styles { } /// Create a style map with a single property-value pair. - pub fn one(key: P, value: P::Value) -> Self - where - P::Value: Debug + Hash + PartialEq + 'static, - { + pub fn one(key: P, value: P::Value) -> Self { let mut styles = Self::new(); styles.set(key, value); styles @@ -36,21 +33,31 @@ impl Styles { } /// Set the value for a style property. - pub fn set(&mut self, key: P, value: P::Value) - where - P::Value: Debug + Hash + PartialEq + 'static, - { + pub fn set(&mut self, key: P, value: P::Value) { let id = StyleId::of::

(); - let entry = Entry::new(key, value); - for pair in &mut self.map { if pair.0 == id { - pair.1 = entry; + let prev = pair.1.downcast::().unwrap(); + let folded = P::combine(value, prev.clone()); + pair.1 = Entry::new(key, folded); return; } } - self.map.push((id, entry)); + self.map.push((id, Entry::new(key, value))); + } + + /// Toggle a boolean style property. + pub fn toggle>(&mut self, key: P) { + let id = StyleId::of::

(); + for (i, pair) in self.map.iter_mut().enumerate() { + if pair.0 == id { + self.map.swap_remove(i); + return; + } + } + + self.map.push((id, Entry::new(key, true))); } /// Get the value of a copyable style property. @@ -84,10 +91,15 @@ impl Styles { /// /// Properties from `self` take precedence over the ones from `outer`. pub fn apply(&mut self, outer: &Self) { - for pair in &outer.map { - if self.map.iter().all(|&(id, _)| pair.0 != id) { - self.map.push(pair.clone()); + 'outer: for pair in &outer.map { + for (id, entry) in &mut self.map { + if pair.0 == *id { + entry.apply(&pair.1); + continue 'outer; + } } + + self.map.push(pair.clone()); } } @@ -105,12 +117,18 @@ impl Styles { self.map.retain(|a| other.map.iter().any(|b| a == b)); } + /// Keep only those styles that are not also in `other`. + pub fn erase(&mut self, other: &Self) { + self.map.retain(|a| other.map.iter().all(|b| a != b)); + } + /// Whether two style maps are equal when filtered down to the given /// properties. pub fn compatible(&self, other: &Self, filter: F) -> bool where F: Fn(StyleId) -> bool, { + // TODO(set): Filtered length + one direction equal should suffice. let f = |e: &&(StyleId, Entry)| filter(e.0); self.map.iter().filter(f).all(|pair| other.map.contains(pair)) && other.map.iter().filter(f).all(|pair| self.map.contains(pair)) @@ -119,73 +137,88 @@ impl Styles { impl Debug for Styles { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.write_str("Styles ")?; - f.debug_set().entries(self.map.iter().map(|pair| &pair.1)).finish() + if f.alternate() { + for pair in &self.map { + writeln!(f, "{:#?}", pair.1)?; + } + Ok(()) + } else { + f.write_str("Styles ")?; + f.debug_set().entries(self.map.iter().map(|pair| &pair.1)).finish() + } + } +} + +impl PartialEq for Styles { + fn eq(&self, other: &Self) -> bool { + self.compatible(other, |_| true) } } /// An entry for a single style property. #[derive(Clone)] -pub(crate) struct Entry { - #[cfg(debug_assertions)] - name: &'static str, - value: Rc, -} +pub(crate) struct Entry(Rc); impl Entry { - fn new(_: P, value: P::Value) -> Self - where - P::Value: Debug + Hash + PartialEq + 'static, - { - Self { - #[cfg(debug_assertions)] - name: P::NAME, - value: Rc::new(value), - } + fn new(key: P, value: P::Value) -> Self { + Self(Rc::new((key, value))) } fn downcast(&self) -> Option<&T> { - self.value.as_any().downcast_ref() + self.0.as_any().downcast_ref() + } + + fn apply(&mut self, outer: &Self) { + *self = self.0.combine(outer); } } impl Debug for Entry { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - #[cfg(debug_assertions)] - write!(f, "{}: ", self.name)?; - write!(f, "{:?}", &self.value) + self.0.fmt(f) } } impl PartialEq for Entry { fn eq(&self, other: &Self) -> bool { - self.value.dyn_eq(other) + self.0.dyn_eq(other) } } impl Hash for Entry { fn hash(&self, state: &mut H) { - state.write_u64(self.value.hash64()); + state.write_u64(self.0.hash64()); } } -trait Bounds: Debug + 'static { +trait Bounds: 'static { fn as_any(&self) -> &dyn Any; + fn fmt(&self, f: &mut Formatter) -> fmt::Result; fn dyn_eq(&self, other: &Entry) -> bool; fn hash64(&self) -> u64; + fn combine(&self, outer: &Entry) -> Entry; } -impl Bounds for T +impl

Bounds for (P, P::Value) where - T: Debug + Hash + PartialEq + 'static, + P: Property, + P::Value: Debug + Hash + PartialEq + 'static, { fn as_any(&self) -> &dyn Any { - self + &self.1 + } + + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + if f.alternate() { + write!(f, "#[{} = {:?}]", P::NAME, self.1) + } else { + write!(f, "{}: {:?}", P::NAME, self.1) + } } fn dyn_eq(&self, other: &Entry) -> bool { - if let Some(other) = other.downcast::() { - self == other + if let Some(other) = other.downcast::() { + &self.1 == other } else { false } @@ -194,7 +227,13 @@ where fn hash64(&self) -> u64 { // No need to hash the TypeId since there's only one // valid value type per property. - fxhash::hash64(self) + fxhash::hash64(&self.1) + } + + fn combine(&self, outer: &Entry) -> Entry { + let outer = outer.downcast::().unwrap(); + let combined = P::combine(self.1.clone(), outer.clone()); + Entry::new(self.0, combined) } } @@ -202,14 +241,17 @@ where /// /// This trait is not intended to be implemented manually, but rather through /// the `properties!` macro. -pub trait Property: 'static { +pub trait Property: Copy + 'static { /// The type of this property, for example, this could be /// [`Length`](crate::geom::Length) for a `WIDTH` property. - type Value; + type Value: Debug + Clone + Hash + PartialEq + 'static; /// The name of the property, used for debug printing. const NAME: &'static str; + /// Combine the property with an outer value. + fn combine(inner: Self::Value, outer: Self::Value) -> Self::Value; + /// The default value of the property. fn default() -> Self::Value; @@ -235,7 +277,8 @@ impl StyleId { /// Generate the property keys for a node. macro_rules! properties { ($node:ty, $( - $(#[$attr:meta])* + $(#[doc = $doc:expr])* + $(#[fold($combine:expr)])? $name:ident: $type:ty = $default:expr ),* $(,)?) => { // TODO(set): Fix possible name clash. @@ -250,6 +293,13 @@ macro_rules! properties { pub struct Key(pub PhantomData); + impl Copy for Key {} + impl Clone for Key { + fn clone(&self) -> Self { + *self + } + } + impl Property for Key<$type> { type Value = $type; @@ -257,6 +307,15 @@ macro_rules! properties { stringify!($node), "::", stringify!($name) ); + #[allow(unused_mut, unused_variables)] + fn combine(mut inner: Self::Value, outer: Self::Value) -> Self::Value { + $( + let combine: fn(Self::Value, Self::Value) -> Self::Value = $combine; + inner = combine(inner, outer); + )? + inner + } + fn default() -> Self::Value { $default } @@ -275,7 +334,7 @@ macro_rules! properties { false || $(id == StyleId::of::<$name::Key<$type>>())||* } - $($(#[$attr])* pub const $name: $name::Key<$type> + $($(#[doc = $doc])* pub const $name: $name::Key<$type> = $name::Key(PhantomData);)* } } @@ -284,9 +343,9 @@ macro_rules! properties { /// Set a style property to a value if the value is `Some`. macro_rules! set { - ($ctx:expr, $target:expr => $value:expr) => { + ($styles:expr, $target:expr => $value:expr) => { if let Some(v) = $value { - $ctx.styles.set($target, v); + $styles.set($target, v); } }; } diff --git a/src/eval/value.rs b/src/eval/value.rs index c2a284eb9..2cf82a269 100644 --- a/src/eval/value.rs +++ b/src/eval/value.rs @@ -26,7 +26,7 @@ pub enum Value { Float(f64), /// A length: `12pt`, `3cm`. Length(Length), - /// An angle: `1.5rad`, `90deg`. + /// An angle: `1.5rad`, `90deg`. Angle(Angle), /// A relative value: `50%`. Relative(Relative), @@ -146,7 +146,7 @@ impl Debug for Value { Self::Str(v) => Debug::fmt(v, f), Self::Array(v) => Debug::fmt(v, f), Self::Dict(v) => Debug::fmt(v, f), - Self::Node(v) => Debug::fmt(v, f), + Self::Node(_) => f.pad("