diff --git a/library/src/core/behave.rs b/library/src/core/behave.rs new file mode 100644 index 000000000..29d5dc490 --- /dev/null +++ b/library/src/core/behave.rs @@ -0,0 +1,128 @@ +//! Node interaction. + +use typst::model::{capability, Content, StyleChain, StyleVec, StyleVecBuilder}; + +/// How a node interacts with other nodes. +#[capability] +pub trait Behave: 'static + Send + Sync { + /// The node's interaction behaviour. + fn behaviour(&self) -> Behaviour; + + /// Whether this weak node is larger than a previous one and thus picked as + /// the maximum when the levels are the same. + #[allow(unused_variables)] + fn larger(&self, prev: &Content) -> bool { + false + } +} + +/// How a node interacts with other nodes in a stream. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum Behaviour { + /// A weak node which only survives when a supportive node is before and + /// after it. Furthermore, per consecutive run of weak nodes, only one + /// survives: The one with the lowest weakness level (or the larger one if + /// there is a tie). + Weak(u8), + /// A node that enables adjacent weak nodes to exist. The default. + Supportive, + /// A node that destroys adjacent weak nodes. + Destructive, + /// A node that does not interact at all with other node, having the + /// same effect as if it didn't exist. + Ignorant, +} + +/// A wrapper around a [`StyleVecBuilder`] that allows items to interact. +pub struct BehavedBuilder<'a> { + /// The internal builder. + builder: StyleVecBuilder<'a, Content>, + /// Staged weak and ignorant items that we can't yet commit to the builder. + /// The option is `Some(_)` for weak items and `None` for ignorant items. + staged: Vec<(Content, Behaviour, StyleChain<'a>)>, + /// What the last non-ignorant item was. + last: Behaviour, +} + +impl<'a> BehavedBuilder<'a> { + /// Create a new style-vec builder. + pub fn new() -> Self { + Self { + builder: StyleVecBuilder::new(), + staged: vec![], + last: Behaviour::Destructive, + } + } + + /// Whether the builder is empty. + pub fn is_empty(&self) -> bool { + self.builder.is_empty() && self.staged.is_empty() + } + + /// Push an item into the sequence. + pub fn push(&mut self, item: Content, styles: StyleChain<'a>) { + let interaction = item + .to::() + .map_or(Behaviour::Supportive, Behave::behaviour); + + match interaction { + Behaviour::Weak(level) => { + if matches!(self.last, Behaviour::Weak(_)) { + let item = item.to::().unwrap(); + let i = self.staged.iter().position(|prev| { + let Behaviour::Weak(prev_level) = prev.1 else { return false }; + level < prev_level + || (level == prev_level && item.larger(&prev.0)) + }); + let Some(i) = i else { return }; + self.staged.remove(i); + } + + if self.last != Behaviour::Destructive { + self.staged.push((item, interaction, styles)); + self.last = interaction; + } + } + Behaviour::Supportive => { + self.flush(true); + self.builder.push(item, styles); + self.last = interaction; + } + Behaviour::Destructive => { + self.flush(false); + self.builder.push(item, styles); + self.last = interaction; + } + Behaviour::Ignorant => { + self.staged.push((item, interaction, styles)); + } + } + } + + /// Iterate over the contained items. + pub fn items(&self) -> impl DoubleEndedIterator { + self.builder.items().chain(self.staged.iter().map(|(item, ..)| item)) + } + + /// Return the finish style vec and the common prefix chain. + pub fn finish(mut self) -> (StyleVec, StyleChain<'a>) { + self.flush(false); + self.builder.finish() + } + + /// Push the staged items, filtering out weak items if `supportive` is + /// false. + fn flush(&mut self, supportive: bool) { + for (item, interaction, styles) in self.staged.drain(..) { + if supportive || interaction == Behaviour::Ignorant { + self.builder.push(item, styles); + } + } + } +} + +impl<'a> Default for BehavedBuilder<'a> { + fn default() -> Self { + Self::new() + } +} diff --git a/library/src/ext.rs b/library/src/core/ext.rs similarity index 81% rename from library/src/ext.rs rename to library/src/core/ext.rs index 0735dc181..44479e9d4 100644 --- a/library/src/ext.rs +++ b/library/src/core/ext.rs @@ -1,4 +1,5 @@ -use super::*; +//! Extension traits. + use crate::prelude::*; /// Additional methods on content. @@ -33,31 +34,31 @@ pub trait ContentExt { impl ContentExt for Content { fn strong(self) -> Self { - text::StrongNode(self).pack() + crate::text::StrongNode(self).pack() } fn emph(self) -> Self { - text::EmphNode(self).pack() + crate::text::EmphNode(self).pack() } fn underlined(self) -> Self { - text::DecoNode::<{ text::UNDERLINE }>(self).pack() + crate::text::DecoNode::<{ crate::text::UNDERLINE }>(self).pack() } fn boxed(self, sizing: Axes>>) -> Self { - layout::BoxNode { sizing, child: self }.pack() + crate::layout::BoxNode { sizing, child: self }.pack() } fn aligned(self, aligns: Axes>) -> Self { - layout::AlignNode { aligns, child: self }.pack() + crate::layout::AlignNode { aligns, child: self }.pack() } fn padded(self, padding: Sides>) -> Self { - layout::PadNode { padding, child: self }.pack() + crate::layout::PadNode { padding, child: self }.pack() } fn moved(self, delta: Axes>) -> Self { - layout::MoveNode { delta, child: self }.pack() + crate::layout::MoveNode { delta, child: self }.pack() } fn filled(self, fill: Paint) -> Self { @@ -73,16 +74,16 @@ impl ContentExt for Content { pub trait StyleMapExt { /// Set a font family composed of a preferred family and existing families /// from a style chain. - fn set_family(&mut self, preferred: text::FontFamily, existing: StyleChain); + fn set_family(&mut self, preferred: crate::text::FontFamily, existing: StyleChain); } impl StyleMapExt for StyleMap { - fn set_family(&mut self, preferred: text::FontFamily, existing: StyleChain) { + fn set_family(&mut self, preferred: crate::text::FontFamily, existing: StyleChain) { self.set( - text::TextNode::FAMILY, - text::FallbackList( + crate::text::TextNode::FAMILY, + crate::text::FallbackList( std::iter::once(preferred) - .chain(existing.get(text::TextNode::FAMILY).0.iter().cloned()) + .chain(existing.get(crate::text::TextNode::FAMILY).0.iter().cloned()) .collect(), ), ); diff --git a/library/src/core/mod.rs b/library/src/core/mod.rs new file mode 100644 index 000000000..6cafa9fc8 --- /dev/null +++ b/library/src/core/mod.rs @@ -0,0 +1,7 @@ +//! Central definitions for the standard library. + +mod behave; +mod ext; + +pub use behave::*; +pub use ext::*; diff --git a/library/src/layout/columns.rs b/library/src/layout/columns.rs index 2faa6329b..ec9510d8d 100644 --- a/library/src/layout/columns.rs +++ b/library/src/layout/columns.rs @@ -104,10 +104,20 @@ pub struct ColbreakNode { pub weak: bool, } -#[node] +#[node(Behave)] impl ColbreakNode { fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { let weak = args.named("weak")?.unwrap_or(false); Ok(Self { weak }.pack()) } } + +impl Behave for ColbreakNode { + fn behaviour(&self) -> Behaviour { + if self.weak { + Behaviour::Weak(1) + } else { + Behaviour::Destructive + } + } +} diff --git a/library/src/layout/container.rs b/library/src/layout/container.rs index 22a9e02eb..20d80cbac 100644 --- a/library/src/layout/container.rs +++ b/library/src/layout/container.rs @@ -66,19 +66,25 @@ pub struct BlockNode(pub Content); impl BlockNode { /// The spacing between the previous and this block. #[property(skip)] - pub const ABOVE: VNode = VNode::weak(Em::new(1.2).into()); + pub const ABOVE: VNode = VNode::block_spacing(Em::new(1.2).into()); /// The spacing between this and the following block. #[property(skip)] - pub const BELOW: VNode = VNode::weak(Em::new(1.2).into()); + pub const BELOW: VNode = VNode::block_spacing(Em::new(1.2).into()); fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { Ok(Self(args.eat()?.unwrap_or_default()).pack()) } fn set(...) { - let spacing = args.named("spacing")?.map(VNode::weak); - styles.set_opt(Self::ABOVE, args.named("above")?.map(VNode::strong).or(spacing)); - styles.set_opt(Self::BELOW, args.named("below")?.map(VNode::strong).or(spacing)); + let spacing = args.named("spacing")?.map(VNode::block_spacing); + styles.set_opt( + Self::ABOVE, + args.named("above")?.map(VNode::block_around).or(spacing), + ); + styles.set_opt( + Self::BELOW, + args.named("below")?.map(VNode::block_around).or(spacing), + ); } } diff --git a/library/src/layout/flow.rs b/library/src/layout/flow.rs index 822d2c388..b05146c94 100644 --- a/library/src/layout/flow.rs +++ b/library/src/layout/flow.rs @@ -1,7 +1,4 @@ -use std::cmp::Ordering; - -use super::{AlignNode, PlaceNode, Spacing, VNode}; -use crate::layout::BlockNode; +use super::{AlignNode, ColbreakNode, PlaceNode, Spacing, VNode}; use crate::prelude::*; use crate::text::ParNode; @@ -10,18 +7,7 @@ use crate::text::ParNode; /// This node is reponsible for layouting both the top-level content flow and /// the contents of boxes. #[derive(Hash)] -pub struct FlowNode(pub StyleVec); - -/// A child of a flow node. -#[derive(Hash, PartialEq)] -pub enum FlowChild { - /// Vertical spacing between other children. - Spacing(VNode), - /// Arbitrary block-level content. - Block(Content), - /// A column / region break. - Colbreak, -} +pub struct FlowNode(pub StyleVec); #[node(LayoutBlock)] impl FlowNode {} @@ -33,20 +19,18 @@ impl LayoutBlock for FlowNode { regions: &Regions, styles: StyleChain, ) -> SourceResult> { - let mut layouter = FlowLayouter::new(regions, styles); + let mut layouter = FlowLayouter::new(regions); for (child, map) in self.0.iter() { let styles = map.chain(&styles); - match child { - FlowChild::Spacing(node) => { - layouter.layout_spacing(node, styles); - } - FlowChild::Block(block) => { - layouter.layout_block(world, block, styles)?; - } - FlowChild::Colbreak => { - layouter.finish_region(); - } + if let Some(&node) = child.downcast::() { + layouter.layout_spacing(node.amount, styles); + } else if child.has::() { + layouter.layout_block(world, child, styles)?; + } else if child.is::() { + layouter.finish_region(); + } else { + panic!("unexpected flow child: {child:?}"); } } @@ -61,31 +45,10 @@ impl Debug for FlowNode { } } -impl Debug for FlowChild { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self { - Self::Spacing(kind) => write!(f, "{:?}", kind), - Self::Block(block) => block.fmt(f), - Self::Colbreak => f.pad("Colbreak"), - } - } -} - -impl PartialOrd for FlowChild { - fn partial_cmp(&self, other: &Self) -> Option { - match (self, other) { - (Self::Spacing(a), Self::Spacing(b)) => a.partial_cmp(b), - _ => None, - } - } -} - /// Performs flow layout. -struct FlowLayouter<'a> { +struct FlowLayouter { /// The regions to layout children into. regions: Regions, - /// The shared styles. - shared: StyleChain<'a>, /// Whether the flow should expand to fill the region. expand: Axes, /// The full size of `regions.size` that was available before we started @@ -95,8 +58,6 @@ struct FlowLayouter<'a> { used: Size, /// The sum of fractions in the current region. fr: Fr, - /// The spacing below the last block. - below: Option, /// Spacing and layouted blocks. items: Vec, /// Finished frames for previous regions. @@ -115,9 +76,9 @@ enum FlowItem { Placed(Frame), } -impl<'a> FlowLayouter<'a> { +impl FlowLayouter { /// Create a new flow layouter. - fn new(regions: &Regions, shared: StyleChain<'a>) -> Self { + fn new(regions: &Regions) -> Self { let expand = regions.expand; let full = regions.first; @@ -127,20 +88,18 @@ impl<'a> FlowLayouter<'a> { Self { regions, - shared, expand, full, used: Size::zero(), fr: Fr::zero(), - below: None, items: vec![], finished: vec![], } } - /// Layout spacing. - fn layout_spacing(&mut self, node: &VNode, styles: StyleChain) { - match node.amount { + /// Actually layout the spacing. + fn layout_spacing(&mut self, spacing: Spacing, styles: StyleChain) { + match spacing { Spacing::Relative(v) => { // Resolve the spacing and limit it to the remaining space. let resolved = v.resolve(styles).relative_to(self.full.y); @@ -154,10 +113,6 @@ impl<'a> FlowLayouter<'a> { self.fr += v; } } - - if node.weak || node.amount.is_fractional() { - self.below = None; - } } /// Layout a block. @@ -172,19 +127,9 @@ impl<'a> FlowLayouter<'a> { self.finish_region(); } - // Add spacing between the last block and this one. - if let Some(below) = self.below.take() { - let above = styles.get(BlockNode::ABOVE); - let pick_below = (above.weak && !below.weak) || (below.amount > above.amount); - let spacing = if pick_below { below } else { above }; - self.layout_spacing(&spacing, self.shared); - } - // Placed nodes that are out of flow produce placed items which aren't // aligned later. - let mut is_placed = false; if let Some(placed) = block.downcast::() { - is_placed = true; if placed.out_of_flow() { let frame = block.layout_block(world, &self.regions, styles)?.remove(0); self.items.push(FlowItem::Placed(frame)); @@ -205,6 +150,7 @@ impl<'a> FlowLayouter<'a> { .unwrap_or(Align::Top), ); + // Layout the block itself. let frames = block.layout_block(world, &self.regions, styles)?; let len = frames.len(); for (i, frame) in frames.into_iter().enumerate() { @@ -220,10 +166,6 @@ impl<'a> FlowLayouter<'a> { } } - if !is_placed { - self.below = Some(styles.get(BlockNode::BELOW)); - } - Ok(()) } @@ -272,7 +214,6 @@ impl<'a> FlowLayouter<'a> { self.full = self.regions.first; self.used = Size::zero(); self.fr = Fr::zero(); - self.below = None; self.finished.push(output); } diff --git a/library/src/layout/mod.rs b/library/src/layout/mod.rs index e0eb431c0..d6bc91751 100644 --- a/library/src/layout/mod.rs +++ b/library/src/layout/mod.rs @@ -32,16 +32,18 @@ use typst::diag::SourceResult; use typst::frame::Frame; use typst::geom::*; use typst::model::{ - capability, Content, Node, SequenceNode, Show, StyleChain, StyleEntry, StyleVec, + capability, Content, Node, SequenceNode, Show, StyleChain, StyleEntry, StyleVecBuilder, StyledNode, Target, }; use typst::World; +use crate::core::BehavedBuilder; +use crate::prelude::*; use crate::structure::{ DescNode, DocNode, EnumNode, ListItem, ListNode, DESC, ENUM, LIST, }; use crate::text::{ - LinebreakNode, ParChild, ParNode, ParbreakNode, SmartQuoteNode, SpaceNode, TextNode, + LinebreakNode, ParNode, ParbreakNode, SmartQuoteNode, SpaceNode, TextNode, }; /// Root-level layout. @@ -468,41 +470,17 @@ impl Default for DocBuilder<'_> { /// Accepts flow content. #[derive(Default)] -struct FlowBuilder<'a>(CollapsingBuilder<'a, FlowChild>, bool); +struct FlowBuilder<'a>(BehavedBuilder<'a>, bool); impl<'a> FlowBuilder<'a> { fn accept(&mut self, content: &Content, styles: StyleChain<'a>) -> bool { - // Weak flow elements: - // Weakness | Element - // 0 | weak colbreak - // 1 | weak fractional spacing - // 2 | weak spacing - // 3 | generated weak spacing - // 4 | generated weak fractional spacing - // 5 | par spacing - let last_was_parbreak = self.1; self.1 = false; if content.is::() { self.1 = true; - } else if let Some(colbreak) = content.downcast::() { - if colbreak.weak { - self.0.weak(FlowChild::Colbreak, styles, 0); - } else { - self.0.destructive(FlowChild::Colbreak, styles); - } - } else if let Some(vertical) = content.downcast::() { - let child = FlowChild::Spacing(*vertical); - let frac = vertical.amount.is_fractional(); - if vertical.weak { - let weakness = 1 + u8::from(frac); - self.0.weak(child, styles, weakness); - } else if frac { - self.0.destructive(child, styles); - } else { - self.0.ignorant(child, styles); - } + } else if content.is::() || content.is::() { + self.0.push(content.clone(), styles); } else if content.has::() { if !last_was_parbreak { let tight = if let Some(node) = content.downcast::() { @@ -517,17 +495,16 @@ impl<'a> FlowBuilder<'a> { if tight { let leading = styles.get(ParNode::LEADING); - let spacing = VNode::weak(leading.into()); - self.0.weak(FlowChild::Spacing(spacing), styles, 1); + let spacing = VNode::list_attach(leading.into()); + self.0.push(spacing.pack(), styles); } } - let child = FlowChild::Block(content.clone()); - if content.is::() { - self.0.ignorant(child, styles); - } else { - self.0.supportive(child, styles); - } + let above = styles.get(BlockNode::ABOVE); + let below = styles.get(BlockNode::BELOW); + self.0.push(above.pack(), styles); + self.0.push(content.clone(), styles); + self.0.push(below.pack(), styles); } else { return false; } @@ -549,43 +526,22 @@ impl<'a> FlowBuilder<'a> { /// Accepts paragraph content. #[derive(Default)] -struct ParBuilder<'a>(CollapsingBuilder<'a, ParChild>); +struct ParBuilder<'a>(BehavedBuilder<'a>); impl<'a> ParBuilder<'a> { fn accept(&mut self, content: &Content, styles: StyleChain<'a>) -> bool { - // Weak par elements: - // Weakness | Element - // 0 | weak fractional spacing - // 1 | weak spacing - // 2 | space - - if content.is::() { - self.0.weak(ParChild::Text(' '.into()), styles, 2); - } else if let Some(linebreak) = content.downcast::() { - let c = if linebreak.justify { '\u{2028}' } else { '\n' }; - self.0.destructive(ParChild::Text(c.into()), styles); - } else if let Some(horizontal) = content.downcast::() { - let child = ParChild::Spacing(horizontal.amount); - let frac = horizontal.amount.is_fractional(); - if horizontal.weak { - let weakness = u8::from(!frac); - self.0.weak(child, styles, weakness); - } else if frac { - self.0.destructive(child, styles); - } else { - self.0.ignorant(child, styles); - } - } else if let Some(quote) = content.downcast::() { - self.0.supportive(ParChild::Quote { double: quote.double }, styles); - } else if let Some(text) = content.downcast::() { - self.0.supportive(ParChild::Text(text.0.clone()), styles); - } else if content.has::() { - self.0.supportive(ParChild::Inline(content.clone()), styles); - } else { - return false; + if content.is::() + || content.is::() + || content.is::() + || content.is::() + || content.is::() + || content.has::() + { + self.0.push(content.clone(), styles); + return true; } - true + false } fn finish(self, parent: &mut Builder<'a>) { @@ -600,10 +556,14 @@ impl<'a> ParBuilder<'a> { if !indent.is_zero() && children .items() - .find_map(|child| match child { - ParChild::Spacing(_) => None, - ParChild::Text(_) | ParChild::Quote { .. } => Some(true), - ParChild::Inline(_) => Some(false), + .find_map(|child| { + if child.is::() || child.is::() { + Some(true) + } else if child.has::() { + Some(false) + } else { + None + } }) .unwrap_or_default() && parent @@ -611,14 +571,10 @@ impl<'a> ParBuilder<'a> { .0 .items() .rev() - .find_map(|child| match child { - FlowChild::Spacing(_) => None, - FlowChild::Block(content) => Some(content.is::()), - FlowChild::Colbreak => Some(false), - }) - .unwrap_or_default() + .find(|child| child.has::()) + .map_or(false, |child| child.is::()) { - children.push_front(ParChild::Spacing(indent.into())); + children.push_front(HNode::strong(indent.into()).pack()); } parent.flow.accept(&ParNode(children).pack(), shared); @@ -701,115 +657,3 @@ impl Default for ListBuilder<'_> { } } } - -/// A wrapper around a [`StyleVecBuilder`] that allows to collapse items. -struct CollapsingBuilder<'a, T> { - /// The internal builder. - builder: StyleVecBuilder<'a, T>, - /// Staged weak and ignorant items that we can't yet commit to the builder. - /// The option is `Some(_)` for weak items and `None` for ignorant items. - staged: Vec<(T, StyleChain<'a>, Option)>, - /// What the last non-ignorant item was. - last: Last, -} - -/// What the last non-ignorant item was. -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -enum Last { - Weak, - Destructive, - Supportive, -} - -impl<'a, T> CollapsingBuilder<'a, T> { - /// Create a new style-vec builder. - fn new() -> Self { - Self { - builder: StyleVecBuilder::new(), - staged: vec![], - last: Last::Destructive, - } - } - - /// Whether the builder is empty. - fn is_empty(&self) -> bool { - self.builder.is_empty() && self.staged.is_empty() - } - - /// Can only exist when there is at least one supportive item to its left - /// and to its right, with no destructive items in between. There may be - /// ignorant items in between in both directions. - /// - /// Between weak items, there may be at least one per layer and among the - /// candidates the strongest one (smallest `weakness`) wins. When tied, - /// the one that compares larger through `PartialOrd` wins. - fn weak(&mut self, item: T, styles: StyleChain<'a>, weakness: u8) - where - T: PartialOrd, - { - if self.last == Last::Destructive { - return; - } - - if self.last == Last::Weak { - let weak = self.staged.iter().position(|(prev_item, _, prev_weakness)| { - prev_weakness.map_or(false, |prev_weakness| { - weakness < prev_weakness - || (weakness == prev_weakness && item > *prev_item) - }) - }); - - let Some(weak) = weak else { return }; - self.staged.remove(weak); - } - - self.staged.push((item, styles, Some(weakness))); - self.last = Last::Weak; - } - - /// Forces nearby weak items to collapse. - fn destructive(&mut self, item: T, styles: StyleChain<'a>) { - self.flush(false); - self.builder.push(item, styles); - self.last = Last::Destructive; - } - - /// Allows nearby weak items to exist. - fn supportive(&mut self, item: T, styles: StyleChain<'a>) { - self.flush(true); - self.builder.push(item, styles); - self.last = Last::Supportive; - } - - /// Has no influence on other items. - fn ignorant(&mut self, item: T, styles: StyleChain<'a>) { - self.staged.push((item, styles, None)); - } - - /// Iterate over the contained items. - fn items(&self) -> impl DoubleEndedIterator { - self.builder.items().chain(self.staged.iter().map(|(item, ..)| item)) - } - - /// Return the finish style vec and the common prefix chain. - fn finish(mut self) -> (StyleVec, StyleChain<'a>) { - self.flush(false); - self.builder.finish() - } - - /// Push the staged items, filtering out weak items if `supportive` is - /// false. - fn flush(&mut self, supportive: bool) { - for (item, styles, meta) in self.staged.drain(..) { - if supportive || meta.is_none() { - self.builder.push(item, styles); - } - } - } -} - -impl<'a, T> Default for CollapsingBuilder<'a, T> { - fn default() -> Self { - Self::new() - } -} diff --git a/library/src/layout/place.rs b/library/src/layout/place.rs index 7d760ab6e..42f7ff7d3 100644 --- a/library/src/layout/place.rs +++ b/library/src/layout/place.rs @@ -5,7 +5,7 @@ use crate::prelude::*; #[derive(Debug, Hash)] pub struct PlaceNode(pub Content); -#[node(LayoutBlock)] +#[node(LayoutBlock, Behave)] impl PlaceNode { fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { let aligns = args.find()?.unwrap_or(Axes::with_x(Some(GenAlign::Start))); @@ -54,3 +54,9 @@ impl PlaceNode { .map_or(false, |node| node.aligns.y.is_some()) } } + +impl Behave for PlaceNode { + fn behaviour(&self) -> Behaviour { + Behaviour::Ignorant + } +} diff --git a/library/src/layout/spacing.rs b/library/src/layout/spacing.rs index 66d71ed17..74bd2f0f0 100644 --- a/library/src/layout/spacing.rs +++ b/library/src/layout/spacing.rs @@ -5,11 +5,13 @@ use crate::prelude::*; /// Horizontal spacing. #[derive(Debug, Copy, Clone, Hash)] pub struct HNode { + /// The amount of horizontal spacing. pub amount: Spacing, + /// Whether the node is weak, see also [`Behaviour`]. pub weak: bool, } -#[node] +#[node(Behave)] impl HNode { fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { let amount = args.expect("spacing")?; @@ -18,31 +20,98 @@ impl HNode { } } -/// Vertical spacing. -#[derive(Debug, Copy, Clone, Hash, PartialEq, PartialOrd)] -pub struct VNode { - pub amount: Spacing, - pub weak: bool, -} - -impl VNode { - /// Create weak vertical spacing. - pub fn weak(amount: Spacing) -> Self { - Self { amount, weak: true } - } - - /// Create strong vertical spacing. +impl HNode { + /// Normal strong spacing. pub fn strong(amount: Spacing) -> Self { Self { amount, weak: false } } + + /// User-created weak spacing. + pub fn weak(amount: Spacing) -> Self { + Self { amount, weak: true } + } } -#[node] +impl Behave for HNode { + fn behaviour(&self) -> Behaviour { + if self.amount.is_fractional() { + Behaviour::Destructive + } else if self.weak { + Behaviour::Weak(1) + } else { + Behaviour::Ignorant + } + } + + fn larger(&self, prev: &Content) -> bool { + let Some(prev) = prev.downcast::() else { return false }; + self.amount > prev.amount + } +} + +/// Vertical spacing. +#[derive(Debug, Copy, Clone, Hash, PartialEq, PartialOrd)] +pub struct VNode { + /// The amount of vertical spacing. + pub amount: Spacing, + /// The node's weakness level, see also [`Behaviour`]. + pub weakness: u8, +} + +#[node(Behave)] impl VNode { fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { let amount = args.expect("spacing")?; - let weak = args.named("weak")?.unwrap_or(false); - Ok(Self { amount, weak }.pack()) + let node = if args.named("weak")?.unwrap_or(false) { + Self::weak(amount) + } else { + Self::strong(amount) + }; + Ok(node.pack()) + } +} + +impl VNode { + /// Normal strong spacing. + pub fn strong(amount: Spacing) -> Self { + Self { amount, weakness: 0 } + } + + /// User-created weak spacing. + pub fn weak(amount: Spacing) -> Self { + Self { amount, weakness: 1 } + } + + /// Weak spacing with list attach weakness. + pub fn list_attach(amount: Spacing) -> Self { + Self { amount, weakness: 2 } + } + + /// Weak spacing with BlockNode::ABOVE/BELOW weakness. + pub fn block_around(amount: Spacing) -> Self { + Self { amount, weakness: 3 } + } + + /// Weak spacing with BlockNode::SPACING weakness. + pub fn block_spacing(amount: Spacing) -> Self { + Self { amount, weakness: 4 } + } +} + +impl Behave for VNode { + fn behaviour(&self) -> Behaviour { + if self.amount.is_fractional() { + Behaviour::Destructive + } else if self.weakness > 0 { + Behaviour::Weak(self.weakness) + } else { + Behaviour::Ignorant + } + } + + fn larger(&self, prev: &Content) -> bool { + let Some(prev) = prev.downcast::() else { return false }; + self.amount > prev.amount } } diff --git a/library/src/lib.rs b/library/src/lib.rs index 7ffb490c1..6f77e0f33 100644 --- a/library/src/lib.rs +++ b/library/src/lib.rs @@ -1,6 +1,7 @@ //! Typst's standard library. pub mod base; +pub mod core; pub mod graphics; pub mod layout; pub mod math; @@ -8,8 +9,6 @@ pub mod prelude; pub mod structure; pub mod text; -mod ext; - use typst::geom::{Align, Color, Dir, GenAlign}; use typst::model::{LangItems, Node, Scope, StyleMap}; diff --git a/library/src/prelude.rs b/library/src/prelude.rs index f08604a8e..b7095aeef 100644 --- a/library/src/prelude.rs +++ b/library/src/prelude.rs @@ -1,20 +1,32 @@ //! Helpful imports for creating library functionality. +#[doc(no_inline)] pub use std::fmt::{self, Debug, Formatter}; +#[doc(no_inline)] pub use std::num::NonZeroUsize; +#[doc(no_inline)] pub use comemo::Tracked; +#[doc(no_inline)] pub use typst::diag::{bail, error, with_alternative, At, SourceResult, StrResult}; +#[doc(no_inline)] pub use typst::frame::*; +#[doc(no_inline)] pub use typst::geom::*; +#[doc(no_inline)] pub use typst::model::{ array, capability, castable, dict, dynamic, format_str, node, Args, Array, Cast, - Content, Dict, Finalize, Fold, Func, Key, Node, RecipeId, Resolve, Scope, Show, - Smart, Str, StyleChain, StyleMap, StyleVec, Value, Vm, + Content, Dict, Finalize, Fold, Func, Node, RecipeId, Resolve, Show, Smart, Str, + StyleChain, StyleMap, StyleVec, Value, Vm, }; +#[doc(no_inline)] pub use typst::syntax::{Span, Spanned}; +#[doc(no_inline)] pub use typst::util::{format_eco, EcoString}; +#[doc(no_inline)] pub use typst::World; -pub use super::ext::{ContentExt, StyleMapExt}; -pub use super::layout::{LayoutBlock, LayoutInline, Regions}; +#[doc(no_inline)] +pub use crate::core::{Behave, Behaviour, ContentExt, StyleMapExt}; +#[doc(no_inline)] +pub use crate::layout::{LayoutBlock, LayoutInline, Regions}; diff --git a/library/src/structure/heading.rs b/library/src/structure/heading.rs index 87b522f7d..e069e3a9e 100644 --- a/library/src/structure/heading.rs +++ b/library/src/structure/heading.rs @@ -50,20 +50,21 @@ impl Finalize for HeadingNode { _: StyleChain, realized: Content, ) -> SourceResult { - let size = Em::new(match self.level.get() { + let scale = match self.level.get() { 1 => 1.4, 2 => 1.2, _ => 1.0, - }); + }; - let above = Em::new(if self.level.get() == 1 { 1.8 } else { 1.44 }); - let below = Em::new(0.66); + let size = Em::new(scale); + let above = Em::new(if self.level.get() == 1 { 1.8 } else { 1.44 }) / scale; + let below = Em::new(0.66) / scale; let mut map = StyleMap::new(); map.set(TextNode::SIZE, TextSize(size.into())); map.set(TextNode::WEIGHT, FontWeight::BOLD); - map.set(BlockNode::ABOVE, VNode::strong(above.into())); - map.set(BlockNode::BELOW, VNode::strong(below.into())); + map.set(BlockNode::ABOVE, VNode::block_around(above.into())); + map.set(BlockNode::BELOW, VNode::block_around(below.into())); Ok(realized.styled_with_map(map)) } diff --git a/library/src/text/mod.rs b/library/src/text/mod.rs index c2a675471..8484ff576 100644 --- a/library/src/text/mod.rs +++ b/library/src/text/mod.rs @@ -410,20 +410,26 @@ impl Fold for FontFeatures { #[derive(Debug, Clone, Hash)] pub struct SpaceNode; -#[node] +#[node(Behave)] impl SpaceNode { fn construct(_: &mut Vm, _: &mut Args) -> SourceResult { Ok(Self.pack()) } } +impl Behave for SpaceNode { + fn behaviour(&self) -> Behaviour { + Behaviour::Weak(2) + } +} + /// A line break. #[derive(Debug, Clone, Hash)] pub struct LinebreakNode { pub justify: bool, } -#[node] +#[node(Behave)] impl LinebreakNode { fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { let justify = args.named("justify")?.unwrap_or(false); @@ -431,6 +437,12 @@ impl LinebreakNode { } } +impl Behave for LinebreakNode { + fn behaviour(&self) -> Behaviour { + Behaviour::Destructive + } +} + /// A smart quote. #[derive(Debug, Clone, Hash)] pub struct SmartQuoteNode { diff --git a/library/src/text/par.rs b/library/src/text/par.rs index b49e49146..3e72b0347 100644 --- a/library/src/text/par.rs +++ b/library/src/text/par.rs @@ -1,30 +1,19 @@ -use std::cmp::Ordering; - -use typst::util::EcoString; use unicode_bidi::{BidiInfo, Level as BidiLevel}; use unicode_script::{Script, UnicodeScript}; use xi_unicode::LineBreakIterator; -use super::{shape, Lang, Quoter, Quotes, ShapedText, TextNode}; -use crate::layout::Spacing; +use typst::model::Key; + +use super::{ + shape, Lang, LinebreakNode, Quoter, Quotes, ShapedText, SmartQuoteNode, SpaceNode, + TextNode, +}; +use crate::layout::{HNode, Spacing}; use crate::prelude::*; /// Arrange text, spacing and inline-level nodes into a paragraph. #[derive(Hash)] -pub struct ParNode(pub StyleVec); - -/// A uniformly styled atomic piece of a paragraph. -#[derive(Hash, PartialEq)] -pub enum ParChild { - /// A chunk of text. - Text(EcoString), - /// A single or double smart quote. - Quote { double: bool }, - /// Horizontal spacing between other children. - Spacing(Spacing), - /// Arbitrary inline-level content. - Inline(Content), -} +pub struct ParNode(pub StyleVec); #[node(LayoutBlock)] impl ParNode { @@ -84,26 +73,6 @@ impl Debug for ParNode { } } -impl Debug for ParChild { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self { - Self::Text(text) => write!(f, "Text({:?})", text), - Self::Quote { double } => write!(f, "Quote({double})"), - Self::Spacing(kind) => write!(f, "{:?}", kind), - Self::Inline(inline) => inline.fmt(f), - } - } -} - -impl PartialOrd for ParChild { - fn partial_cmp(&self, other: &Self) -> Option { - match (self, other) { - (Self::Spacing(a), Self::Spacing(b)) => a.partial_cmp(b), - _ => None, - } - } -} - /// A horizontal alignment. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct HorizontalAlign(pub GenAlign); @@ -426,43 +395,52 @@ fn collect<'a>( while let Some((child, map)) = iter.next() { let styles = map.chain(styles); - let segment = match child { - ParChild::Text(text) => { - let prev = full.len(); - if let Some(case) = styles.get(TextNode::CASE) { - full.push_str(&case.apply(text)); - } else { - full.push_str(text); - } - Segment::Text(full.len() - prev) + let segment = if child.is::() { + full.push(' '); + Segment::Text(1) + } else if let Some(node) = child.downcast::() { + let prev = full.len(); + if let Some(case) = styles.get(TextNode::CASE) { + full.push_str(&case.apply(&node.0)); + } else { + full.push_str(&node.0); } - &ParChild::Quote { double } => { - let prev = full.len(); - if styles.get(TextNode::SMART_QUOTES) { - let lang = styles.get(TextNode::LANG); - let region = styles.get(TextNode::REGION); - let quotes = Quotes::from_lang(lang, region); - let peeked = iter.peek().and_then(|(child, _)| match child { - ParChild::Text(text) => text.chars().next(), - ParChild::Quote { .. } => Some('"'), - ParChild::Spacing(_) => Some(SPACING_REPLACE), - ParChild::Inline(_) => Some(NODE_REPLACE), - }); + Segment::Text(full.len() - prev) + } else if let Some(node) = child.downcast::() { + let c = if node.justify { '\u{2028}' } else { '\n' }; + full.push(c); + Segment::Text(c.len_utf8()) + } else if let Some(node) = child.downcast::() { + let prev = full.len(); + if styles.get(TextNode::SMART_QUOTES) { + let lang = styles.get(TextNode::LANG); + let region = styles.get(TextNode::REGION); + let quotes = Quotes::from_lang(lang, region); + let peeked = iter.peek().and_then(|(child, _)| { + if let Some(node) = child.downcast::() { + node.0.chars().next() + } else if child.is::() { + Some('"') + } else if child.is::() || child.is::() { + Some(SPACING_REPLACE) + } else { + Some(NODE_REPLACE) + } + }); - full.push_str(quoter.quote("es, double, peeked)); - } else { - full.push(if double { '"' } else { '\'' }); - } - Segment::Text(full.len() - prev) - } - &ParChild::Spacing(spacing) => { - full.push(SPACING_REPLACE); - Segment::Spacing(spacing) - } - ParChild::Inline(inline) => { - full.push(NODE_REPLACE); - Segment::Inline(inline) + full.push_str(quoter.quote("es, node.double, peeked)); + } else { + full.push(if node.double { '"' } else { '\'' }); } + Segment::Text(full.len() - prev) + } else if let Some(&node) = child.downcast::() { + full.push(SPACING_REPLACE); + Segment::Spacing(node.amount) + } else if child.has::() { + full.push(NODE_REPLACE); + Segment::Inline(child) + } else { + panic!("unexpected par child: {child:?}"); }; if let Some(last) = full.chars().last() { @@ -608,7 +586,7 @@ fn is_compatible(a: Script, b: Script) -> bool { /// paragraph. fn shared_get<'a, K: Key<'a>>( styles: StyleChain<'a>, - children: &StyleVec, + children: &StyleVec, key: K, ) -> Option { children diff --git a/tests/ref/text/indent.png b/tests/ref/text/indent.png index deafc3f68..9cf2ace5a 100644 Binary files a/tests/ref/text/indent.png and b/tests/ref/text/indent.png differ