summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--library/src/core/behave.rs128
-rw-r--r--library/src/core/ext.rs (renamed from library/src/ext.rs)27
-rw-r--r--library/src/core/mod.rs7
-rw-r--r--library/src/layout/columns.rs12
-rw-r--r--library/src/layout/container.rs16
-rw-r--r--library/src/layout/flow.rs95
-rw-r--r--library/src/layout/mod.rs228
-rw-r--r--library/src/layout/place.rs8
-rw-r--r--library/src/layout/spacing.rs95
-rw-r--r--library/src/lib.rs3
-rw-r--r--library/src/prelude.rs20
-rw-r--r--library/src/structure/heading.rs13
-rw-r--r--library/src/text/mod.rs16
-rw-r--r--library/src/text/par.rs128
-rw-r--r--tests/ref/text/indent.pngbin46495 -> 46139 bytes
15 files changed, 405 insertions, 391 deletions
diff --git a/library/src/core/behave.rs b/library/src/core/behave.rs
new file mode 100644
index 00000000..29d5dc49
--- /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::<dyn Behave>()
+ .map_or(Behaviour::Supportive, Behave::behaviour);
+
+ match interaction {
+ Behaviour::Weak(level) => {
+ if matches!(self.last, Behaviour::Weak(_)) {
+ let item = item.to::<dyn Behave>().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<Item = &Content> {
+ 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<Content>, 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
index 0735dc18..44479e9d 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<Option<Rel<Length>>>) -> Self {
- layout::BoxNode { sizing, child: self }.pack()
+ crate::layout::BoxNode { sizing, child: self }.pack()
}
fn aligned(self, aligns: Axes<Option<GenAlign>>) -> Self {
- layout::AlignNode { aligns, child: self }.pack()
+ crate::layout::AlignNode { aligns, child: self }.pack()
}
fn padded(self, padding: Sides<Rel<Length>>) -> Self {
- layout::PadNode { padding, child: self }.pack()
+ crate::layout::PadNode { padding, child: self }.pack()
}
fn moved(self, delta: Axes<Rel<Length>>) -> 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 00000000..6cafa9fc
--- /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 2faa6329..ec9510d8 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<Content> {
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 22a9e02e..20d80cba 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<Content> {
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 822d2c38..b05146c9 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<FlowChild>);
-
-/// 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<Content>);
#[node(LayoutBlock)]
impl FlowNode {}
@@ -33,20 +19,18 @@ impl LayoutBlock for FlowNode {
regions: &Regions,
styles: StyleChain,
) -> SourceResult<Vec<Frame>> {
- 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::<VNode>() {
+ layouter.layout_spacing(node.amount, styles);
+ } else if child.has::<dyn LayoutBlock>() {
+ layouter.layout_block(world, child, styles)?;
+ } else if child.is::<ColbreakNode>() {
+ 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<Ordering> {
- 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<bool>,
/// 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<VNode>,
/// Spacing and layouted blocks.
items: Vec<FlowItem>,
/// 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::<PlaceNode>() {
- 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 e0eb431c..d6bc9175 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::<ParbreakNode>() {
self.1 = true;
- } else if let Some(colbreak) = content.downcast::<ColbreakNode>() {
- if colbreak.weak {
- self.0.weak(FlowChild::Colbreak, styles, 0);
- } else {
- self.0.destructive(FlowChild::Colbreak, styles);
- }
- } else if let Some(vertical) = content.downcast::<VNode>() {
- 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::<VNode>() || content.is::<ColbreakNode>() {
+ self.0.push(content.clone(), styles);
} else if content.has::<dyn LayoutBlock>() {
if !last_was_parbreak {
let tight = if let Some(node) = content.downcast::<ListNode>() {
@@ -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::<PlaceNode>() {
- 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::<SpaceNode>() {
- self.0.weak(ParChild::Text(' '.into()), styles, 2);
- } else if let Some(linebreak) = content.downcast::<LinebreakNode>() {
- let c = if linebreak.justify { '\u{2028}' } else { '\n' };
- self.0.destructive(ParChild::Text(c.into()), styles);
- } else if let Some(horizontal) = content.downcast::<HNode>() {
- 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::<SmartQuoteNode>() {
- self.0.supportive(ParChild::Quote { double: quote.double }, styles);
- } else if let Some(text) = content.downcast::<TextNode>() {
- self.0.supportive(ParChild::Text(text.0.clone()), styles);
- } else if content.has::<dyn LayoutInline>() {
- self.0.supportive(ParChild::Inline(content.clone()), styles);
- } else {
- return false;
+ if content.is::<SpaceNode>()
+ || content.is::<LinebreakNode>()
+ || content.is::<HNode>()
+ || content.is::<SmartQuoteNode>()
+ || content.is::<TextNode>()
+ || content.has::<dyn LayoutInline>()
+ {
+ 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::<TextNode>() || child.is::<SmartQuoteNode>() {
+ Some(true)
+ } else if child.has::<dyn LayoutInline>() {
+ 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::<ParNode>()),
- FlowChild::Colbreak => Some(false),
- })
- .unwrap_or_default()
+ .find(|child| child.has::<dyn LayoutBlock>())
+ .map_or(false, |child| child.is::<ParNode>())
{
- 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<u8>)>,
- /// 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<Item = &T> {
- 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<T>, 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 7d760ab6..42f7ff7d 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<Content> {
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 66d71ed1..74bd2f0f 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<Content> {
let amount = args.expect("spacing")?;
@@ -18,31 +20,98 @@ impl HNode {
}
}
+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 }
+ }
+}
+
+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::<Self>() 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,
- pub weak: bool,
+ /// The node's weakness level, see also [`Behaviour`].
+ pub weakness: u8,
}
+#[node(Behave)]
impl VNode {
- /// Create weak vertical spacing.
- pub fn weak(amount: Spacing) -> Self {
- Self { amount, weak: true }
+ fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
+ let amount = args.expect("spacing")?;
+ let node = if args.named("weak")?.unwrap_or(false) {
+ Self::weak(amount)
+ } else {
+ Self::strong(amount)
+ };
+ Ok(node.pack())
}
+}
- /// Create strong vertical spacing.
+impl VNode {
+ /// Normal strong spacing.
pub fn strong(amount: Spacing) -> Self {
- Self { amount, weak: false }
+ 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 }
}
}
-#[node]
-impl VNode {
- fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
- let amount = args.expect("spacing")?;
- let weak = args.named("weak")?.unwrap_or(false);
- Ok(Self { amount, weak }.pack())
+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::<Self>() else { return false };
+ self.amount > prev.amount
}
}
diff --git a/library/src/lib.rs b/library/src/lib.rs
index 7ffb490c..6f77e0f3 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 f08604a8..b7095aee 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 87b522f7..e069e3a9 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<Content> {
- 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 c2a67547..8484ff57 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<Content> {
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<Content> {
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 b49e4914..3e72b034 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<ParChild>);
-
-/// 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<Content>);
#[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<Ordering> {
- 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)
- }
- &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),
- });
-
- full.push_str(quoter.quote(&quotes, double, peeked));
- } else {
- full.push(if double { '"' } else { '\'' });
- }
- Segment::Text(full.len() - prev)
- }
- &ParChild::Spacing(spacing) => {
- full.push(SPACING_REPLACE);
- Segment::Spacing(spacing)
+ let segment = if child.is::<SpaceNode>() {
+ full.push(' ');
+ Segment::Text(1)
+ } else if let Some(node) = child.downcast::<TextNode>() {
+ 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::Inline(inline) => {
- full.push(NODE_REPLACE);
- Segment::Inline(inline)
+ Segment::Text(full.len() - prev)
+ } else if let Some(node) = child.downcast::<LinebreakNode>() {
+ let c = if node.justify { '\u{2028}' } else { '\n' };
+ full.push(c);
+ Segment::Text(c.len_utf8())
+ } else if let Some(node) = child.downcast::<SmartQuoteNode>() {
+ 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::<TextNode>() {
+ node.0.chars().next()
+ } else if child.is::<SmartQuoteNode>() {
+ Some('"')
+ } else if child.is::<SpaceNode>() || child.is::<HNode>() {
+ Some(SPACING_REPLACE)
+ } else {
+ Some(NODE_REPLACE)
+ }
+ });
+
+ full.push_str(quoter.quote(&quotes, node.double, peeked));
+ } else {
+ full.push(if node.double { '"' } else { '\'' });
}
+ Segment::Text(full.len() - prev)
+ } else if let Some(&node) = child.downcast::<HNode>() {
+ full.push(SPACING_REPLACE);
+ Segment::Spacing(node.amount)
+ } else if child.has::<dyn LayoutInline>() {
+ 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<ParChild>,
+ children: &StyleVec<Content>,
key: K,
) -> Option<K::Output> {
children
diff --git a/tests/ref/text/indent.png b/tests/ref/text/indent.png
index deafc3f6..9cf2ace5 100644
--- a/tests/ref/text/indent.png
+++ b/tests/ref/text/indent.png
Binary files differ