summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/lib.rs5
-rw-r--r--src/library/ext.rs179
-rw-r--r--src/library/graphics/hide.rs12
-rw-r--r--src/library/graphics/image.rs10
-rw-r--r--src/library/graphics/line.rs10
-rw-r--r--src/library/graphics/shape.rs20
-rw-r--r--src/library/layout/align.rs21
-rw-r--r--src/library/layout/columns.rs12
-rw-r--r--src/library/layout/container.rs26
-rw-r--r--src/library/layout/flow.rs45
-rw-r--r--src/library/layout/grid.rs32
-rw-r--r--src/library/layout/mod.rs789
-rw-r--r--src/library/layout/pad.rs14
-rw-r--r--src/library/layout/page.rs2
-rw-r--r--src/library/layout/place.rs12
-rw-r--r--src/library/layout/spacing.rs2
-rw-r--r--src/library/layout/stack.rs42
-rw-r--r--src/library/layout/transform.rs36
-rw-r--r--src/library/math/mod.rs49
-rw-r--r--src/library/mod.rs200
-rw-r--r--src/library/prelude.rs13
-rw-r--r--src/library/raw.rs (renamed from src/model/raw.rs)2
-rw-r--r--src/library/structure/table.rs2
-rw-r--r--src/library/text/par.rs58
-rw-r--r--src/model/capture.rs186
-rw-r--r--src/model/collapse.rs116
-rw-r--r--src/model/content.rs70
-rw-r--r--src/model/fold.rs81
-rw-r--r--src/model/func.rs187
-rw-r--r--src/model/layout.rs142
-rw-r--r--src/model/mod.rs33
-rw-r--r--src/model/ops.rs3
-rw-r--r--src/model/property.rs195
-rw-r--r--src/model/realize.rs486
-rw-r--r--src/model/recipe.rs185
-rw-r--r--src/model/resolve.rs84
-rw-r--r--src/model/str.rs3
-rw-r--r--src/model/styles.rs525
-rw-r--r--src/model/value.rs2
39 files changed, 1887 insertions, 2004 deletions
diff --git a/src/lib.rs b/src/lib.rs
index caba9707..051a58f3 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -11,7 +11,7 @@
//! structure, layouts, etc. of the module. The nodes of the content tree are
//! well structured and order-independent and thus much better suited for
//! layouting than the raw markup.
-//! - **Layouting:** Next, the content is [layouted] into a portable version of
+//! - **Layouting:** Next, the content is layouted into a portable version of
//! the typeset document. The output of this is a collection of [`Frame`]s
//! (one per page), ready for exporting.
//! - **Exporting:** The finished layout can be exported into a supported
@@ -24,7 +24,6 @@
//! [evaluate]: model::eval
//! [module]: model::Module
//! [content]: model::Content
-//! [layouted]: model::layout
//! [PDF]: export::pdf
#![allow(clippy::len_without_is_empty)]
@@ -69,7 +68,7 @@ pub fn typeset(
) -> SourceResult<Vec<Frame>> {
let route = Route::default();
let module = model::eval(world.track(), route.track(), main)?;
- model::layout(world.track(), &module.content)
+ library::layout::Layout::layout(&module.content, world.track())
}
/// The environment in which typesetting occurs.
diff --git a/src/library/ext.rs b/src/library/ext.rs
new file mode 100644
index 00000000..07b55a7b
--- /dev/null
+++ b/src/library/ext.rs
@@ -0,0 +1,179 @@
+use super::*;
+use crate::library::prelude::*;
+
+/// Additional methods on content.
+pub trait ContentExt {
+ /// Make this content strong.
+ fn strong(self) -> Self;
+
+ /// Make this content emphasized.
+ fn emph(self) -> Self;
+
+ /// Underline this content.
+ fn underlined(self) -> Self;
+
+ /// Add weak vertical spacing above and below the content.
+ fn spaced(self, above: Option<Abs>, below: Option<Abs>) -> Self;
+
+ /// Force a size for this content.
+ fn boxed(self, sizing: Axes<Option<Rel<Length>>>) -> Self;
+
+ /// Set alignments for this content.
+ fn aligned(self, aligns: Axes<Option<RawAlign>>) -> Self;
+
+ /// Pad this content at the sides.
+ fn padded(self, padding: Sides<Rel<Length>>) -> Self;
+
+ /// Transform this content's contents without affecting layout.
+ fn moved(self, delta: Axes<Rel<Length>>) -> Self;
+
+ /// Fill the frames resulting from a content.
+ fn filled(self, fill: Paint) -> Self;
+
+ /// Stroke the frames resulting from a content.
+ fn stroked(self, stroke: Stroke) -> Self;
+}
+
+impl ContentExt for Content {
+ fn strong(self) -> Self {
+ text::StrongNode(self).pack()
+ }
+
+ fn emph(self) -> Self {
+ text::EmphNode(self).pack()
+ }
+
+ fn underlined(self) -> Self {
+ text::DecoNode::<{ text::UNDERLINE }>(self).pack()
+ }
+
+ fn spaced(self, above: Option<Abs>, below: Option<Abs>) -> Self {
+ if above.is_none() && below.is_none() {
+ return self;
+ }
+
+ let mut seq = vec![];
+ if let Some(above) = above {
+ seq.push(
+ layout::VNode {
+ amount: above.into(),
+ weak: true,
+ generated: true,
+ }
+ .pack(),
+ );
+ }
+
+ seq.push(self);
+ if let Some(below) = below {
+ seq.push(
+ layout::VNode {
+ amount: below.into(),
+ weak: true,
+ generated: true,
+ }
+ .pack(),
+ );
+ }
+
+ Content::sequence(seq)
+ }
+
+ fn boxed(self, sizing: Axes<Option<Rel<Length>>>) -> Self {
+ layout::BoxNode { sizing, child: self }.pack()
+ }
+
+ fn aligned(self, aligns: Axes<Option<RawAlign>>) -> Self {
+ layout::AlignNode { aligns, child: self }.pack()
+ }
+
+ fn padded(self, padding: Sides<Rel<Length>>) -> Self {
+ layout::PadNode { padding, child: self }.pack()
+ }
+
+ fn moved(self, delta: Axes<Rel<Length>>) -> Self {
+ layout::MoveNode { delta, child: self }.pack()
+ }
+
+ fn filled(self, fill: Paint) -> Self {
+ FillNode { fill, child: self }.pack()
+ }
+
+ fn stroked(self, stroke: Stroke) -> Self {
+ StrokeNode { stroke, child: self }.pack()
+ }
+}
+
+/// Additional methods for the style chain.
+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);
+}
+
+impl StyleMapExt for StyleMap {
+ fn set_family(&mut self, preferred: text::FontFamily, existing: StyleChain) {
+ self.set(
+ text::TextNode::FAMILY,
+ std::iter::once(preferred)
+ .chain(existing.get(text::TextNode::FAMILY).iter().cloned())
+ .collect(),
+ );
+ }
+}
+
+/// Fill the frames resulting from content.
+#[derive(Debug, Hash)]
+struct FillNode {
+ /// How to fill the frames resulting from the `child`.
+ fill: Paint,
+ /// The content whose frames should be filled.
+ child: Content,
+}
+
+#[node(LayoutBlock)]
+impl FillNode {}
+
+impl LayoutBlock for FillNode {
+ fn layout_block(
+ &self,
+ world: Tracked<dyn World>,
+ regions: &Regions,
+ styles: StyleChain,
+ ) -> SourceResult<Vec<Frame>> {
+ let mut frames = self.child.layout_block(world, regions, styles)?;
+ for frame in &mut frames {
+ let shape = Geometry::Rect(frame.size()).filled(self.fill);
+ frame.prepend(Point::zero(), Element::Shape(shape));
+ }
+ Ok(frames)
+ }
+}
+
+/// Stroke the frames resulting from content.
+#[derive(Debug, Hash)]
+struct StrokeNode {
+ /// How to stroke the frames resulting from the `child`.
+ stroke: Stroke,
+ /// The content whose frames should be stroked.
+ child: Content,
+}
+
+#[node(LayoutBlock)]
+impl StrokeNode {}
+
+impl LayoutBlock for StrokeNode {
+ fn layout_block(
+ &self,
+ world: Tracked<dyn World>,
+ regions: &Regions,
+ styles: StyleChain,
+ ) -> SourceResult<Vec<Frame>> {
+ let mut frames = self.child.layout_block(world, regions, styles)?;
+ for frame in &mut frames {
+ let shape = Geometry::Rect(frame.size()).stroked(self.stroke);
+ frame.prepend(Point::zero(), Element::Shape(shape));
+ }
+ Ok(frames)
+ }
+}
diff --git a/src/library/graphics/hide.rs b/src/library/graphics/hide.rs
index fafd7421..d320b06c 100644
--- a/src/library/graphics/hide.rs
+++ b/src/library/graphics/hide.rs
@@ -1,18 +1,18 @@
use crate::library::prelude::*;
-/// Hide a node without affecting layout.
+/// Hide content without affecting layout.
#[derive(Debug, Hash)]
pub struct HideNode(pub Content);
-#[node(Layout)]
+#[node(LayoutInline)]
impl HideNode {
fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
Ok(Self(args.expect("body")?).pack())
}
}
-impl Layout for HideNode {
- fn layout(
+impl LayoutInline for HideNode {
+ fn layout_inline(
&self,
world: Tracked<dyn World>,
regions: &Regions,
@@ -24,8 +24,4 @@ impl Layout for HideNode {
}
Ok(frames)
}
-
- fn level(&self) -> Level {
- Level::Inline
- }
}
diff --git a/src/library/graphics/image.rs b/src/library/graphics/image.rs
index 9c3a775a..e27ea488 100644
--- a/src/library/graphics/image.rs
+++ b/src/library/graphics/image.rs
@@ -8,7 +8,7 @@ use crate::library::text::TextNode;
#[derive(Debug, Hash)]
pub struct ImageNode(pub Image);
-#[node(Layout)]
+#[node(LayoutInline)]
impl ImageNode {
/// How the image should adjust itself to a given area.
pub const FIT: ImageFit = ImageFit::Cover;
@@ -36,8 +36,8 @@ impl ImageNode {
}
}
-impl Layout for ImageNode {
- fn layout(
+impl LayoutInline for ImageNode {
+ fn layout_inline(
&self,
_: Tracked<dyn World>,
regions: &Regions,
@@ -95,10 +95,6 @@ impl Layout for ImageNode {
Ok(vec![frame])
}
-
- fn level(&self) -> Level {
- Level::Inline
- }
}
/// How an image should adjust itself to a given area.
diff --git a/src/library/graphics/line.rs b/src/library/graphics/line.rs
index c2f89404..ee7813a5 100644
--- a/src/library/graphics/line.rs
+++ b/src/library/graphics/line.rs
@@ -9,7 +9,7 @@ pub struct LineNode {
delta: Axes<Rel<Length>>,
}
-#[node(Layout)]
+#[node(LayoutInline)]
impl LineNode {
/// How to stroke the line.
#[property(resolve, fold)]
@@ -36,8 +36,8 @@ impl LineNode {
}
}
-impl Layout for LineNode {
- fn layout(
+impl LayoutInline for LineNode {
+ fn layout_inline(
&self,
_: Tracked<dyn World>,
regions: &Regions,
@@ -65,10 +65,6 @@ impl Layout for LineNode {
Ok(vec![frame])
}
-
- fn level(&self) -> Level {
- Level::Inline
- }
}
castable! {
diff --git a/src/library/graphics/shape.rs b/src/library/graphics/shape.rs
index 608c9842..4804cd68 100644
--- a/src/library/graphics/shape.rs
+++ b/src/library/graphics/shape.rs
@@ -3,23 +3,23 @@ use std::f64::consts::SQRT_2;
use crate::library::prelude::*;
use crate::library::text::TextNode;
-/// Place a node into a sizable and fillable shape.
+/// A sizable and fillable shape with optional content.
#[derive(Debug, Hash)]
pub struct ShapeNode<const S: ShapeKind>(pub Option<Content>);
-/// Place a node into a square.
+/// A square with optional content.
pub type SquareNode = ShapeNode<SQUARE>;
-/// Place a node into a rectangle.
+/// A rectangle with optional content.
pub type RectNode = ShapeNode<RECT>;
-/// Place a node into a circle.
+/// A circle with optional content.
pub type CircleNode = ShapeNode<CIRCLE>;
-/// Place a node into an ellipse.
+/// A ellipse with optional content.
pub type EllipseNode = ShapeNode<ELLIPSE>;
-#[node(Layout)]
+#[node(LayoutInline)]
impl<const S: ShapeKind> ShapeNode<S> {
/// How to fill the shape.
pub const FILL: Option<Paint> = None;
@@ -72,8 +72,8 @@ impl<const S: ShapeKind> ShapeNode<S> {
}
}
-impl<const S: ShapeKind> Layout for ShapeNode<S> {
- fn layout(
+impl<const S: ShapeKind> LayoutInline for ShapeNode<S> {
+ fn layout_inline(
&self,
world: Tracked<dyn World>,
regions: &Regions,
@@ -173,10 +173,6 @@ impl<const S: ShapeKind> Layout for ShapeNode<S> {
Ok(frames)
}
-
- fn level(&self) -> Level {
- Level::Inline
- }
}
/// A category of shape.
diff --git a/src/library/layout/align.rs b/src/library/layout/align.rs
index f49763b5..2ee565cc 100644
--- a/src/library/layout/align.rs
+++ b/src/library/layout/align.rs
@@ -1,26 +1,23 @@
use crate::library::prelude::*;
use crate::library::text::{HorizontalAlign, ParNode};
-/// Align a node along the layouting axes.
+/// Align content along the layouting axes.
#[derive(Debug, Hash)]
pub struct AlignNode {
- /// How to align the node horizontally and vertically.
+ /// How to align the content horizontally and vertically.
pub aligns: Axes<Option<RawAlign>>,
- /// The node to be aligned.
+ /// The content to be aligned.
pub child: Content,
}
-#[node(Layout)]
+#[node(LayoutBlock)]
impl AlignNode {
fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
let aligns: Axes<Option<RawAlign>> = args.find()?.unwrap_or_default();
let body: Content = args.expect("body")?;
if let Axes { x: Some(x), y: None } = aligns {
- if body
- .to::<dyn Layout>()
- .map_or(true, |node| node.level() == Level::Inline)
- {
+ if !body.has::<dyn LayoutBlock>() {
return Ok(body.styled(ParNode::ALIGN, HorizontalAlign(x)));
}
}
@@ -29,8 +26,8 @@ impl AlignNode {
}
}
-impl Layout for AlignNode {
- fn layout(
+impl LayoutBlock for AlignNode {
+ fn layout_block(
&self,
world: Tracked<dyn World>,
regions: &Regions,
@@ -62,8 +59,4 @@ impl Layout for AlignNode {
Ok(frames)
}
-
- fn level(&self) -> Level {
- Level::Block
- }
}
diff --git a/src/library/layout/columns.rs b/src/library/layout/columns.rs
index 79d98e11..df259eab 100644
--- a/src/library/layout/columns.rs
+++ b/src/library/layout/columns.rs
@@ -11,7 +11,7 @@ pub struct ColumnsNode {
pub child: Content,
}
-#[node(Layout)]
+#[node(LayoutBlock)]
impl ColumnsNode {
/// The size of the gutter space between each column.
#[property(resolve)]
@@ -26,8 +26,8 @@ impl ColumnsNode {
}
}
-impl Layout for ColumnsNode {
- fn layout(
+impl LayoutBlock for ColumnsNode {
+ fn layout_block(
&self,
world: Tracked<dyn World>,
regions: &Regions,
@@ -66,7 +66,7 @@ impl Layout for ColumnsNode {
// Stitch together the columns for each region.
for region in regions.iter().take(total_regions) {
- // The height should be the parent height if the node shall expand.
+ // The height should be the parent height if we should expand.
// Otherwise its the maximum column height for the frame. In that
// case, the frame is first created with zero height and then
// resized.
@@ -100,10 +100,6 @@ impl Layout for ColumnsNode {
Ok(finished)
}
-
- fn level(&self) -> Level {
- Level::Block
- }
}
/// A column break.
diff --git a/src/library/layout/container.rs b/src/library/layout/container.rs
index 60f9139b..023809d0 100644
--- a/src/library/layout/container.rs
+++ b/src/library/layout/container.rs
@@ -1,15 +1,15 @@
use crate::library::prelude::*;
-/// An inline-level container that sizes content and places it into a paragraph.
+/// An inline-level container that sizes content.
#[derive(Debug, Clone, Hash)]
pub struct BoxNode {
- /// How to size the node horizontally and vertically.
+ /// How to size the content horizontally and vertically.
pub sizing: Axes<Option<Rel<Length>>>,
- /// The node to be sized.
+ /// The content to be sized.
pub child: Content,
}
-#[node(Layout)]
+#[node(LayoutInline)]
impl BoxNode {
fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
let width = args.named("width")?;
@@ -19,8 +19,8 @@ impl BoxNode {
}
}
-impl Layout for BoxNode {
- fn layout(
+impl LayoutInline for BoxNode {
+ fn layout_inline(
&self,
world: Tracked<dyn World>,
regions: &Regions,
@@ -55,25 +55,21 @@ impl Layout for BoxNode {
Ok(frames)
}
-
- fn level(&self) -> Level {
- Level::Inline
- }
}
/// A block-level container that places content into a separate flow.
#[derive(Debug, Clone, Hash)]
pub struct BlockNode(pub Content);
-#[node(Layout)]
+#[node(LayoutBlock)]
impl BlockNode {
fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
Ok(Self(args.eat()?.unwrap_or_default()).pack())
}
}
-impl Layout for BlockNode {
- fn layout(
+impl LayoutBlock for BlockNode {
+ fn layout_block(
&self,
world: Tracked<dyn World>,
regions: &Regions,
@@ -81,8 +77,4 @@ impl Layout for BlockNode {
) -> SourceResult<Vec<Frame>> {
self.0.layout_block(world, regions, styles)
}
-
- fn level(&self) -> Level {
- Level::Block
- }
}
diff --git a/src/library/layout/flow.rs b/src/library/layout/flow.rs
index 01ee9dc9..f4d18699 100644
--- a/src/library/layout/flow.rs
+++ b/src/library/layout/flow.rs
@@ -4,7 +4,7 @@ use super::{AlignNode, PlaceNode, Spacing};
use crate::library::prelude::*;
use crate::library::text::ParNode;
-/// Arrange spacing, paragraphs and other block-level nodes into a flow.
+/// Arrange spacing, paragraphs and block-level nodes into a flow.
///
/// This node is reponsible for layouting both the top-level content flow and
/// the contents of boxes.
@@ -16,17 +16,17 @@ pub struct FlowNode(pub StyleVec<FlowChild>);
pub enum FlowChild {
/// Vertical spacing between other children.
Spacing(Spacing),
- /// An arbitrary block-level node.
- Node(Content),
+ /// Arbitrary block-level content.
+ Block(Content),
/// A column / region break.
Colbreak,
}
-#[node(Layout)]
+#[node(LayoutBlock)]
impl FlowNode {}
-impl Layout for FlowNode {
- fn layout(
+impl LayoutBlock for FlowNode {
+ fn layout_block(
&self,
world: Tracked<dyn World>,
regions: &Regions,
@@ -40,8 +40,8 @@ impl Layout for FlowNode {
FlowChild::Spacing(kind) => {
layouter.layout_spacing(*kind, styles);
}
- FlowChild::Node(ref node) => {
- layouter.layout_node(world, node, styles)?;
+ FlowChild::Block(block) => {
+ layouter.layout_block(world, block, styles)?;
}
FlowChild::Colbreak => {
layouter.finish_region();
@@ -51,10 +51,6 @@ impl Layout for FlowNode {
Ok(layouter.finish())
}
-
- fn level(&self) -> Level {
- Level::Block
- }
}
impl Debug for FlowNode {
@@ -68,7 +64,7 @@ impl Debug for FlowChild {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Self::Spacing(kind) => write!(f, "{:?}", kind),
- Self::Node(node) => node.fmt(f),
+ Self::Block(block) => block.fmt(f),
Self::Colbreak => f.pad("Colbreak"),
}
}
@@ -96,7 +92,7 @@ pub struct FlowLayouter {
used: Size,
/// The sum of fractions in the current region.
fr: Fr,
- /// Spacing and layouted nodes.
+ /// Spacing and layouted blocks.
items: Vec<FlowItem>,
/// Finished frames for previous regions.
finished: Vec<Frame>,
@@ -108,7 +104,7 @@ enum FlowItem {
Absolute(Abs),
/// Fractional spacing between other items.
Fractional(Fr),
- /// A frame for a layouted child node and how to align it.
+ /// A frame for a layouted block and how to align it.
Frame(Frame, Axes<Align>),
/// An absolutely placed frame.
Placed(Frame),
@@ -153,11 +149,11 @@ impl FlowLayouter {
}
}
- /// Layout a node.
- pub fn layout_node(
+ /// Layout a block.
+ pub fn layout_block(
&mut self,
world: Tracked<dyn World>,
- node: &Content,
+ block: &Content,
styles: StyleChain,
) -> SourceResult<()> {
// Don't even try layouting into a full region.
@@ -167,27 +163,28 @@ impl FlowLayouter {
// Placed nodes that are out of flow produce placed items which aren't
// aligned later.
- if let Some(placed) = node.downcast::<PlaceNode>() {
+ if let Some(placed) = block.downcast::<PlaceNode>() {
if placed.out_of_flow() {
- let frame = node.layout_block(world, &self.regions, styles)?.remove(0);
+ let frame = block.layout_block(world, &self.regions, styles)?.remove(0);
self.items.push(FlowItem::Placed(frame));
return Ok(());
}
}
- // How to align the node.
+ // How to align the block.
let aligns = Axes::new(
// For non-expanding paragraphs it is crucial that we align the
// whole paragraph as it is itself aligned.
styles.get(ParNode::ALIGN),
- // Vertical align node alignment is respected by the flow node.
- node.downcast::<AlignNode>()
+ // Vertical align node alignment is respected by the flow.
+ block
+ .downcast::<AlignNode>()
.and_then(|aligned| aligned.aligns.y)
.map(|align| align.resolve(styles))
.unwrap_or(Align::Top),
);
- let frames = node.layout_block(world, &self.regions, styles)?;
+ let frames = block.layout_block(world, &self.regions, styles)?;
let len = frames.len();
for (i, mut frame) in frames.into_iter().enumerate() {
// Set the generic block role.
diff --git a/src/library/layout/grid.rs b/src/library/layout/grid.rs
index 7e5cbbd5..1bb67691 100644
--- a/src/library/layout/grid.rs
+++ b/src/library/layout/grid.rs
@@ -1,17 +1,17 @@
use crate::library::prelude::*;
-/// Arrange nodes in a grid.
+/// Arrange content in a grid.
#[derive(Debug, Hash)]
pub struct GridNode {
/// Defines sizing for content rows and columns.
pub tracks: Axes<Vec<TrackSizing>>,
/// Defines sizing of gutter rows and columns between content.
pub gutter: Axes<Vec<TrackSizing>>,
- /// The nodes to be arranged in a grid.
+ /// The content to be arranged in a grid.
pub cells: Vec<Content>,
}
-#[node(Layout)]
+#[node(LayoutBlock)]
impl GridNode {
fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
let columns = args.named("columns")?.unwrap_or_default();
@@ -31,8 +31,8 @@ impl GridNode {
}
}
-impl Layout for GridNode {
- fn layout(
+impl LayoutBlock for GridNode {
+ fn layout_block(
&self,
world: Tracked<dyn World>,
regions: &Regions,
@@ -51,10 +51,6 @@ impl Layout for GridNode {
// Measure the columns and layout the grid row-by-row.
layouter.layout()
}
-
- fn level(&self) -> Level {
- Level::Block
- }
}
/// Defines how to size a grid cell along an axis.
@@ -293,7 +289,7 @@ impl<'a> GridLayouter<'a> {
let mut resolved = Abs::zero();
for y in 0 .. self.rows.len() {
- if let Some(node) = self.cell(x, y) {
+ if let Some(cell) = self.cell(x, y) {
let size = Size::new(available, self.regions.base.y);
let mut pod =
Regions::one(size, self.regions.base, Axes::splat(false));
@@ -307,7 +303,7 @@ impl<'a> GridLayouter<'a> {
}
let frame =
- node.layout_block(self.world, &pod, self.styles)?.remove(0);
+ cell.layout_block(self.world, &pod, self.styles)?.remove(0);
resolved.set_max(frame.width());
}
}
@@ -366,7 +362,7 @@ impl<'a> GridLayouter<'a> {
// Determine the size for each region of the row.
for (x, &rcol) in self.rcols.iter().enumerate() {
- if let Some(node) = self.cell(x, y) {
+ if let Some(cell) = self.cell(x, y) {
let mut pod = self.regions.clone();
pod.first.x = rcol;
pod.base.x = rcol;
@@ -376,7 +372,7 @@ impl<'a> GridLayouter<'a> {
pod.base.x = self.regions.base.x;
}
- let mut sizes = node
+ let mut sizes = cell
.layout_block(self.world, &pod, self.styles)?
.into_iter()
.map(|frame| frame.height());
@@ -456,7 +452,7 @@ impl<'a> GridLayouter<'a> {
let mut pos = Point::zero();
for (x, &rcol) in self.rcols.iter().enumerate() {
- if let Some(node) = self.cell(x, y) {
+ if let Some(cell) = self.cell(x, y) {
let size = Size::new(rcol, height);
// Set the base to the region's base for auto rows and to the
@@ -466,7 +462,7 @@ impl<'a> GridLayouter<'a> {
.select(self.regions.base, size);
let pod = Regions::one(size, base, Axes::splat(true));
- let frame = node.layout_block(self.world, &pod, self.styles)?.remove(0);
+ let frame = cell.layout_block(self.world, &pod, self.styles)?.remove(0);
match frame.role() {
Some(Role::ListLabel | Role::ListItemBody) => {
output.apply_role(Role::ListItem)
@@ -504,7 +500,7 @@ impl<'a> GridLayouter<'a> {
// Layout the row.
let mut pos = Point::zero();
for (x, &rcol) in self.rcols.iter().enumerate() {
- if let Some(node) = self.cell(x, y) {
+ if let Some(cell) = self.cell(x, y) {
pod.first.x = rcol;
pod.base.x = rcol;
@@ -514,7 +510,7 @@ impl<'a> GridLayouter<'a> {
}
// Push the layouted frames into the individual output frames.
- let frames = node.layout_block(self.world, &pod, self.styles)?;
+ let frames = cell.layout_block(self.world, &pod, self.styles)?;
for (output, frame) in outputs.iter_mut().zip(frames) {
match frame.role() {
Some(Role::ListLabel | Role::ListItemBody) => {
@@ -578,7 +574,7 @@ impl<'a> GridLayouter<'a> {
Ok(())
}
- /// Get the node in the cell in column `x` and row `y`.
+ /// Get the content of the cell in column `x` and row `y`.
///
/// Returns `None` if it's a gutter cell.
#[track_caller]
diff --git a/src/library/layout/mod.rs b/src/library/layout/mod.rs
index 02276f22..000cb212 100644
--- a/src/library/layout/mod.rs
+++ b/src/library/layout/mod.rs
@@ -23,3 +23,792 @@ pub use place::*;
pub use spacing::*;
pub use stack::*;
pub use transform::*;
+
+use std::mem;
+
+use comemo::Tracked;
+use typed_arena::Arena;
+
+use crate::diag::SourceResult;
+use crate::frame::Frame;
+use crate::geom::*;
+use crate::library::structure::{DocNode, ListItem, ListNode, DESC, ENUM, LIST};
+use crate::library::text::{
+ LinebreakNode, ParChild, ParNode, ParbreakNode, SmartQuoteNode, SpaceNode, TextNode,
+};
+use crate::model::{
+ capability, Barrier, Content, Interruption, Node, SequenceNode, Show, StyleChain,
+ StyleEntry, StyleMap, StyleVec, StyleVecBuilder, StyledNode, Target,
+};
+use crate::World;
+
+/// The root-level layout.
+#[capability]
+pub trait Layout: 'static + Sync + Send {
+ /// Layout into one frame per page.
+ fn layout(&self, world: Tracked<dyn World>) -> SourceResult<Vec<Frame>>;
+}
+
+impl Layout for Content {
+ #[comemo::memoize]
+ fn layout(&self, world: Tracked<dyn World>) -> SourceResult<Vec<Frame>> {
+ let styles = StyleChain::with_root(&world.config().styles);
+ let scratch = Scratch::default();
+
+ let mut builder = Builder::new(world, &scratch, true);
+ builder.accept(self, styles)?;
+
+ let (doc, shared) = builder.into_doc(styles)?;
+ doc.layout(world, shared)
+ }
+}
+
+/// Block-level layout.
+#[capability]
+pub trait LayoutBlock: 'static + Sync + Send {
+ /// Layout into one frame per region.
+ fn layout_block(
+ &self,
+ world: Tracked<dyn World>,
+ regions: &Regions,
+ styles: StyleChain,
+ ) -> SourceResult<Vec<Frame>>;
+}
+
+impl LayoutBlock for Content {
+ #[comemo::memoize]
+ fn layout_block(
+ &self,
+ world: Tracked<dyn World>,
+ regions: &Regions,
+ styles: StyleChain,
+ ) -> SourceResult<Vec<Frame>> {
+ if let Some(node) = self.to::<dyn LayoutBlock>() {
+ let barrier = StyleEntry::Barrier(Barrier::new(self.id()));
+ let styles = barrier.chain(&styles);
+ return node.layout_block(world, regions, styles);
+ }
+
+ let scratch = Scratch::default();
+ let mut builder = Builder::new(world, &scratch, false);
+ builder.accept(self, styles)?;
+ let (flow, shared) = builder.into_flow(styles)?;
+ flow.layout_block(world, regions, shared)
+ }
+}
+
+/// Inline-level layout.
+#[capability]
+pub trait LayoutInline: 'static + Sync + Send {
+ /// Layout into a single frame.
+ fn layout_inline(
+ &self,
+ world: Tracked<dyn World>,
+ regions: &Regions,
+ styles: StyleChain,
+ ) -> SourceResult<Vec<Frame>>;
+}
+
+impl LayoutInline for Content {
+ #[comemo::memoize]
+ fn layout_inline(
+ &self,
+ world: Tracked<dyn World>,
+ regions: &Regions,
+ styles: StyleChain,
+ ) -> SourceResult<Vec<Frame>> {
+ if let Some(node) = self.to::<dyn LayoutInline>() {
+ let barrier = StyleEntry::Barrier(Barrier::new(self.id()));
+ let styles = barrier.chain(&styles);
+ return node.layout_inline(world, regions, styles);
+ }
+
+ if let Some(node) = self.to::<dyn LayoutBlock>() {
+ let barrier = StyleEntry::Barrier(Barrier::new(self.id()));
+ let styles = barrier.chain(&styles);
+ return node.layout_block(world, regions, styles);
+ }
+
+ let scratch = Scratch::default();
+ let mut builder = Builder::new(world, &scratch, false);
+ builder.accept(self, styles)?;
+ let (flow, shared) = builder.into_flow(styles)?;
+ flow.layout_block(world, regions, shared)
+ }
+}
+
+/// A sequence of regions to layout into.
+#[derive(Debug, Clone, Hash)]
+pub struct Regions {
+ /// The (remaining) size of the first region.
+ pub first: Size,
+ /// The base size for relative sizing.
+ pub base: Size,
+ /// The height of followup regions. The width is the same for all regions.
+ pub backlog: Vec<Abs>,
+ /// The height of the final region that is repeated once the backlog is
+ /// drained. The width is the same for all regions.
+ pub last: Option<Abs>,
+ /// Whether nodes should expand to fill the regions instead of shrinking to
+ /// fit the content.
+ pub expand: Axes<bool>,
+}
+
+impl Regions {
+ /// Create a new region sequence with exactly one region.
+ pub fn one(size: Size, base: Size, expand: Axes<bool>) -> Self {
+ Self {
+ first: size,
+ base,
+ backlog: vec![],
+ last: None,
+ expand,
+ }
+ }
+
+ /// Create a new sequence of same-size regions that repeats indefinitely.
+ pub fn repeat(size: Size, base: Size, expand: Axes<bool>) -> Self {
+ Self {
+ first: size,
+ base,
+ backlog: vec![],
+ last: Some(size.y),
+ expand,
+ }
+ }
+
+ /// Create new regions where all sizes are mapped with `f`.
+ ///
+ /// Note that since all regions must have the same width, the width returned
+ /// by `f` is ignored for the backlog and the final region.
+ pub fn map<F>(&self, mut f: F) -> Self
+ where
+ F: FnMut(Size) -> Size,
+ {
+ let x = self.first.x;
+ Self {
+ first: f(self.first),
+ base: f(self.base),
+ backlog: self.backlog.iter().map(|&y| f(Size::new(x, y)).y).collect(),
+ last: self.last.map(|y| f(Size::new(x, y)).y),
+ expand: self.expand,
+ }
+ }
+
+ /// Whether the first region is full and a region break is called for.
+ pub fn is_full(&self) -> bool {
+ Abs::zero().fits(self.first.y) && !self.in_last()
+ }
+
+ /// Whether the first region is the last usable region.
+ ///
+ /// If this is true, calling `next()` will have no effect.
+ pub fn in_last(&self) -> bool {
+ self.backlog.len() == 0 && self.last.map_or(true, |height| self.first.y == height)
+ }
+
+ /// Advance to the next region if there is any.
+ pub fn next(&mut self) {
+ if let Some(height) = (!self.backlog.is_empty())
+ .then(|| self.backlog.remove(0))
+ .or(self.last)
+ {
+ self.first.y = height;
+ self.base.y = height;
+ }
+ }
+
+ /// An iterator that returns the sizes of the first and all following
+ /// regions, equivalently to what would be produced by calling
+ /// [`next()`](Self::next) repeatedly until all regions are exhausted.
+ /// This iterater may be infinite.
+ pub fn iter(&self) -> impl Iterator<Item = Size> + '_ {
+ let first = std::iter::once(self.first);
+ let backlog = self.backlog.iter();
+ let last = self.last.iter().cycle();
+ first.chain(backlog.chain(last).map(|&h| Size::new(self.first.x, h)))
+ }
+}
+
+/// Builds a document or a flow node from content.
+struct Builder<'a> {
+ /// The core context.
+ world: Tracked<'a, dyn World>,
+ /// Scratch arenas for building.
+ scratch: &'a Scratch<'a>,
+ /// The current document building state.
+ doc: Option<DocBuilder<'a>>,
+ /// The current flow building state.
+ flow: FlowBuilder<'a>,
+ /// The current paragraph building state.
+ par: ParBuilder<'a>,
+ /// The current list building state.
+ list: ListBuilder<'a>,
+}
+
+/// Temporary storage arenas for building.
+#[derive(Default)]
+struct Scratch<'a> {
+ /// An arena where intermediate style chains are stored.
+ styles: Arena<StyleChain<'a>>,
+ /// An arena where intermediate content resulting from show rules is stored.
+ templates: Arena<Content>,
+}
+
+impl<'a> Builder<'a> {
+ pub fn new(
+ world: Tracked<'a, dyn World>,
+ scratch: &'a Scratch<'a>,
+ top: bool,
+ ) -> Self {
+ Self {
+ world,
+ scratch,
+ doc: top.then(|| DocBuilder::default()),
+ flow: FlowBuilder::default(),
+ par: ParBuilder::default(),
+ list: ListBuilder::default(),
+ }
+ }
+
+ pub fn into_doc(
+ mut self,
+ styles: StyleChain<'a>,
+ ) -> SourceResult<(DocNode, StyleChain<'a>)> {
+ self.interrupt(Interruption::Page, styles, true)?;
+ let (pages, shared) = self.doc.unwrap().pages.finish();
+ Ok((DocNode(pages), shared))
+ }
+
+ pub fn into_flow(
+ mut self,
+ styles: StyleChain<'a>,
+ ) -> SourceResult<(FlowNode, StyleChain<'a>)> {
+ self.interrupt(Interruption::Par, styles, false)?;
+ let (children, shared) = self.flow.0.finish();
+ Ok((FlowNode(children), shared))
+ }
+
+ pub fn accept(
+ &mut self,
+ content: &'a Content,
+ styles: StyleChain<'a>,
+ ) -> SourceResult<()> {
+ if let Some(text) = content.downcast::<TextNode>() {
+ if let Some(realized) = styles.apply(self.world, Target::Text(&text.0))? {
+ let stored = self.scratch.templates.alloc(realized);
+ return self.accept(stored, styles);
+ }
+ } else if let Some(styled) = content.downcast::<StyledNode>() {
+ return self.styled(styled, styles);
+ } else if let Some(seq) = content.downcast::<SequenceNode>() {
+ return self.sequence(seq, styles);
+ } else if content.has::<dyn Show>() {
+ if self.show(&content, styles)? {
+ return Ok(());
+ }
+ }
+
+ if self.list.accept(content, styles) {
+ return Ok(());
+ }
+
+ self.interrupt(Interruption::List, styles, false)?;
+
+ if content.is::<ListItem>() {
+ self.list.accept(content, styles);
+ return Ok(());
+ }
+
+ if self.par.accept(content, styles) {
+ return Ok(());
+ }
+
+ self.interrupt(Interruption::Par, styles, false)?;
+
+ if self.flow.accept(content, styles) {
+ return Ok(());
+ }
+
+ let keep = content
+ .downcast::<PagebreakNode>()
+ .map_or(false, |pagebreak| !pagebreak.weak);
+ self.interrupt(Interruption::Page, styles, keep)?;
+
+ if let Some(doc) = &mut self.doc {
+ doc.accept(content, styles);
+ }
+
+ // We might want to issue a warning or error for content that wasn't
+ // handled (e.g. a pagebreak in a flow building process). However, we
+ // don't have the spans here at the moment.
+ Ok(())
+ }
+
+ fn show(
+ &mut self,
+ content: &'a Content,
+ styles: StyleChain<'a>,
+ ) -> SourceResult<bool> {
+ if let Some(mut realized) = styles.apply(self.world, Target::Node(content))? {
+ let mut map = StyleMap::new();
+ let barrier = Barrier::new(content.id());
+ map.push(StyleEntry::Barrier(barrier));
+ map.push(StyleEntry::Barrier(barrier));
+ realized = realized.styled_with_map(map);
+ let stored = self.scratch.templates.alloc(realized);
+ self.accept(stored, styles)?;
+ Ok(true)
+ } else {
+ Ok(false)
+ }
+ }
+
+ fn styled(
+ &mut self,
+ styled: &'a StyledNode,
+ styles: StyleChain<'a>,
+ ) -> SourceResult<()> {
+ let stored = self.scratch.styles.alloc(styles);
+ let styles = styled.map.chain(stored);
+ let intr = styled.map.interruption();
+
+ if let Some(intr) = intr {
+ self.interrupt(intr, styles, false)?;
+ }
+
+ self.accept(&styled.sub, styles)?;
+
+ if let Some(intr) = intr {
+ self.interrupt(intr, styles, true)?;
+ }
+
+ Ok(())
+ }
+
+ fn interrupt(
+ &mut self,
+ intr: Interruption,
+ styles: StyleChain<'a>,
+ keep: bool,
+ ) -> SourceResult<()> {
+ if intr >= Interruption::List && !self.list.is_empty() {
+ mem::take(&mut self.list).finish(self)?;
+ }
+
+ if intr >= Interruption::Par {
+ if !self.par.is_empty() {
+ mem::take(&mut self.par).finish(self);
+ }
+ }
+
+ if intr >= Interruption::Page {
+ if let Some(doc) = &mut self.doc {
+ if !self.flow.is_empty() || (doc.keep_next && keep) {
+ mem::take(&mut self.flow).finish(doc, styles);
+ }
+ doc.keep_next = !keep;
+ }
+ }
+
+ Ok(())
+ }
+
+ fn sequence(
+ &mut self,
+ seq: &'a SequenceNode,
+ styles: StyleChain<'a>,
+ ) -> SourceResult<()> {
+ for content in &seq.0 {
+ self.accept(content, styles)?;
+ }
+ Ok(())
+ }
+}
+
+/// Accepts pagebreaks and pages.
+struct DocBuilder<'a> {
+ /// The page runs built so far.
+ pages: StyleVecBuilder<'a, PageNode>,
+ /// Whether to keep a following page even if it is empty.
+ keep_next: bool,
+}
+
+impl<'a> DocBuilder<'a> {
+ fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) {
+ if let Some(pagebreak) = content.downcast::<PagebreakNode>() {
+ self.keep_next = !pagebreak.weak;
+ }
+
+ if let Some(page) = content.downcast::<PageNode>() {
+ self.pages.push(page.clone(), styles);
+ self.keep_next = false;
+ }
+ }
+}
+
+impl Default for DocBuilder<'_> {
+ fn default() -> Self {
+ Self {
+ pages: StyleVecBuilder::new(),
+ keep_next: true,
+ }
+ }
+}
+
+/// Accepts flow content.
+#[derive(Default)]
+struct FlowBuilder<'a>(CollapsingBuilder<'a, FlowChild>);
+
+impl<'a> FlowBuilder<'a> {
+ fn accept(&mut self, content: &'a 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
+
+ if let Some(_) = content.downcast::<ParbreakNode>() {
+ /* Nothing to do */
+ } 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.amount);
+ let frac = vertical.amount.is_fractional();
+ if vertical.weak {
+ let weakness = 1 + u8::from(frac) + 2 * u8::from(vertical.generated);
+ self.0.weak(child, styles, weakness);
+ } else if frac {
+ self.0.destructive(child, styles);
+ } else {
+ self.0.ignorant(child, styles);
+ }
+ } else if content.has::<dyn LayoutBlock>() {
+ let child = FlowChild::Block(content.clone());
+ if content.is::<PlaceNode>() {
+ self.0.ignorant(child, styles);
+ } else {
+ self.0.supportive(child, styles);
+ }
+ } else {
+ return false;
+ }
+
+ true
+ }
+
+ fn par(&mut self, par: ParNode, styles: StyleChain<'a>, indent: bool) {
+ let amount = if indent && !styles.get(ParNode::SPACING_AND_INDENT) {
+ styles.get(ParNode::LEADING).into()
+ } else {
+ styles.get(ParNode::SPACING).into()
+ };
+
+ self.0.weak(FlowChild::Spacing(amount), styles, 5);
+ self.0.supportive(FlowChild::Block(par.pack()), styles);
+ self.0.weak(FlowChild::Spacing(amount), styles, 5);
+ }
+
+ fn finish(self, doc: &mut DocBuilder<'a>, styles: StyleChain<'a>) {
+ let (flow, shared) = self.0.finish();
+ let styles = if flow.is_empty() { styles } else { shared };
+ let node = PageNode(FlowNode(flow).pack());
+ doc.pages.push(node, styles);
+ }
+
+ fn is_empty(&self) -> bool {
+ self.0.is_empty()
+ }
+}
+
+/// Accepts paragraph content.
+#[derive(Default)]
+struct ParBuilder<'a>(CollapsingBuilder<'a, ParChild>);
+
+impl<'a> ParBuilder<'a> {
+ fn accept(&mut self, content: &'a 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;
+ }
+
+ true
+ }
+
+ fn finish(self, parent: &mut Builder<'a>) {
+ let (mut children, shared) = self.0.finish();
+ if children.is_empty() {
+ return;
+ }
+
+ // Paragraph indent should only apply if the paragraph starts with
+ // text and follows directly after another paragraph.
+ let indent = shared.get(ParNode::INDENT);
+ if !indent.is_zero()
+ && children
+ .items()
+ .find_map(|child| match child {
+ ParChild::Spacing(_) => None,
+ ParChild::Text(_) | ParChild::Quote { .. } => Some(true),
+ ParChild::Inline(_) => Some(false),
+ })
+ .unwrap_or_default()
+ && parent
+ .flow
+ .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()
+ {
+ children.push_front(ParChild::Spacing(indent.into()));
+ }
+
+ parent.flow.par(ParNode(children), shared, !indent.is_zero());
+ }
+
+ fn is_empty(&self) -> bool {
+ self.0.is_empty()
+ }
+}
+
+/// Accepts list / enum items, spaces, paragraph breaks.
+struct ListBuilder<'a> {
+ /// The list items collected so far.
+ items: StyleVecBuilder<'a, ListItem>,
+ /// Whether the list contains no paragraph breaks.
+ tight: bool,
+ /// Whether the list can be attached.
+ attachable: bool,
+ /// Trailing content for which it is unclear whether it is part of the list.
+ staged: Vec<(&'a Content, StyleChain<'a>)>,
+}
+
+impl<'a> ListBuilder<'a> {
+ fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool {
+ if self.items.is_empty() {
+ if content.is::<ParbreakNode>() {
+ self.attachable = false;
+ } else if !content.is::<SpaceNode>() && !content.is::<ListItem>() {
+ self.attachable = true;
+ }
+ }
+
+ if let Some(item) = content.downcast::<ListItem>() {
+ if self
+ .items
+ .items()
+ .next()
+ .map_or(true, |first| item.kind() == first.kind())
+ {
+ self.items.push(item.clone(), styles);
+ self.tight &= self.staged.drain(..).all(|(t, _)| !t.is::<ParbreakNode>());
+ } else {
+ return false;
+ }
+ } else if !self.items.is_empty()
+ && (content.is::<SpaceNode>() || content.is::<ParbreakNode>())
+ {
+ self.staged.push((content, styles));
+ } else {
+ return false;
+ }
+
+ true
+ }
+
+ fn finish(self, parent: &mut Builder<'a>) -> SourceResult<()> {
+ let (items, shared) = self.items.finish();
+ let kind = match items.items().next() {
+ Some(item) => item.kind(),
+ None => return Ok(()),
+ };
+
+ let tight = self.tight;
+ let attached = tight && self.attachable;
+ let content = match kind {
+ LIST => ListNode::<LIST> { tight, attached, items }.pack(),
+ ENUM => ListNode::<ENUM> { tight, attached, items }.pack(),
+ DESC | _ => ListNode::<DESC> { tight, attached, items }.pack(),
+ };
+
+ let stored = parent.scratch.templates.alloc(content);
+ parent.accept(stored, shared)?;
+
+ for (content, styles) in self.staged {
+ parent.accept(content, styles)?;
+ }
+
+ parent.list.attachable = true;
+
+ Ok(())
+ }
+
+ fn is_empty(&self) -> bool {
+ self.items.is_empty()
+ }
+}
+
+impl Default for ListBuilder<'_> {
+ fn default() -> Self {
+ Self {
+ items: StyleVecBuilder::default(),
+ tight: true,
+ attachable: true,
+ staged: vec![],
+ }
+ }
+}
+
+/// 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.
+ pub fn new() -> Self {
+ Self {
+ builder: StyleVecBuilder::new(),
+ staged: vec![],
+ last: Last::Destructive,
+ }
+ }
+
+ /// Whether the builder is empty.
+ pub 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.
+ pub 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 {
+ if let Some(i) =
+ self.staged.iter().position(|(prev_item, _, prev_weakness)| {
+ prev_weakness.map_or(false, |prev_weakness| {
+ weakness < prev_weakness
+ || (weakness == prev_weakness && item > *prev_item)
+ })
+ })
+ {
+ self.staged.remove(i);
+ } else {
+ return;
+ }
+ }
+
+ self.staged.push((item, styles, Some(weakness)));
+ self.last = Last::Weak;
+ }
+
+ /// Forces nearby weak items to collapse.
+ pub 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.
+ pub 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.
+ pub fn ignorant(&mut self, item: T, styles: StyleChain<'a>) {
+ self.staged.push((item, styles, None));
+ }
+
+ /// Iterate over the contained items.
+ pub 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.
+ pub 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/src/library/layout/pad.rs b/src/library/layout/pad.rs
index effdd5f8..920660d6 100644
--- a/src/library/layout/pad.rs
+++ b/src/library/layout/pad.rs
@@ -1,15 +1,15 @@
use crate::library::prelude::*;
-/// Pad a node at the sides.
+/// Pad content at the sides.
#[derive(Debug, Hash)]
pub struct PadNode {
/// The amount of padding.
pub padding: Sides<Rel<Length>>,
- /// The child node whose sides to pad.
+ /// The content whose sides to pad.
pub child: Content,
}
-#[node(Layout)]
+#[node(LayoutBlock)]
impl PadNode {
fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
let all = args.named("rest")?.or(args.find()?);
@@ -25,8 +25,8 @@ impl PadNode {
}
}
-impl Layout for PadNode {
- fn layout(
+impl LayoutBlock for PadNode {
+ fn layout_block(
&self,
world: Tracked<dyn World>,
regions: &Regions,
@@ -51,10 +51,6 @@ impl Layout for PadNode {
Ok(frames)
}
-
- fn level(&self) -> Level {
- Level::Block
- }
}
/// Shrink a size by padding relative to the size itself.
diff --git a/src/library/layout/page.rs b/src/library/layout/page.rs
index f5821ae6..8d081749 100644
--- a/src/library/layout/page.rs
+++ b/src/library/layout/page.rs
@@ -80,7 +80,7 @@ impl PageNode {
let mut child = self.0.clone();
- // Realize columns with columns node.
+ // Realize columns.
let columns = styles.get(Self::COLUMNS);
if columns.get() > 1 {
child = ColumnsNode { columns, child: self.0.clone() }.pack();
diff --git a/src/library/layout/place.rs b/src/library/layout/place.rs
index 42ab0fba..ee38ebe6 100644
--- a/src/library/layout/place.rs
+++ b/src/library/layout/place.rs
@@ -1,11 +1,11 @@
use super::AlignNode;
use crate::library::prelude::*;
-/// Place a node at an absolute position.
+/// Place content at an absolute position.
#[derive(Debug, Hash)]
pub struct PlaceNode(pub Content);
-#[node(Layout)]
+#[node(LayoutBlock)]
impl PlaceNode {
fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
let aligns = args.find()?.unwrap_or(Axes::with_x(Some(RawAlign::Start)));
@@ -16,8 +16,8 @@ impl PlaceNode {
}
}
-impl Layout for PlaceNode {
- fn layout(
+impl LayoutBlock for PlaceNode {
+ fn layout_block(
&self,
world: Tracked<dyn World>,
regions: &Regions,
@@ -42,10 +42,6 @@ impl Layout for PlaceNode {
Ok(frames)
}
-
- fn level(&self) -> Level {
- Level::Block
- }
}
impl PlaceNode {
diff --git a/src/library/layout/spacing.rs b/src/library/layout/spacing.rs
index 6df5cde5..c410eee7 100644
--- a/src/library/layout/spacing.rs
+++ b/src/library/layout/spacing.rs
@@ -78,7 +78,7 @@ castable! {
Value::Fraction(v) => Self::Fractional(v),
}
-/// Spacing around and between block-level nodes, relative to paragraph spacing.
+/// Spacing around and between blocks, relative to paragraph spacing.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct BlockSpacing(Rel<Length>);
diff --git a/src/library/layout/stack.rs b/src/library/layout/stack.rs
index 9ea7965d..e1e70de9 100644
--- a/src/library/layout/stack.rs
+++ b/src/library/layout/stack.rs
@@ -3,7 +3,7 @@ use crate::library::prelude::*;
use crate::library::text::ParNode;
use crate::model::StyledNode;
-/// Arrange nodes and spacing along an axis.
+/// Arrange content and spacing along an axis.
#[derive(Debug, Hash)]
pub struct StackNode {
/// The stacking direction.
@@ -14,7 +14,7 @@ pub struct StackNode {
pub children: Vec<StackChild>,
}
-#[node(Layout)]
+#[node(LayoutBlock)]
impl StackNode {
fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
Ok(Self {
@@ -26,8 +26,8 @@ impl StackNode {
}
}
-impl Layout for StackNode {
- fn layout(
+impl LayoutBlock for StackNode {
+ fn layout_block(
&self,
world: Tracked<dyn World>,
regions: &Regions,
@@ -35,7 +35,7 @@ impl Layout for StackNode {
) -> SourceResult<Vec<Frame>> {
let mut layouter = StackLayouter::new(self.dir, regions, styles);
- // Spacing to insert before the next node.
+ // Spacing to insert before the next block.
let mut deferred = None;
for child in &self.children {
@@ -44,12 +44,12 @@ impl Layout for StackNode {
layouter.layout_spacing(*kind);
deferred = None;
}
- StackChild::Node(node) => {
+ StackChild::Block(block) => {
if let Some(kind) = deferred {
layouter.layout_spacing(kind);
}
- layouter.layout_node(world, node, styles)?;
+ layouter.layout_block(world, block, styles)?;
deferred = self.spacing;
}
}
@@ -57,26 +57,22 @@ impl Layout for StackNode {
Ok(layouter.finish())
}
-
- fn level(&self) -> Level {
- Level::Block
- }
}
/// A child of a stack node.
#[derive(Hash)]
pub enum StackChild {
- /// Spacing between other nodes.
+ /// Spacing between other children.
Spacing(Spacing),
- /// An arbitrary node.
- Node(Content),
+ /// Arbitrary block-level content.
+ Block(Content),
}
impl Debug for StackChild {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Self::Spacing(kind) => kind.fmt(f),
- Self::Node(node) => node.fmt(f),
+ Self::Block(block) => block.fmt(f),
}
}
}
@@ -88,7 +84,7 @@ castable! {
Value::Ratio(v) => Self::Spacing(Spacing::Relative(v.into())),
Value::Relative(v) => Self::Spacing(Spacing::Relative(v)),
Value::Fraction(v) => Self::Spacing(Spacing::Fractional(v)),
- Value::Content(v) => Self::Node(v),
+ Value::Content(v) => Self::Block(v),
}
/// Performs stack layout.
@@ -122,7 +118,7 @@ enum StackItem {
Absolute(Abs),
/// Fractional spacing between other items.
Fractional(Fr),
- /// A frame for a layouted child node.
+ /// A frame for a layouted block.
Frame(Frame, Align),
}
@@ -171,11 +167,11 @@ impl<'a> StackLayouter<'a> {
}
}
- /// Layout an arbitrary node.
- pub fn layout_node(
+ /// Layout an arbitrary block.
+ pub fn layout_block(
&mut self,
world: Tracked<dyn World>,
- node: &Content,
+ block: &Content,
styles: StyleChain,
) -> SourceResult<()> {
if self.regions.is_full() {
@@ -184,12 +180,12 @@ impl<'a> StackLayouter<'a> {
// Block-axis alignment of the `AlignNode` is respected
// by the stack node.
- let align = node
+ let align = block
.downcast::<AlignNode>()
.and_then(|node| node.aligns.get(self.axis))
.map(|align| align.resolve(styles))
.unwrap_or_else(|| {
- if let Some(styled) = node.downcast::<StyledNode>() {
+ if let Some(styled) = block.downcast::<StyledNode>() {
let map = &styled.map;
if map.contains(ParNode::ALIGN) {
return StyleChain::with_root(map).get(ParNode::ALIGN);
@@ -199,7 +195,7 @@ impl<'a> StackLayouter<'a> {
self.dir.start().into()
});
- let frames = node.layout_block(world, &self.regions, styles)?;
+ let frames = block.layout_block(world, &self.regions, styles)?;
let len = frames.len();
for (i, mut frame) in frames.into_iter().enumerate() {
// Set the generic block role.
diff --git a/src/library/layout/transform.rs b/src/library/layout/transform.rs
index 061efa6b..a73a1827 100644
--- a/src/library/layout/transform.rs
+++ b/src/library/layout/transform.rs
@@ -1,16 +1,16 @@
use crate::geom::Transform;
use crate::library::prelude::*;
-/// Move a node without affecting layout.
+/// Move content without affecting layout.
#[derive(Debug, Hash)]
pub struct MoveNode {
- /// The offset by which to move the node.
+ /// The offset by which to move the content.
pub delta: Axes<Rel<Length>>,
- /// The node whose contents should be moved.
+ /// The content that should be moved.
pub child: Content,
}
-#[node(Layout)]
+#[node(LayoutInline)]
impl MoveNode {
fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
let dx = args.named("dx")?.unwrap_or_default();
@@ -23,8 +23,8 @@ impl MoveNode {
}
}
-impl Layout for MoveNode {
- fn layout(
+impl LayoutInline for MoveNode {
+ fn layout_inline(
&self,
world: Tracked<dyn World>,
regions: &Regions,
@@ -40,28 +40,24 @@ impl Layout for MoveNode {
Ok(frames)
}
-
- fn level(&self) -> Level {
- Level::Inline
- }
}
-/// Transform a node without affecting layout.
+/// Transform content without affecting layout.
#[derive(Debug, Hash)]
pub struct TransformNode<const T: TransformKind> {
- /// Transformation to apply to the contents.
+ /// Transformation to apply to the content.
pub transform: Transform,
- /// The node whose contents should be transformed.
+ /// The content that should be transformed.
pub child: Content,
}
-/// Rotate a node without affecting layout.
+/// Rotate content without affecting layout.
pub type RotateNode = TransformNode<ROTATE>;
-/// Scale a node without affecting layout.
+/// Scale content without affecting layout.
pub type ScaleNode = TransformNode<SCALE>;
-#[node(Layout)]
+#[node(LayoutInline)]
impl<const T: TransformKind> TransformNode<T> {
/// The origin of the transformation.
#[property(resolve)]
@@ -85,8 +81,8 @@ impl<const T: TransformKind> TransformNode<T> {
}
}
-impl<const T: TransformKind> Layout for TransformNode<T> {
- fn layout(
+impl<const T: TransformKind> LayoutInline for TransformNode<T> {
+ fn layout_inline(
&self,
world: Tracked<dyn World>,
regions: &Regions,
@@ -106,10 +102,6 @@ impl<const T: TransformKind> Layout for TransformNode<T> {
Ok(frames)
}
-
- fn level(&self) -> Level {
- Level::Inline
- }
}
/// Kinds of transformations.
diff --git a/src/library/math/mod.rs b/src/library/math/mod.rs
index 84d4b6ee..5bb5054d 100644
--- a/src/library/math/mod.rs
+++ b/src/library/math/mod.rs
@@ -36,7 +36,7 @@ pub enum MathNode {
Row(Arc<Vec<MathNode>>, Span),
}
-#[node(Show, Layout)]
+#[node(Show, LayoutInline)]
impl MathNode {
/// The math font family.
#[property(referenced)]
@@ -67,6 +67,15 @@ impl MathNode {
self
}
+
+ /// Whether the formula is display level.
+ pub fn display(&self) -> bool {
+ if let Self::Row(row, _) = self {
+ matches!(row.as_slice(), [MathNode::Space, .., MathNode::Space])
+ } else {
+ false
+ }
+ }
}
impl Show for MathNode {
@@ -79,11 +88,10 @@ impl Show for MathNode {
}
fn realize(&self, _: Tracked<dyn World>, _: StyleChain) -> SourceResult<Content> {
- Ok(match self.level() {
- Level::Inline => self.clone().pack(),
- Level::Block => {
- self.clone().pack().aligned(Axes::with_x(Some(Align::Center.into())))
- }
+ Ok(if self.display() {
+ self.clone().pack().aligned(Axes::with_x(Some(Align::Center.into())))
+ } else {
+ self.clone().pack()
})
}
@@ -93,27 +101,22 @@ impl Show for MathNode {
styles: StyleChain,
realized: Content,
) -> SourceResult<Content> {
- Ok(match self.level() {
- Level::Inline => realized,
- Level::Block => {
- realized.spaced(styles.get(Self::ABOVE), styles.get(Self::BELOW))
- }
+ Ok(if self.display() {
+ realized.spaced(styles.get(Self::ABOVE), styles.get(Self::BELOW))
+ } else {
+ realized
})
}
}
-impl Layout for MathNode {
- fn layout(
+impl LayoutInline for MathNode {
+ fn layout_inline(
&self,
world: Tracked<dyn World>,
_: &Regions,
styles: StyleChain,
) -> SourceResult<Vec<Frame>> {
- let style = match self.level() {
- Level::Inline => Style::Text,
- Level::Block => Style::Display,
- };
-
+ let style = if self.display() { Style::Display } else { Style::Text };
let span = match self {
&Self::Row(_, span) => span,
_ => Span::detached(),
@@ -121,16 +124,6 @@ impl Layout for MathNode {
Ok(vec![layout_tex(world, self, span, style, styles)?])
}
-
- fn level(&self) -> Level {
- if let Self::Row(row, _) = self {
- if matches!(row.as_slice(), [MathNode::Space, .., MathNode::Space]) {
- return Level::Block;
- }
- }
-
- Level::Inline
- }
}
/// Layout a TeX formula into a frame.
diff --git a/src/library/mod.rs b/src/library/mod.rs
index d75e2459..184c515e 100644
--- a/src/library/mod.rs
+++ b/src/library/mod.rs
@@ -11,7 +11,14 @@ pub mod structure;
pub mod text;
pub mod utility;
-use prelude::*;
+mod ext;
+mod raw;
+
+pub use raw::*;
+
+use crate::geom::{Align, Color, Dir};
+use crate::model::{Node, Scope};
+use crate::LangItems;
/// Construct a scope containing all standard library definitions.
pub fn scope() -> Scope {
@@ -156,10 +163,10 @@ pub fn items() -> LangItems {
strong: |body| text::StrongNode(body).pack(),
emph: |body| text::EmphNode(body).pack(),
raw: |text, lang, block| {
- let node = text::RawNode { text, block }.pack();
+ let content = text::RawNode { text, block }.pack();
match lang {
- Some(_) => node.styled(text::RawNode::LANG, lang),
- None => node,
+ Some(_) => content.styled(text::RawNode::LANG, lang),
+ None => content,
}
},
link: |url| text::LinkNode::from_url(url).pack(),
@@ -174,188 +181,3 @@ pub fn items() -> LangItems {
},
}
}
-
-/// Additional methods on content.
-pub trait ContentExt {
- /// Make this content strong.
- fn strong(self) -> Self;
-
- /// Make this content emphasized.
- fn emph(self) -> Self;
-
- /// Underline this content.
- fn underlined(self) -> Self;
-
- /// Add weak vertical spacing above and below the content.
- fn spaced(self, above: Option<Abs>, below: Option<Abs>) -> Self;
-
- /// Force a size for this node.
- fn boxed(self, sizing: Axes<Option<Rel<Length>>>) -> Self;
-
- /// Set alignments for this node.
- fn aligned(self, aligns: Axes<Option<RawAlign>>) -> Self;
-
- /// Pad this node at the sides.
- fn padded(self, padding: Sides<Rel<Length>>) -> Self;
-
- /// Transform this node's contents without affecting layout.
- fn moved(self, delta: Axes<Rel<Length>>) -> Self;
-
- /// Fill the frames resulting from a node.
- fn filled(self, fill: Paint) -> Self;
-
- /// Stroke the frames resulting from a node.
- fn stroked(self, stroke: Stroke) -> Self;
-}
-
-impl ContentExt for Content {
- fn strong(self) -> Self {
- text::StrongNode(self).pack()
- }
-
- fn emph(self) -> Self {
- text::EmphNode(self).pack()
- }
-
- fn underlined(self) -> Self {
- text::DecoNode::<{ text::UNDERLINE }>(self).pack()
- }
-
- fn spaced(self, above: Option<Abs>, below: Option<Abs>) -> Self {
- if above.is_none() && below.is_none() {
- return self;
- }
-
- let mut seq = vec![];
- if let Some(above) = above {
- seq.push(
- layout::VNode {
- amount: above.into(),
- weak: true,
- generated: true,
- }
- .pack(),
- );
- }
-
- seq.push(self);
- if let Some(below) = below {
- seq.push(
- layout::VNode {
- amount: below.into(),
- weak: true,
- generated: true,
- }
- .pack(),
- );
- }
-
- Self::sequence(seq)
- }
-
- fn boxed(self, sizing: Axes<Option<Rel<Length>>>) -> Self {
- layout::BoxNode { sizing, child: self }.pack()
- }
-
- fn aligned(self, aligns: Axes<Option<RawAlign>>) -> Self {
- layout::AlignNode { aligns, child: self }.pack()
- }
-
- fn padded(self, padding: Sides<Rel<Length>>) -> Self {
- layout::PadNode { padding, child: self }.pack()
- }
-
- fn moved(self, delta: Axes<Rel<Length>>) -> Self {
- layout::MoveNode { delta, child: self }.pack()
- }
-
- fn filled(self, fill: Paint) -> Self {
- FillNode { fill, child: self }.pack()
- }
-
- fn stroked(self, stroke: Stroke) -> Self {
- StrokeNode { stroke, child: self }.pack()
- }
-}
-
-/// Additional methods for the style chain.
-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);
-}
-
-impl StyleMapExt for StyleMap {
- fn set_family(&mut self, preferred: text::FontFamily, existing: StyleChain) {
- self.set(
- text::TextNode::FAMILY,
- std::iter::once(preferred)
- .chain(existing.get(text::TextNode::FAMILY).iter().cloned())
- .collect(),
- );
- }
-}
-
-/// Fill the frames resulting from a node.
-#[derive(Debug, Hash)]
-struct FillNode {
- /// How to fill the frames resulting from the `child`.
- fill: Paint,
- /// The node whose frames should be filled.
- child: Content,
-}
-
-#[node(Layout)]
-impl FillNode {}
-
-impl Layout for FillNode {
- fn layout(
- &self,
- world: Tracked<dyn World>,
- regions: &Regions,
- styles: StyleChain,
- ) -> SourceResult<Vec<Frame>> {
- let mut frames = self.child.layout_block(world, regions, styles)?;
- for frame in &mut frames {
- let shape = Geometry::Rect(frame.size()).filled(self.fill);
- frame.prepend(Point::zero(), Element::Shape(shape));
- }
- Ok(frames)
- }
-
- fn level(&self) -> Level {
- Level::Block
- }
-}
-
-/// Stroke the frames resulting from a node.
-#[derive(Debug, Hash)]
-struct StrokeNode {
- /// How to stroke the frames resulting from the `child`.
- stroke: Stroke,
- /// The node whose frames should be stroked.
- child: Content,
-}
-
-#[node(Layout)]
-impl StrokeNode {}
-
-impl Layout for StrokeNode {
- fn layout(
- &self,
- world: Tracked<dyn World>,
- regions: &Regions,
- styles: StyleChain,
- ) -> SourceResult<Vec<Frame>> {
- let mut frames = self.child.layout_block(world, regions, styles)?;
- for frame in &mut frames {
- let shape = Geometry::Rect(frame.size()).stroked(self.stroke);
- frame.prepend(Point::zero(), Element::Shape(shape));
- }
- Ok(frames)
- }
-
- fn level(&self) -> Level {
- Level::Block
- }
-}
diff --git a/src/library/prelude.rs b/src/library/prelude.rs
index 756904f0..66e35e68 100644
--- a/src/library/prelude.rs
+++ b/src/library/prelude.rs
@@ -7,19 +7,20 @@ pub use std::num::NonZeroUsize;
pub use std::sync::Arc;
pub use comemo::Tracked;
-pub use typst_macros::node;
-pub use super::{ContentExt, StyleMapExt};
+pub use super::ext::{ContentExt, StyleMapExt};
+pub use super::layout::{Layout, LayoutBlock, LayoutInline, Regions};
+pub use super::text::TextNode;
+pub use super::{RawAlign, RawStroke};
pub use crate::diag::{
with_alternative, At, FileError, FileResult, SourceError, SourceResult, StrResult,
};
pub use crate::frame::*;
pub use crate::geom::*;
-pub use crate::library::text::TextNode;
pub use crate::model::{
- Arg, Args, Array, Cast, Content, Dict, Dynamic, Fold, Func, Key, Layout, Level, Node,
- RawAlign, RawStroke, Regions, Resolve, Scope, Selector, Show, Smart, Str, StyleChain,
- StyleMap, StyleVec, Value, Vm,
+ capability, node, Arg, Args, Array, Capability, Cast, Content, Dict, Dynamic, Fold,
+ Func, Key, Node, Resolve, Scope, Selector, Show, Smart, Str, StyleChain, StyleMap,
+ StyleVec, Value, Vm,
};
pub use crate::syntax::{Span, Spanned};
pub use crate::util::EcoString;
diff --git a/src/model/raw.rs b/src/library/raw.rs
index b9242b51..67aa651d 100644
--- a/src/model/raw.rs
+++ b/src/library/raw.rs
@@ -1,8 +1,8 @@
use std::fmt::{self, Debug, Formatter};
-use super::{Fold, Resolve, Smart, StyleChain, Value};
use crate::geom::{Abs, Align, Axes, Axis, Get, Length, Paint, Stroke};
use crate::library::text::TextNode;
+use crate::model::{Fold, Resolve, Smart, StyleChain, Value};
/// The unresolved alignment representation.
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
diff --git a/src/library/structure/table.rs b/src/library/structure/table.rs
index d5e8920e..8a4eb302 100644
--- a/src/library/structure/table.rs
+++ b/src/library/structure/table.rs
@@ -8,7 +8,7 @@ pub struct TableNode {
pub tracks: Axes<Vec<TrackSizing>>,
/// Defines sizing of gutter rows and columns between content.
pub gutter: Axes<Vec<TrackSizing>>,
- /// The nodes to be arranged in the table.
+ /// The content to be arranged in the table.
pub cells: Vec<Content>,
}
diff --git a/src/library/text/par.rs b/src/library/text/par.rs
index 7c862366..50089b20 100644
--- a/src/library/text/par.rs
+++ b/src/library/text/par.rs
@@ -22,11 +22,11 @@ pub enum ParChild {
Quote { double: bool },
/// Horizontal spacing between other children.
Spacing(Spacing),
- /// An arbitrary inline-level node.
- Node(Content),
+ /// Arbitrary inline-level content.
+ Inline(Content),
}
-#[node(Layout)]
+#[node(LayoutBlock)]
impl ParNode {
/// The spacing between lines.
#[property(resolve)]
@@ -61,8 +61,8 @@ impl ParNode {
}
}
-impl Layout for ParNode {
- fn layout(
+impl LayoutBlock for ParNode {
+ fn layout_block(
&self,
world: Tracked<dyn World>,
regions: &Regions,
@@ -82,10 +82,6 @@ impl Layout for ParNode {
// Stack the lines into one frame per region.
stack(&p, world, &lines, regions)
}
-
- fn level(&self) -> Level {
- Level::Block
- }
}
impl Debug for ParNode {
@@ -101,7 +97,7 @@ impl Debug for ParChild {
Self::Text(text) => write!(f, "Text({:?})", text),
Self::Quote { double } => write!(f, "Quote({double})"),
Self::Spacing(kind) => write!(f, "{:?}", kind),
- Self::Node(node) => node.fmt(f),
+ Self::Inline(inline) => inline.fmt(f),
}
}
}
@@ -180,19 +176,19 @@ impl ParbreakNode {
}
}
-/// A node that should be repeated to fill up a line.
+/// Repeats content to fill a line.
#[derive(Debug, Hash)]
pub struct RepeatNode(pub Content);
-#[node(Layout)]
+#[node(LayoutInline)]
impl RepeatNode {
fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
Ok(Self(args.expect("body")?).pack())
}
}
-impl Layout for RepeatNode {
- fn layout(
+impl LayoutInline for RepeatNode {
+ fn layout_inline(
&self,
world: Tracked<dyn World>,
regions: &Regions,
@@ -200,16 +196,12 @@ impl Layout for RepeatNode {
) -> SourceResult<Vec<Frame>> {
self.0.layout_inline(world, regions, styles)
}
-
- fn level(&self) -> Level {
- Level::Inline
- }
}
/// Range of a substring of text.
type Range = std::ops::Range<usize>;
-// The characters by which spacing, nodes and pins are replaced in the
+// The characters by which spacing, inline content and pins are replaced in the
// paragraph's full text.
const SPACING_REPLACE: char = ' '; // Space
const NODE_REPLACE: char = '\u{FFFC}'; // Object Replacement Character
@@ -291,8 +283,8 @@ enum Segment<'a> {
Text(usize),
/// Horizontal spacing between other segments.
Spacing(Spacing),
- /// An arbitrary inline-level layout node.
- Node(&'a Content),
+ /// Arbitrary inline-level content.
+ Inline(&'a Content),
}
impl Segment<'_> {
@@ -301,7 +293,7 @@ impl Segment<'_> {
match *self {
Self::Text(len) => len,
Self::Spacing(_) => SPACING_REPLACE.len_utf8(),
- Self::Node(_) => NODE_REPLACE.len_utf8(),
+ Self::Inline(_) => NODE_REPLACE.len_utf8(),
}
}
}
@@ -315,9 +307,9 @@ enum Item<'a> {
Absolute(Abs),
/// Fractional spacing between other items.
Fractional(Fr),
- /// A layouted child node.
+ /// Layouted inline-level content.
Frame(Frame),
- /// A repeating node that fills the remaining space.
+ /// A repeating node that fills the remaining space in a line.
Repeat(&'a RepeatNode, StyleChain<'a>),
}
@@ -475,7 +467,7 @@ fn collect<'a>(
ParChild::Text(text) => text.chars().next(),
ParChild::Quote { .. } => Some('"'),
ParChild::Spacing(_) => Some(SPACING_REPLACE),
- ParChild::Node(_) => Some(NODE_REPLACE),
+ ParChild::Inline(_) => Some(NODE_REPLACE),
});
full.push_str(quoter.quote(&quotes, double, peeked));
@@ -488,9 +480,9 @@ fn collect<'a>(
full.push(SPACING_REPLACE);
Segment::Spacing(spacing)
}
- ParChild::Node(node) => {
+ ParChild::Inline(inline) => {
full.push(NODE_REPLACE);
- Segment::Node(node)
+ Segment::Inline(inline)
}
};
@@ -514,7 +506,7 @@ fn collect<'a>(
}
/// Prepare paragraph layout by shaping the whole paragraph and layouting all
-/// contained inline-level nodes.
+/// contained inline-level content.
fn prepare<'a>(
world: Tracked<dyn World>,
par: &'a ParNode,
@@ -548,13 +540,13 @@ fn prepare<'a>(
items.push(Item::Fractional(v));
}
},
- Segment::Node(node) => {
- if let Some(repeat) = node.downcast::<RepeatNode>() {
+ Segment::Inline(inline) => {
+ if let Some(repeat) = inline.downcast::<RepeatNode>() {
items.push(Item::Repeat(repeat, styles));
} else {
let size = Size::new(regions.first.x, regions.base.y);
let pod = Regions::one(size, regions.base, Axes::splat(false));
- let mut frame = node.layout_inline(world, &pod, styles)?.remove(0);
+ let mut frame = inline.layout_inline(world, &pod, styles)?.remove(0);
frame.translate(Point::with_y(styles.get(TextNode::BASELINE)));
frame.apply_role(Role::GenericInline);
items.push(Item::Frame(frame));
@@ -1169,12 +1161,12 @@ fn commit(
Item::Frame(frame) => {
push(&mut offset, frame.clone());
}
- Item::Repeat(node, styles) => {
+ Item::Repeat(repeat, styles) => {
let before = offset;
let fill = Fr::one().share(fr, remaining);
let size = Size::new(fill, regions.base.y);
let pod = Regions::one(size, regions.base, Axes::new(false, false));
- let frame = node.layout(world, &pod, *styles)?.remove(0);
+ let frame = repeat.layout_inline(world, &pod, *styles)?.remove(0);
let width = frame.width();
let count = (fill / width).floor();
let remaining = fill % width;
diff --git a/src/model/capture.rs b/src/model/capture.rs
deleted file mode 100644
index c4c107b2..00000000
--- a/src/model/capture.rs
+++ /dev/null
@@ -1,186 +0,0 @@
-use super::{Scope, Scopes, Value};
-use crate::syntax::ast::TypedNode;
-use crate::syntax::{ast, SyntaxNode};
-
-/// A visitor that captures variable slots.
-pub struct CapturesVisitor<'a> {
- external: &'a Scopes<'a>,
- internal: Scopes<'a>,
- captures: Scope,
-}
-
-impl<'a> CapturesVisitor<'a> {
- /// Create a new visitor for the given external scopes.
- pub fn new(external: &'a Scopes) -> Self {
- Self {
- external,
- internal: Scopes::new(None),
- captures: Scope::new(),
- }
- }
-
- /// Return the scope of captured variables.
- pub fn finish(self) -> Scope {
- self.captures
- }
-
- /// Bind a new internal variable.
- pub fn bind(&mut self, ident: ast::Ident) {
- self.internal.top.define(ident.take(), Value::None);
- }
-
- /// Capture a variable if it isn't internal.
- pub fn capture(&mut self, ident: ast::Ident) {
- if self.internal.get(&ident).is_err() {
- if let Ok(value) = self.external.get(&ident) {
- self.captures.define_captured(ident.take(), value.clone());
- }
- }
- }
-
- /// Visit any node and collect all captured variables.
- pub fn visit(&mut self, node: &SyntaxNode) {
- match node.cast() {
- // Every identifier is a potential variable that we need to capture.
- // Identifiers that shouldn't count as captures because they
- // actually bind a new name are handled below (individually through
- // the expressions that contain them).
- Some(ast::Expr::Ident(ident)) => self.capture(ident),
-
- // Code and content blocks create a scope.
- Some(ast::Expr::Code(_) | ast::Expr::Content(_)) => {
- self.internal.enter();
- for child in node.children() {
- self.visit(child);
- }
- self.internal.exit();
- }
-
- // A closure contains parameter bindings, which are bound before the
- // body is evaluated. Care must be taken so that the default values
- // of named parameters cannot access previous parameter bindings.
- Some(ast::Expr::Closure(expr)) => {
- for param in expr.params() {
- if let ast::Param::Named(named) = param {
- self.visit(named.expr().as_untyped());
- }
- }
-
- for param in expr.params() {
- match param {
- ast::Param::Pos(ident) => self.bind(ident),
- ast::Param::Named(named) => self.bind(named.name()),
- ast::Param::Sink(ident) => self.bind(ident),
- }
- }
-
- self.visit(expr.body().as_untyped());
- }
-
- // A let expression contains a binding, but that binding is only
- // active after the body is evaluated.
- Some(ast::Expr::Let(expr)) => {
- if let Some(init) = expr.init() {
- self.visit(init.as_untyped());
- }
- self.bind(expr.binding());
- }
-
- // A show rule contains a binding, but that binding is only active
- // after the target has been evaluated.
- Some(ast::Expr::Show(show)) => {
- self.visit(show.pattern().as_untyped());
- if let Some(binding) = show.binding() {
- self.bind(binding);
- }
- self.visit(show.body().as_untyped());
- }
-
- // A for loop contains one or two bindings in its pattern. These are
- // active after the iterable is evaluated but before the body is
- // evaluated.
- Some(ast::Expr::For(expr)) => {
- self.visit(expr.iter().as_untyped());
- let pattern = expr.pattern();
- if let Some(key) = pattern.key() {
- self.bind(key);
- }
- self.bind(pattern.value());
- self.visit(expr.body().as_untyped());
- }
-
- // An import contains items, but these are active only after the
- // path is evaluated.
- Some(ast::Expr::Import(expr)) => {
- self.visit(expr.path().as_untyped());
- if let ast::Imports::Items(items) = expr.imports() {
- for item in items {
- self.bind(item);
- }
- }
- }
-
- // Everything else is traversed from left to right.
- _ => {
- for child in node.children() {
- self.visit(child);
- }
- }
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::syntax::parse;
-
- #[track_caller]
- fn test(text: &str, result: &[&str]) {
- let mut scopes = Scopes::new(None);
- scopes.top.define("x", 0);
- scopes.top.define("y", 0);
- scopes.top.define("z", 0);
-
- let mut visitor = CapturesVisitor::new(&scopes);
- let root = parse(text);
- visitor.visit(&root);
-
- let captures = visitor.finish();
- let mut names: Vec<_> = captures.iter().map(|(k, _)| k).collect();
- names.sort();
-
- assert_eq!(names, result);
- }
-
- #[test]
- fn test_captures() {
- // Let binding and function definition.
- test("#let x = x", &["x"]);
- test("#let x; {x + y}", &["y"]);
- test("#let f(x, y) = x + y", &[]);
-
- // Closure with different kinds of params.
- test("{(x, y) => x + z}", &["z"]);
- test("{(x: y, z) => x + z}", &["y"]);
- test("{(..x) => x + y}", &["y"]);
- test("{(x, y: x + z) => x + y}", &["x", "z"]);
-
- // Show rule.
- test("#show x: y as x", &["y"]);
- test("#show x: y as x + z", &["y", "z"]);
- test("#show x: x as x", &["x"]);
-
- // For loop.
- test("#for x in y { x + z }", &["y", "z"]);
- test("#for x, y in y { x + y }", &["y"]);
-
- // Import.
- test("#import x, y from z", &["z"]);
- test("#import x, y, z from x + y", &["x", "y"]);
-
- // Blocks.
- test("{ let x = 1; { let y = 2; y }; x + y }", &["y"]);
- test("[#let x = 1]#x", &["x"]);
- }
-}
diff --git a/src/model/collapse.rs b/src/model/collapse.rs
deleted file mode 100644
index f57a3a42..00000000
--- a/src/model/collapse.rs
+++ /dev/null
@@ -1,116 +0,0 @@
-use super::{StyleChain, StyleVec, StyleVecBuilder};
-
-/// A wrapper around a [`StyleVecBuilder`] that allows to collapse items.
-pub(super) 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.
- pub fn new() -> Self {
- Self {
- builder: StyleVecBuilder::new(),
- staged: vec![],
- last: Last::Destructive,
- }
- }
-
- /// Whether the builder is empty.
- pub 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.
- pub 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 {
- if let Some(i) =
- self.staged.iter().position(|(prev_item, _, prev_weakness)| {
- prev_weakness.map_or(false, |prev_weakness| {
- weakness < prev_weakness
- || (weakness == prev_weakness && item > *prev_item)
- })
- })
- {
- self.staged.remove(i);
- } else {
- return;
- }
- }
-
- self.staged.push((item, styles, Some(weakness)));
- self.last = Last::Weak;
- }
-
- /// Forces nearby weak items to collapse.
- pub 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.
- pub 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.
- pub fn ignorant(&mut self, item: T, styles: StyleChain<'a>) {
- self.staged.push((item, styles, None));
- }
-
- /// Iterate over the contained items.
- pub 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.
- pub 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/src/model/content.rs b/src/model/content.rs
index 170d47a1..1cffa773 100644
--- a/src/model/content.rs
+++ b/src/model/content.rs
@@ -5,18 +5,12 @@ use std::iter::{self, Sum};
use std::ops::{Add, AddAssign};
use std::sync::Arc;
-use comemo::Tracked;
use siphasher::sip128::{Hasher128, SipHasher};
use typst_macros::node;
-use super::{
- Args, Barrier, Builder, Key, Layout, Level, Property, Regions, Scratch, Selector,
- StyleChain, StyleEntry, StyleMap, Vm,
-};
+use super::{Args, Key, Property, Selector, StyleEntry, StyleMap, Vm};
use crate::diag::{SourceResult, StrResult};
-use crate::frame::Frame;
use crate::util::ReadableTypeId;
-use crate::World;
/// Composable representation of styled content.
///
@@ -40,22 +34,27 @@ impl Content {
}
}
+ /// Whether the content is empty.
pub fn is_empty(&self) -> bool {
self.downcast::<SequenceNode>().map_or(false, |seq| seq.0.is_empty())
}
+ /// The id of the contained node.
pub fn id(&self) -> NodeId {
(*self.0).id()
}
+ /// Whether the contained node is of type `T`.
pub fn is<T: 'static>(&self) -> bool {
(*self.0).as_any().is::<T>()
}
+ /// Cast to `T` if the contained node is of type `T`.
pub fn downcast<T: 'static>(&self) -> Option<&T> {
(*self.0).as_any().downcast_ref::<T>()
}
+ /// Try to cast to a mutable instance of `T`.
fn try_downcast_mut<T: 'static>(&mut self) -> Option<&mut T> {
Arc::get_mut(&mut self.0)?.as_any_mut().downcast_mut::<T>()
}
@@ -120,51 +119,6 @@ impl Content {
pub fn unguard(&self, sel: Selector) -> Self {
self.clone().styled_with_entry(StyleEntry::Unguard(sel))
}
-
- #[comemo::memoize]
- pub fn layout_block(
- &self,
- world: Tracked<dyn World>,
- regions: &Regions,
- styles: StyleChain,
- ) -> SourceResult<Vec<Frame>> {
- let barrier = StyleEntry::Barrier(Barrier::new(self.id()));
- let styles = barrier.chain(&styles);
-
- if let Some(node) = self.to::<dyn Layout>() {
- if node.level() == Level::Block {
- return node.layout(world, regions, styles);
- }
- }
-
- let scratch = Scratch::default();
- let mut builder = Builder::new(world, &scratch, false);
- builder.accept(self, styles)?;
- let (flow, shared) = builder.into_flow(styles)?;
- flow.layout(world, regions, shared)
- }
-
-
- #[comemo::memoize]
- pub fn layout_inline(
- &self,
- world: Tracked<dyn World>,
- regions: &Regions,
- styles: StyleChain,
- ) -> SourceResult<Vec<Frame>> {
- let barrier = StyleEntry::Barrier(Barrier::new(self.id()));
- let styles = barrier.chain(&styles);
-
- if let Some(node) = self.to::<dyn Layout>() {
- return node.layout(world, regions, styles);
- }
-
- let scratch = Scratch::default();
- let mut builder = Builder::new(world, &scratch, false);
- builder.accept(self, styles)?;
- let (flow, shared) = builder.into_flow(styles)?;
- flow.layout(world, regions, shared)
- }
}
impl Default for Content {
@@ -293,6 +247,11 @@ pub trait Node: 'static {
fn vtable(&self, id: TypeId) -> Option<*const ()>;
}
+/// A capability a node can have.
+///
+/// This is implemented by trait objects.
+pub trait Capability: 'static + Send + Sync {}
+
/// A unique identifier for a node.
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub struct NodeId(ReadableTypeId);
@@ -310,15 +269,12 @@ impl Debug for NodeId {
}
}
-/// A capability a node can have.
-///
-/// This is implemented by trait objects.
-pub trait Capability: 'static + Send + Sync {}
-
/// A node with applied styles.
#[derive(Clone, Hash)]
pub struct StyledNode {
+ /// The styled content.
pub sub: Content,
+ /// The styles.
pub map: StyleMap,
}
diff --git a/src/model/fold.rs b/src/model/fold.rs
deleted file mode 100644
index bc85be01..00000000
--- a/src/model/fold.rs
+++ /dev/null
@@ -1,81 +0,0 @@
-use super::Smart;
-use crate::geom::{Abs, Corners, Length, Rel, Sides};
-
-/// A property that is folded to determine its final value.
-pub trait Fold {
- /// The type of the folded output.
- type Output;
-
- /// Fold this inner value with an outer folded value.
- fn fold(self, outer: Self::Output) -> Self::Output;
-}
-
-impl<T> Fold for Option<T>
-where
- T: Fold,
- T::Output: Default,
-{
- type Output = Option<T::Output>;
-
- fn fold(self, outer: Self::Output) -> Self::Output {
- self.map(|inner| inner.fold(outer.unwrap_or_default()))
- }
-}
-
-impl<T> Fold for Smart<T>
-where
- T: Fold,
- T::Output: Default,
-{
- type Output = Smart<T::Output>;
-
- fn fold(self, outer: Self::Output) -> Self::Output {
- self.map(|inner| inner.fold(outer.unwrap_or_default()))
- }
-}
-
-impl<T> Fold for Sides<T>
-where
- T: Fold,
-{
- type Output = Sides<T::Output>;
-
- fn fold(self, outer: Self::Output) -> Self::Output {
- self.zip(outer, |inner, outer| inner.fold(outer))
- }
-}
-
-impl Fold for Sides<Option<Rel<Abs>>> {
- type Output = Sides<Rel<Abs>>;
-
- fn fold(self, outer: Self::Output) -> Self::Output {
- self.zip(outer, |inner, outer| inner.unwrap_or(outer))
- }
-}
-
-impl Fold for Sides<Option<Smart<Rel<Length>>>> {
- type Output = Sides<Smart<Rel<Length>>>;
-
- fn fold(self, outer: Self::Output) -> Self::Output {
- self.zip(outer, |inner, outer| inner.unwrap_or(outer))
- }
-}
-
-impl<T> Fold for Corners<T>
-where
- T: Fold,
-{
- type Output = Corners<T::Output>;
-
- fn fold(self, outer: Self::Output) -> Self::Output {
- self.zip(outer, |inner, outer| inner.fold(outer))
- }
-}
-
-impl Fold for Corners<Option<Rel<Abs>>> {
- type Output = Corners<Rel<Abs>>;
-
- fn fold(self, outer: Self::Output) -> Self::Output {
- self.zip(outer, |inner, outer| inner.unwrap_or(outer))
- }
-}
diff --git a/src/model/func.rs b/src/model/func.rs
index 47ad4436..dff58233 100644
--- a/src/model/func.rs
+++ b/src/model/func.rs
@@ -6,8 +6,8 @@ use comemo::{Track, Tracked};
use super::{Args, Eval, Flow, Node, NodeId, Route, Scope, Scopes, StyleMap, Value, Vm};
use crate::diag::{SourceResult, StrResult};
-use crate::syntax::ast::Expr;
-use crate::syntax::SourceId;
+use crate::syntax::ast::{self, Expr, TypedNode};
+use crate::syntax::{SourceId, SyntaxNode};
use crate::util::EcoString;
use crate::World;
@@ -227,3 +227,186 @@ impl Closure {
result
}
}
+
+/// A visitor that determines which variables to capture for a closure.
+pub struct CapturesVisitor<'a> {
+ external: &'a Scopes<'a>,
+ internal: Scopes<'a>,
+ captures: Scope,
+}
+
+impl<'a> CapturesVisitor<'a> {
+ /// Create a new visitor for the given external scopes.
+ pub fn new(external: &'a Scopes) -> Self {
+ Self {
+ external,
+ internal: Scopes::new(None),
+ captures: Scope::new(),
+ }
+ }
+
+ /// Return the scope of captured variables.
+ pub fn finish(self) -> Scope {
+ self.captures
+ }
+
+ /// Bind a new internal variable.
+ pub fn bind(&mut self, ident: ast::Ident) {
+ self.internal.top.define(ident.take(), Value::None);
+ }
+
+ /// Capture a variable if it isn't internal.
+ pub fn capture(&mut self, ident: ast::Ident) {
+ if self.internal.get(&ident).is_err() {
+ if let Ok(value) = self.external.get(&ident) {
+ self.captures.define_captured(ident.take(), value.clone());
+ }
+ }
+ }
+
+ /// Visit any node and collect all captured variables.
+ pub fn visit(&mut self, node: &SyntaxNode) {
+ match node.cast() {
+ // Every identifier is a potential variable that we need to capture.
+ // Identifiers that shouldn't count as captures because they
+ // actually bind a new name are handled below (individually through
+ // the expressions that contain them).
+ Some(ast::Expr::Ident(ident)) => self.capture(ident),
+
+ // Code and content blocks create a scope.
+ Some(ast::Expr::Code(_) | ast::Expr::Content(_)) => {
+ self.internal.enter();
+ for child in node.children() {
+ self.visit(child);
+ }
+ self.internal.exit();
+ }
+
+ // A closure contains parameter bindings, which are bound before the
+ // body is evaluated. Care must be taken so that the default values
+ // of named parameters cannot access previous parameter bindings.
+ Some(ast::Expr::Closure(expr)) => {
+ for param in expr.params() {
+ if let ast::Param::Named(named) = param {
+ self.visit(named.expr().as_untyped());
+ }
+ }
+
+ for param in expr.params() {
+ match param {
+ ast::Param::Pos(ident) => self.bind(ident),
+ ast::Param::Named(named) => self.bind(named.name()),
+ ast::Param::Sink(ident) => self.bind(ident),
+ }
+ }
+
+ self.visit(expr.body().as_untyped());
+ }
+
+ // A let expression contains a binding, but that binding is only
+ // active after the body is evaluated.
+ Some(ast::Expr::Let(expr)) => {
+ if let Some(init) = expr.init() {
+ self.visit(init.as_untyped());
+ }
+ self.bind(expr.binding());
+ }
+
+ // A show rule contains a binding, but that binding is only active
+ // after the target has been evaluated.
+ Some(ast::Expr::Show(show)) => {
+ self.visit(show.pattern().as_untyped());
+ if let Some(binding) = show.binding() {
+ self.bind(binding);
+ }
+ self.visit(show.body().as_untyped());
+ }
+
+ // A for loop contains one or two bindings in its pattern. These are
+ // active after the iterable is evaluated but before the body is
+ // evaluated.
+ Some(ast::Expr::For(expr)) => {
+ self.visit(expr.iter().as_untyped());
+ let pattern = expr.pattern();
+ if let Some(key) = pattern.key() {
+ self.bind(key);
+ }
+ self.bind(pattern.value());
+ self.visit(expr.body().as_untyped());
+ }
+
+ // An import contains items, but these are active only after the
+ // path is evaluated.
+ Some(ast::Expr::Import(expr)) => {
+ self.visit(expr.path().as_untyped());
+ if let ast::Imports::Items(items) = expr.imports() {
+ for item in items {
+ self.bind(item);
+ }
+ }
+ }
+
+ // Everything else is traversed from left to right.
+ _ => {
+ for child in node.children() {
+ self.visit(child);
+ }
+ }
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::syntax::parse;
+
+ #[track_caller]
+ fn test(text: &str, result: &[&str]) {
+ let mut scopes = Scopes::new(None);
+ scopes.top.define("x", 0);
+ scopes.top.define("y", 0);
+ scopes.top.define("z", 0);
+
+ let mut visitor = CapturesVisitor::new(&scopes);
+ let root = parse(text);
+ visitor.visit(&root);
+
+ let captures = visitor.finish();
+ let mut names: Vec<_> = captures.iter().map(|(k, _)| k).collect();
+ names.sort();
+
+ assert_eq!(names, result);
+ }
+
+ #[test]
+ fn test_captures() {
+ // Let binding and function definition.
+ test("#let x = x", &["x"]);
+ test("#let x; {x + y}", &["y"]);
+ test("#let f(x, y) = x + y", &[]);
+
+ // Closure with different kinds of params.
+ test("{(x, y) => x + z}", &["z"]);
+ test("{(x: y, z) => x + z}", &["y"]);
+ test("{(..x) => x + y}", &["y"]);
+ test("{(x, y: x + z) => x + y}", &["x", "z"]);
+
+ // Show rule.
+ test("#show x: y as x", &["y"]);
+ test("#show x: y as x + z", &["y", "z"]);
+ test("#show x: x as x", &["x"]);
+
+ // For loop.
+ test("#for x in y { x + z }", &["y", "z"]);
+ test("#for x, y in y { x + y }", &["y"]);
+
+ // Import.
+ test("#import x, y from z", &["z"]);
+ test("#import x, y, z from x + y", &["x", "y"]);
+
+ // Blocks.
+ test("{ let x = 1; { let y = 2; y }; x + y }", &["y"]);
+ test("[#let x = 1]#x", &["x"]);
+ }
+}
diff --git a/src/model/layout.rs b/src/model/layout.rs
deleted file mode 100644
index e8aa0e96..00000000
--- a/src/model/layout.rs
+++ /dev/null
@@ -1,142 +0,0 @@
-//! Layouting infrastructure.
-
-use std::hash::Hash;
-
-use comemo::Tracked;
-
-use super::{Builder, Capability, Content, Scratch, StyleChain};
-use crate::diag::SourceResult;
-use crate::frame::Frame;
-use crate::geom::{Abs, Axes, Size};
-use crate::World;
-
-/// Layout content into a collection of pages.
-#[comemo::memoize]
-pub fn layout(world: Tracked<dyn World>, content: &Content) -> SourceResult<Vec<Frame>> {
- let styles = StyleChain::with_root(&world.config().styles);
- let scratch = Scratch::default();
-
- let mut builder = Builder::new(world, &scratch, true);
- builder.accept(content, styles)?;
-
- let (doc, shared) = builder.into_doc(styles)?;
- doc.layout(world, shared)
-}
-
-/// A node that can be layouted into a sequence of regions.
-///
-/// Layouting returns one frame per used region.
-pub trait Layout: 'static + Sync + Send {
- /// Layout this node into the given regions, producing frames.
- fn layout(
- &self,
- world: Tracked<dyn World>,
- regions: &Regions,
- styles: StyleChain,
- ) -> SourceResult<Vec<Frame>>;
-
- /// Whether this is an inline-level or block-level node.
- fn level(&self) -> Level;
-}
-
-impl Capability for dyn Layout {}
-
-/// At which level a node operates.
-#[derive(Debug, Copy, Clone, Eq, PartialEq)]
-pub enum Level {
- Inline,
- Block,
-}
-
-/// A sequence of regions to layout into.
-#[derive(Debug, Clone, Hash)]
-pub struct Regions {
- /// The (remaining) size of the first region.
- pub first: Size,
- /// The base size for relative sizing.
- pub base: Size,
- /// The height of followup regions. The width is the same for all regions.
- pub backlog: Vec<Abs>,
- /// The height of the final region that is repeated once the backlog is
- /// drained. The width is the same for all regions.
- pub last: Option<Abs>,
- /// Whether nodes should expand to fill the regions instead of shrinking to
- /// fit the content.
- pub expand: Axes<bool>,
-}
-
-impl Regions {
- /// Create a new region sequence with exactly one region.
- pub fn one(size: Size, base: Size, expand: Axes<bool>) -> Self {
- Self {
- first: size,
- base,
- backlog: vec![],
- last: None,
- expand,
- }
- }
-
- /// Create a new sequence of same-size regions that repeats indefinitely.
- pub fn repeat(size: Size, base: Size, expand: Axes<bool>) -> Self {
- Self {
- first: size,
- base,
- backlog: vec![],
- last: Some(size.y),
- expand,
- }
- }
-
- /// Create new regions where all sizes are mapped with `f`.
- ///
- /// Note that since all regions must have the same width, the width returned
- /// by `f` is ignored for the backlog and the final region.
- pub fn map<F>(&self, mut f: F) -> Self
- where
- F: FnMut(Size) -> Size,
- {
- let x = self.first.x;
- Self {
- first: f(self.first),
- base: f(self.base),
- backlog: self.backlog.iter().map(|&y| f(Size::new(x, y)).y).collect(),
- last: self.last.map(|y| f(Size::new(x, y)).y),
- expand: self.expand,
- }
- }
-
- /// Whether the first region is full and a region break is called for.
- pub fn is_full(&self) -> bool {
- Abs::zero().fits(self.first.y) && !self.in_last()
- }
-
- /// Whether the first region is the last usable region.
- ///
- /// If this is true, calling `next()` will have no effect.
- pub fn in_last(&self) -> bool {
- self.backlog.len() == 0 && self.last.map_or(true, |height| self.first.y == height)
- }
-
- /// Advance to the next region if there is any.
- pub fn next(&mut self) {
- if let Some(height) = (!self.backlog.is_empty())
- .then(|| self.backlog.remove(0))
- .or(self.last)
- {
- self.first.y = height;
- self.base.y = height;
- }
- }
-
- /// An iterator that returns the sizes of the first and all following
- /// regions, equivalently to what would be produced by calling
- /// [`next()`](Self::next) repeatedly until all regions are exhausted.
- /// This iterater may be infinite.
- pub fn iter(&self) -> impl Iterator<Item = Size> + '_ {
- let first = std::iter::once(self.first);
- let backlog = self.backlog.iter();
- let last = self.last.iter().cycle();
- first.chain(backlog.chain(last).map(|&h| Size::new(self.first.x, h)))
- }
-}
diff --git a/src/model/mod.rs b/src/model/mod.rs
index aba9514c..b4f8f653 100644
--- a/src/model/mod.rs
+++ b/src/model/mod.rs
@@ -1,14 +1,6 @@
//! Layout and computation model.
#[macro_use]
-mod styles;
-mod collapse;
-mod content;
-mod eval;
-mod layout;
-mod property;
-mod recipe;
-#[macro_use]
mod cast;
#[macro_use]
mod array;
@@ -18,38 +10,29 @@ mod dict;
mod str;
#[macro_use]
mod value;
+#[macro_use]
+mod styles;
mod args;
-mod capture;
-mod fold;
+mod content;
+mod eval;
mod func;
-pub mod methods;
-pub mod ops;
-mod raw;
-mod realize;
-mod resolve;
mod scope;
mod vm;
+pub mod methods;
+pub mod ops;
+
pub use self::str::*;
pub use args::*;
pub use array::*;
-pub use capture::*;
pub use cast::*;
pub use content::*;
pub use dict::*;
pub use eval::*;
-pub use fold::*;
pub use func::*;
-pub use layout::*;
-pub use property::*;
-pub use raw::*;
-pub use recipe::*;
-pub use resolve::*;
pub use scope::*;
pub use styles::*;
-pub use typst_macros::node;
pub use value::*;
pub use vm::*;
-// use collapse::*;
-use realize::*;
+pub use typst_macros::{capability, node};
diff --git a/src/model/ops.rs b/src/model/ops.rs
index 7a8c6950..7eb814c1 100644
--- a/src/model/ops.rs
+++ b/src/model/ops.rs
@@ -2,10 +2,11 @@
use std::cmp::Ordering;
-use super::{Node, RawAlign, RawStroke, Regex, Smart, Value};
+use super::{Node, Regex, Smart, Value};
use crate::diag::StrResult;
use crate::geom::{Axes, Axis, Length, Numeric, Rel};
use crate::library::text::TextNode;
+use crate::library::{RawAlign, RawStroke};
use Value::*;
/// Bail with a type mismatch error.
diff --git a/src/model/property.rs b/src/model/property.rs
deleted file mode 100644
index 3a498d2c..00000000
--- a/src/model/property.rs
+++ /dev/null
@@ -1,195 +0,0 @@
-use std::any::Any;
-use std::fmt::{self, Debug, Formatter};
-use std::hash::Hash;
-use std::sync::Arc;
-
-use comemo::Prehashed;
-
-use super::{Interruption, NodeId, StyleChain};
-use crate::library::layout::PageNode;
-use crate::library::structure::{DescNode, EnumNode, ListNode};
-use crate::library::text::ParNode;
-use crate::util::ReadableTypeId;
-
-/// A style property originating from a set rule or constructor.
-#[derive(Clone, Hash)]
-pub struct Property {
- /// The id of the property's [key](Key).
- key: KeyId,
- /// The id of the node the property belongs to.
- node: NodeId,
- /// Whether the property should only affect the first node down the
- /// hierarchy. Used by constructors.
- scoped: bool,
- /// The property's value.
- value: Arc<Prehashed<dyn Bounds>>,
- /// The name of the property.
- #[cfg(debug_assertions)]
- name: &'static str,
-}
-
-impl Property {
- /// Create a new property from a key-value pair.
- pub fn new<'a, K: Key<'a>>(_: K, value: K::Value) -> Self {
- Self {
- key: KeyId::of::<K>(),
- node: K::node(),
- value: Arc::new(Prehashed::new(value)),
- scoped: false,
- #[cfg(debug_assertions)]
- name: K::NAME,
- }
- }
-
- /// Whether this property has the given key.
- pub fn is<'a, K: Key<'a>>(&self) -> bool {
- self.key == KeyId::of::<K>()
- }
-
- /// Whether this property belongs to the node `T`.
- pub fn is_of<T: 'static>(&self) -> bool {
- self.node == NodeId::of::<T>()
- }
-
- /// Access the property's value if it is of the given key.
- pub fn downcast<'a, K: Key<'a>>(&'a self) -> Option<&'a K::Value> {
- if self.key == KeyId::of::<K>() {
- (**self.value).as_any().downcast_ref()
- } else {
- None
- }
- }
-
- /// The node this property is for.
- pub fn node(&self) -> NodeId {
- self.node
- }
-
- /// Whether the property is scoped.
- pub fn scoped(&self) -> bool {
- self.scoped
- }
-
- /// Make the property scoped.
- pub fn make_scoped(&mut self) {
- self.scoped = true;
- }
-
- /// What kind of structure the property interrupts.
- pub fn interruption(&self) -> Option<Interruption> {
- if self.is_of::<PageNode>() {
- Some(Interruption::Page)
- } else if self.is_of::<ParNode>() {
- Some(Interruption::Par)
- } else if self.is_of::<ListNode>()
- || self.is_of::<EnumNode>()
- || self.is_of::<DescNode>()
- {
- Some(Interruption::List)
- } else {
- None
- }
- }
-}
-
-impl Debug for Property {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- #[cfg(debug_assertions)]
- write!(f, "{} = ", self.name)?;
- write!(f, "{:?}", self.value)?;
- if self.scoped {
- write!(f, " [scoped]")?;
- }
- Ok(())
- }
-}
-
-impl PartialEq for Property {
- fn eq(&self, other: &Self) -> bool {
- self.key == other.key
- && self.value.eq(&other.value)
- && self.scoped == other.scoped
- }
-}
-
-trait Bounds: Debug + Sync + Send + 'static {
- fn as_any(&self) -> &dyn Any;
-}
-
-impl<T> Bounds for T
-where
- T: Debug + Sync + Send + 'static,
-{
- fn as_any(&self) -> &dyn Any {
- self
- }
-}
-
-/// A style property key.
-///
-/// This trait is not intended to be implemented manually, but rather through
-/// the `#[node]` proc-macro.
-pub trait Key<'a>: Copy + 'static {
- /// The unfolded type which this property is stored as in a style map.
- type Value: Debug + Clone + Hash + Sync + Send + 'static;
-
- /// The folded type of value that is returned when reading this property
- /// from a style chain.
- type Output;
-
- /// The name of the property, used for debug printing.
- const NAME: &'static str;
-
- /// The id of the node the key belongs to.
- fn node() -> NodeId;
-
- /// Compute an output value from a sequence of values belonging to this key,
- /// folding if necessary.
- fn get(
- chain: StyleChain<'a>,
- values: impl Iterator<Item = &'a Self::Value>,
- ) -> Self::Output;
-}
-
-/// A unique identifier for a property key.
-#[derive(Copy, Clone, Eq, PartialEq, Hash)]
-struct KeyId(ReadableTypeId);
-
-impl KeyId {
- /// The id of the given key.
- pub fn of<'a, T: Key<'a>>() -> Self {
- Self(ReadableTypeId::of::<T>())
- }
-}
-
-impl Debug for KeyId {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- self.0.fmt(f)
- }
-}
-
-/// A scoped property barrier.
-///
-/// Barriers interact with [scoped](super::StyleMap::scoped) styles: A scoped
-/// style can still be read through a single barrier (the one of the node it
-/// _should_ apply to), but a second barrier will make it invisible.
-#[derive(Copy, Clone, Eq, PartialEq, Hash)]
-pub struct Barrier(NodeId);
-
-impl Barrier {
- /// Create a new barrier for the given node.
- pub fn new(node: NodeId) -> Self {
- Self(node)
- }
-
- /// Whether this barrier is for the node `T`.
- pub fn is_for(&self, node: NodeId) -> bool {
- self.0 == node
- }
-}
-
-impl Debug for Barrier {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- write!(f, "Barrier for {:?}", self.0)
- }
-}
diff --git a/src/model/realize.rs b/src/model/realize.rs
deleted file mode 100644
index a99abdd3..00000000
--- a/src/model/realize.rs
+++ /dev/null
@@ -1,486 +0,0 @@
-use std::mem;
-
-use comemo::Tracked;
-use typed_arena::Arena;
-
-use super::collapse::CollapsingBuilder;
-use super::{
- Barrier, Content, Interruption, Layout, Level, Node, SequenceNode, Show, StyleChain,
- StyleEntry, StyleMap, StyleVecBuilder, StyledNode, Target,
-};
-use crate::diag::SourceResult;
-use crate::geom::Numeric;
-use crate::library::layout::{
- ColbreakNode, FlowChild, FlowNode, HNode, PageNode, PagebreakNode, PlaceNode, VNode,
-};
-use crate::library::structure::{DocNode, ListItem, ListNode, DESC, ENUM, LIST};
-use crate::library::text::{
- LinebreakNode, ParChild, ParNode, ParbreakNode, SmartQuoteNode, SpaceNode, TextNode,
-};
-use crate::World;
-
-/// Builds a document or a flow node from content.
-pub(super) struct Builder<'a> {
- /// The core context.
- world: Tracked<'a, dyn World>,
- /// Scratch arenas for building.
- scratch: &'a Scratch<'a>,
- /// The current document building state.
- doc: Option<DocBuilder<'a>>,
- /// The current flow building state.
- flow: FlowBuilder<'a>,
- /// The current paragraph building state.
- par: ParBuilder<'a>,
- /// The current list building state.
- list: ListBuilder<'a>,
-}
-
-/// Temporary storage arenas for building.
-#[derive(Default)]
-pub(super) struct Scratch<'a> {
- /// An arena where intermediate style chains are stored.
- styles: Arena<StyleChain<'a>>,
- /// An arena where intermediate content resulting from show rules is stored.
- templates: Arena<Content>,
-}
-
-impl<'a> Builder<'a> {
- pub fn new(
- world: Tracked<'a, dyn World>,
- scratch: &'a Scratch<'a>,
- top: bool,
- ) -> Self {
- Self {
- world,
- scratch,
- doc: top.then(|| DocBuilder::default()),
- flow: FlowBuilder::default(),
- par: ParBuilder::default(),
- list: ListBuilder::default(),
- }
- }
-
- pub fn into_doc(
- mut self,
- styles: StyleChain<'a>,
- ) -> SourceResult<(DocNode, StyleChain<'a>)> {
- self.interrupt(Interruption::Page, styles, true)?;
- let (pages, shared) = self.doc.unwrap().pages.finish();
- Ok((DocNode(pages), shared))
- }
-
- pub fn into_flow(
- mut self,
- styles: StyleChain<'a>,
- ) -> SourceResult<(FlowNode, StyleChain<'a>)> {
- self.interrupt(Interruption::Par, styles, false)?;
- let (children, shared) = self.flow.0.finish();
- Ok((FlowNode(children), shared))
- }
-
- pub fn accept(
- &mut self,
- content: &'a Content,
- styles: StyleChain<'a>,
- ) -> SourceResult<()> {
- if let Some(node) = content.downcast::<TextNode>() {
- if let Some(realized) = styles.apply(self.world, Target::Text(&node.0))? {
- let stored = self.scratch.templates.alloc(realized);
- return self.accept(stored, styles);
- }
- } else if let Some(styled) = content.downcast::<StyledNode>() {
- return self.styled(styled, styles);
- } else if let Some(seq) = content.downcast::<SequenceNode>() {
- return self.sequence(seq, styles);
- } else if content.has::<dyn Show>() {
- if self.show(&content, styles)? {
- return Ok(());
- }
- }
-
- if self.list.accept(content, styles) {
- return Ok(());
- }
-
- self.interrupt(Interruption::List, styles, false)?;
-
- if content.is::<ListItem>() {
- self.list.accept(content, styles);
- return Ok(());
- }
-
- if self.par.accept(content, styles) {
- return Ok(());
- }
-
- self.interrupt(Interruption::Par, styles, false)?;
-
- if self.flow.accept(content, styles) {
- return Ok(());
- }
-
- let keep = content.downcast::<PagebreakNode>().map_or(false, |node| !node.weak);
- self.interrupt(Interruption::Page, styles, keep)?;
-
- if let Some(doc) = &mut self.doc {
- doc.accept(content, styles);
- }
-
- // We might want to issue a warning or error for content that wasn't
- // handled (e.g. a pagebreak in a flow building process). However, we
- // don't have the spans here at the moment.
- Ok(())
- }
-
- fn show(&mut self, node: &'a Content, styles: StyleChain<'a>) -> SourceResult<bool> {
- if let Some(mut realized) = styles.apply(self.world, Target::Node(node))? {
- let mut map = StyleMap::new();
- let barrier = Barrier::new(node.id());
- map.push(StyleEntry::Barrier(barrier));
- map.push(StyleEntry::Barrier(barrier));
- realized = realized.styled_with_map(map);
- let stored = self.scratch.templates.alloc(realized);
- self.accept(stored, styles)?;
- Ok(true)
- } else {
- Ok(false)
- }
- }
-
- fn styled(
- &mut self,
- styled: &'a StyledNode,
- styles: StyleChain<'a>,
- ) -> SourceResult<()> {
- let stored = self.scratch.styles.alloc(styles);
- let styles = styled.map.chain(stored);
- let intr = styled.map.interruption();
-
- if let Some(intr) = intr {
- self.interrupt(intr, styles, false)?;
- }
-
- self.accept(&styled.sub, styles)?;
-
- if let Some(intr) = intr {
- self.interrupt(intr, styles, true)?;
- }
-
- Ok(())
- }
-
- fn interrupt(
- &mut self,
- intr: Interruption,
- styles: StyleChain<'a>,
- keep: bool,
- ) -> SourceResult<()> {
- if intr >= Interruption::List && !self.list.is_empty() {
- mem::take(&mut self.list).finish(self)?;
- }
-
- if intr >= Interruption::Par {
- if !self.par.is_empty() {
- mem::take(&mut self.par).finish(self);
- }
- }
-
- if intr >= Interruption::Page {
- if let Some(doc) = &mut self.doc {
- if !self.flow.is_empty() || (doc.keep_next && keep) {
- mem::take(&mut self.flow).finish(doc, styles);
- }
- doc.keep_next = !keep;
- }
- }
-
- Ok(())
- }
-
- fn sequence(
- &mut self,
- seq: &'a SequenceNode,
- styles: StyleChain<'a>,
- ) -> SourceResult<()> {
- for content in &seq.0 {
- self.accept(content, styles)?;
- }
- Ok(())
- }
-}
-
-/// Accepts pagebreaks and pages.
-struct DocBuilder<'a> {
- /// The page runs built so far.
- pages: StyleVecBuilder<'a, PageNode>,
- /// Whether to keep a following page even if it is empty.
- keep_next: bool,
-}
-
-impl<'a> DocBuilder<'a> {
- fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) {
- if let Some(pagebreak) = content.downcast::<PagebreakNode>() {
- self.keep_next = !pagebreak.weak;
- }
-
- if let Some(page) = content.downcast::<PageNode>() {
- self.pages.push(page.clone(), styles);
- self.keep_next = false;
- }
- }
-}
-
-impl Default for DocBuilder<'_> {
- fn default() -> Self {
- Self {
- pages: StyleVecBuilder::new(),
- keep_next: true,
- }
- }
-}
-
-/// Accepts flow content.
-#[derive(Default)]
-struct FlowBuilder<'a>(CollapsingBuilder<'a, FlowChild>);
-
-impl<'a> FlowBuilder<'a> {
- fn accept(&mut self, content: &'a 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
-
- if let Some(_) = content.downcast::<ParbreakNode>() {
- /* Nothing to do */
- } 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.amount);
- let frac = vertical.amount.is_fractional();
- if vertical.weak {
- let weakness = 1 + u8::from(frac) + 2 * u8::from(vertical.generated);
- self.0.weak(child, styles, weakness);
- } else if frac {
- self.0.destructive(child, styles);
- } else {
- self.0.ignorant(child, styles);
- }
- } else if content.has::<dyn Layout>() {
- let child = FlowChild::Node(content.clone());
- if content.is::<PlaceNode>() {
- self.0.ignorant(child, styles);
- } else {
- self.0.supportive(child, styles);
- }
- } else {
- return false;
- }
-
- true
- }
-
- fn par(&mut self, par: ParNode, styles: StyleChain<'a>, indent: bool) {
- let amount = if indent && !styles.get(ParNode::SPACING_AND_INDENT) {
- styles.get(ParNode::LEADING).into()
- } else {
- styles.get(ParNode::SPACING).into()
- };
-
- self.0.weak(FlowChild::Spacing(amount), styles, 5);
- self.0.supportive(FlowChild::Node(par.pack()), styles);
- self.0.weak(FlowChild::Spacing(amount), styles, 5);
- }
-
- fn finish(self, doc: &mut DocBuilder<'a>, styles: StyleChain<'a>) {
- let (flow, shared) = self.0.finish();
- let styles = if flow.is_empty() { styles } else { shared };
- let node = PageNode(FlowNode(flow).pack());
- doc.pages.push(node, styles);
- }
-
- fn is_empty(&self) -> bool {
- self.0.is_empty()
- }
-}
-
-/// Accepts paragraph content.
-#[derive(Default)]
-struct ParBuilder<'a>(CollapsingBuilder<'a, ParChild>);
-
-impl<'a> ParBuilder<'a> {
- fn accept(&mut self, content: &'a 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(node) = content.downcast::<TextNode>() {
- self.0.supportive(ParChild::Text(node.0.clone()), styles);
- } else if let Some(node) = content.to::<dyn Layout>() {
- if node.level() == Level::Inline {
- self.0.supportive(ParChild::Node(content.clone()), styles);
- } else {
- return false;
- }
- } else {
- return false;
- }
-
- true
- }
-
- fn finish(self, parent: &mut Builder<'a>) {
- let (mut children, shared) = self.0.finish();
- if children.is_empty() {
- return;
- }
-
- // Paragraph indent should only apply if the paragraph starts with
- // text and follows directly after another paragraph.
- let indent = shared.get(ParNode::INDENT);
- if !indent.is_zero()
- && children
- .items()
- .find_map(|child| match child {
- ParChild::Spacing(_) => None,
- ParChild::Text(_) | ParChild::Quote { .. } => Some(true),
- ParChild::Node(_) => Some(false),
- })
- .unwrap_or_default()
- && parent
- .flow
- .0
- .items()
- .rev()
- .find_map(|child| match child {
- FlowChild::Spacing(_) => None,
- FlowChild::Node(node) => Some(node.is::<ParNode>()),
- FlowChild::Colbreak => Some(false),
- })
- .unwrap_or_default()
- {
- children.push_front(ParChild::Spacing(indent.into()));
- }
-
- parent.flow.par(ParNode(children), shared, !indent.is_zero());
- }
-
- fn is_empty(&self) -> bool {
- self.0.is_empty()
- }
-}
-
-/// Accepts list / enum items, spaces, paragraph breaks.
-struct ListBuilder<'a> {
- /// The list items collected so far.
- items: StyleVecBuilder<'a, ListItem>,
- /// Whether the list contains no paragraph breaks.
- tight: bool,
- /// Whether the list can be attached.
- attachable: bool,
- /// Trailing content for which it is unclear whether it is part of the list.
- staged: Vec<(&'a Content, StyleChain<'a>)>,
-}
-
-impl<'a> ListBuilder<'a> {
- fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool {
- if self.items.is_empty() {
- if content.is::<ParbreakNode>() {
- self.attachable = false;
- } else if !content.is::<SpaceNode>() && !content.is::<ListItem>() {
- self.attachable = true;
- }
- }
-
- if let Some(item) = content.downcast::<ListItem>() {
- if self
- .items
- .items()
- .next()
- .map_or(true, |first| item.kind() == first.kind())
- {
- self.items.push(item.clone(), styles);
- self.tight &= self.staged.drain(..).all(|(t, _)| !t.is::<ParbreakNode>());
- } else {
- return false;
- }
- } else if !self.items.is_empty()
- && (content.is::<SpaceNode>() || content.is::<ParbreakNode>())
- {
- self.staged.push((content, styles));
- } else {
- return false;
- }
-
- true
- }
-
- fn finish(self, parent: &mut Builder<'a>) -> SourceResult<()> {
- let (items, shared) = self.items.finish();
- let kind = match items.items().next() {
- Some(item) => item.kind(),
- None => return Ok(()),
- };
-
- let tight = self.tight;
- let attached = tight && self.attachable;
- let content = match kind {
- LIST => ListNode::<LIST> { tight, attached, items }.pack(),
- ENUM => ListNode::<ENUM> { tight, attached, items }.pack(),
- DESC | _ => ListNode::<DESC> { tight, attached, items }.pack(),
- };
-
- let stored = parent.scratch.templates.alloc(content);
- parent.accept(stored, shared)?;
-
- for (content, styles) in self.staged {
- parent.accept(content, styles)?;
- }
-
- parent.list.attachable = true;
-
- Ok(())
- }
-
- fn is_empty(&self) -> bool {
- self.items.is_empty()
- }
-}
-
-impl Default for ListBuilder<'_> {
- fn default() -> Self {
- Self {
- items: StyleVecBuilder::default(),
- tight: true,
- attachable: true,
- staged: vec![],
- }
- }
-}
diff --git a/src/model/recipe.rs b/src/model/recipe.rs
deleted file mode 100644
index c687f4fb..00000000
--- a/src/model/recipe.rs
+++ /dev/null
@@ -1,185 +0,0 @@
-use std::fmt::{self, Debug, Formatter};
-use std::hash::Hash;
-
-use comemo::Tracked;
-
-use super::{
- Args, Capability, Content, Func, Interruption, Node, NodeId, Regex, StyleChain,
- StyleEntry, Value,
-};
-use crate::diag::SourceResult;
-use crate::library::structure::{DescNode, EnumNode, ListNode};
-use crate::library::text::TextNode;
-use crate::syntax::Spanned;
-use crate::World;
-
-/// A show rule recipe.
-#[derive(Clone, PartialEq, Hash)]
-pub struct Recipe {
- /// The patterns to customize.
- pub pattern: Pattern,
- /// The function that defines the recipe.
- pub func: Spanned<Func>,
-}
-
-impl Recipe {
- /// Whether the recipe is applicable to the target.
- pub fn applicable(&self, target: Target) -> bool {
- match (&self.pattern, target) {
- (Pattern::Node(id), Target::Node(node)) => *id == node.id(),
- (Pattern::Regex(_), Target::Text(_)) => true,
- _ => false,
- }
- }
-
- /// Try to apply the recipe to the target.
- pub fn apply(
- &self,
- world: Tracked<dyn World>,
- sel: Selector,
- target: Target,
- ) -> SourceResult<Option<Content>> {
- let content = match (target, &self.pattern) {
- (Target::Node(node), &Pattern::Node(id)) if node.id() == id => {
- let node = node.to::<dyn Show>().unwrap().unguard_parts(sel);
- self.call(world, || Value::Content(node))?
- }
-
- (Target::Text(text), Pattern::Regex(regex)) => {
- let mut result = vec![];
- let mut cursor = 0;
-
- for mat in regex.find_iter(text) {
- let start = mat.start();
- if cursor < start {
- result.push(TextNode(text[cursor .. start].into()).pack());
- }
-
- result.push(self.call(world, || Value::Str(mat.as_str().into()))?);
- cursor = mat.end();
- }
-
- if result.is_empty() {
- return Ok(None);
- }
-
- if cursor < text.len() {
- result.push(TextNode(text[cursor ..].into()).pack());
- }
-
- Content::sequence(result)
- }
-
- _ => return Ok(None),
- };
-
- Ok(Some(content.styled_with_entry(StyleEntry::Guard(sel))))
- }
-
- /// Call the recipe function, with the argument if desired.
- fn call<F>(&self, world: Tracked<dyn World>, arg: F) -> SourceResult<Content>
- where
- F: FnOnce() -> Value,
- {
- let args = if self.func.v.argc() == Some(0) {
- Args::new(self.func.span, [])
- } else {
- Args::new(self.func.span, [arg()])
- };
-
- Ok(self.func.v.call_detached(world, args)?.display(world))
- }
-
- /// What kind of structure the property interrupts.
- pub fn interruption(&self) -> Option<Interruption> {
- if let Pattern::Node(id) = self.pattern {
- if id == NodeId::of::<ListNode>()
- || id == NodeId::of::<EnumNode>()
- || id == NodeId::of::<DescNode>()
- {
- return Some(Interruption::List);
- }
- }
-
- None
- }
-}
-
-impl Debug for Recipe {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- write!(
- f,
- "Recipe matching {:?} from {:?}",
- self.pattern, self.func.span
- )
- }
-}
-
-/// A show rule pattern that may match a target.
-#[derive(Debug, Clone, PartialEq, Hash)]
-pub enum Pattern {
- /// Defines the appearence of some node.
- Node(NodeId),
- /// Defines text to be replaced.
- Regex(Regex),
-}
-
-impl Pattern {
- /// Define a simple text replacement pattern.
- pub fn text(text: &str) -> Self {
- Self::Regex(Regex::new(&regex::escape(text)).unwrap())
- }
-}
-
-/// A target for a show rule recipe.
-#[derive(Debug, Copy, Clone, PartialEq)]
-pub enum Target<'a> {
- /// A showable node.
- Node(&'a Content),
- /// A slice of text.
- Text(&'a str),
-}
-
-/// Identifies a show rule recipe.
-#[derive(Debug, Copy, Clone, PartialEq, Hash)]
-pub enum Selector {
- /// The nth recipe from the top of the chain.
- Nth(usize),
- /// The base recipe for a kind of node.
- Base(NodeId),
-}
-
-/// A node that can be realized given some styles.
-pub trait Show: 'static + Sync + Send {
- /// Unguard nested content against recursive show rules.
- fn unguard_parts(&self, sel: Selector) -> Content;
-
- /// Access a field on this node.
- fn field(&self, name: &str) -> Option<Value>;
-
- /// The base recipe for this node that is executed if there is no
- /// user-defined show rule.
- fn realize(
- &self,
- world: Tracked<dyn World>,
- styles: StyleChain,
- ) -> SourceResult<Content>;
-
- /// Finalize this node given the realization of a base or user recipe. Use
- /// this for effects that should work even in the face of a user-defined
- /// show rule, for example:
- /// - Application of general settable properties
- ///
- /// Defaults to just the realized content.
- #[allow(unused_variables)]
- fn finalize(
- &self,
- world: Tracked<dyn World>,
- styles: StyleChain,
- realized: Content,
- ) -> SourceResult<Content> {
- Ok(realized)
- }
-}
-
-impl Capability for dyn Show {}
diff --git a/src/model/resolve.rs b/src/model/resolve.rs
deleted file mode 100644
index 1ca4be69..00000000
--- a/src/model/resolve.rs
+++ /dev/null
@@ -1,84 +0,0 @@
-use super::{Smart, StyleChain};
-use crate::geom::{Abs, Axes, Corners, Em, Length, Numeric, Rel, Sides};
-use crate::library::text::TextNode;
-
-/// A property that is resolved with other properties from the style chain.
-pub trait Resolve {
- /// The type of the resolved output.
- type Output;
-
- /// Resolve the value using the style chain.
- fn resolve(self, styles: StyleChain) -> Self::Output;
-}
-
-impl Resolve for Em {
- type Output = Abs;
-
- fn resolve(self, styles: StyleChain) -> Self::Output {
- if self.is_zero() {
- Abs::zero()
- } else {
- self.at(styles.get(TextNode::SIZE))
- }
- }
-}
-
-impl Resolve for Length {
- type Output = Abs;
-
- fn resolve(self, styles: StyleChain) -> Self::Output {
- self.abs + self.em.resolve(styles)
- }
-}
-
-impl<T: Resolve> Resolve for Option<T> {
- type Output = Option<T::Output>;
-
- fn resolve(self, styles: StyleChain) -> Self::Output {
- self.map(|v| v.resolve(styles))
- }
-}
-
-impl<T: Resolve> Resolve for Smart<T> {
- type Output = Smart<T::Output>;
-
- fn resolve(self, styles: StyleChain) -> Self::Output {
- self.map(|v| v.resolve(styles))
- }
-}
-
-impl<T: Resolve> Resolve for Axes<T> {
- type Output = Axes<T::Output>;
-
- fn resolve(self, styles: StyleChain) -> Self::Output {
- self.map(|v| v.resolve(styles))
- }
-}
-
-impl<T: Resolve> Resolve for Sides<T> {
- type Output = Sides<T::Output>;
-
- fn resolve(self, styles: StyleChain) -> Self::Output {
- self.map(|v| v.resolve(styles))
- }
-}
-
-impl<T: Resolve> Resolve for Corners<T> {
- type Output = Corners<T::Output>;
-
- fn resolve(self, styles: StyleChain) -> Self::Output {
- self.map(|v| v.resolve(styles))
- }
-}
-
-impl<T> Resolve for Rel<T>
-where
- T: Resolve + Numeric,
- <T as Resolve>::Output: Numeric,
-{
- type Output = Rel<<T as Resolve>::Output>;
-
- fn resolve(self, styles: StyleChain) -> Self::Output {
- self.map(|abs| abs.resolve(styles))
- }
-}
diff --git a/src/model/str.rs b/src/model/str.rs
index 62b37845..843da9a8 100644
--- a/src/model/str.rs
+++ b/src/model/str.rs
@@ -5,8 +5,9 @@ use std::ops::{Add, AddAssign, Deref};
use unicode_segmentation::UnicodeSegmentation;
-use super::{Array, Dict, RawAlign, Value};
+use super::{Array, Dict, Value};
use crate::diag::StrResult;
+use crate::library::RawAlign;
use crate::util::EcoString;
/// Create a new [`Str`] from a format string.
diff --git a/src/model/styles.rs b/src/model/styles.rs
index d160a4a6..c58a1beb 100644
--- a/src/model/styles.rs
+++ b/src/model/styles.rs
@@ -1,12 +1,20 @@
+use std::any::Any;
use std::fmt::{self, Debug, Formatter};
use std::hash::Hash;
use std::iter;
use std::marker::PhantomData;
+use std::sync::Arc;
-use comemo::Tracked;
+use comemo::{Prehashed, Tracked};
-use super::{Barrier, Content, Key, Property, Recipe, Selector, Show, Target};
+use super::{capability, Args, Content, Func, Node, NodeId, Regex, Smart, Value};
use crate::diag::SourceResult;
+use crate::geom::{Abs, Axes, Corners, Em, Length, Numeric, Rel, Sides};
+use crate::library::layout::PageNode;
+use crate::library::structure::{DescNode, EnumNode, ListNode};
+use crate::library::text::{ParNode, TextNode};
+use crate::syntax::Spanned;
+use crate::util::ReadableTypeId;
use crate::World;
/// A map of style properties.
@@ -618,3 +626,516 @@ impl<'a, T> Default for StyleVecBuilder<'a, T> {
Self::new()
}
}
+
+/// A style property originating from a set rule or constructor.
+#[derive(Clone, Hash)]
+pub struct Property {
+ /// The id of the property's [key](Key).
+ key: KeyId,
+ /// The id of the node the property belongs to.
+ node: NodeId,
+ /// Whether the property should only affect the first node down the
+ /// hierarchy. Used by constructors.
+ scoped: bool,
+ /// The property's value.
+ value: Arc<Prehashed<dyn Bounds>>,
+ /// The name of the property.
+ #[cfg(debug_assertions)]
+ name: &'static str,
+}
+
+impl Property {
+ /// Create a new property from a key-value pair.
+ pub fn new<'a, K: Key<'a>>(_: K, value: K::Value) -> Self {
+ Self {
+ key: KeyId::of::<K>(),
+ node: K::node(),
+ value: Arc::new(Prehashed::new(value)),
+ scoped: false,
+ #[cfg(debug_assertions)]
+ name: K::NAME,
+ }
+ }
+
+ /// Whether this property has the given key.
+ pub fn is<'a, K: Key<'a>>(&self) -> bool {
+ self.key == KeyId::of::<K>()
+ }
+
+ /// Whether this property belongs to the node `T`.
+ pub fn is_of<T: 'static>(&self) -> bool {
+ self.node == NodeId::of::<T>()
+ }
+
+ /// Access the property's value if it is of the given key.
+ pub fn downcast<'a, K: Key<'a>>(&'a self) -> Option<&'a K::Value> {
+ if self.key == KeyId::of::<K>() {
+ (**self.value).as_any().downcast_ref()
+ } else {
+ None
+ }
+ }
+
+ /// The node this property is for.
+ pub fn node(&self) -> NodeId {
+ self.node
+ }
+
+ /// Whether the property is scoped.
+ pub fn scoped(&self) -> bool {
+ self.scoped
+ }
+
+ /// Make the property scoped.
+ pub fn make_scoped(&mut self) {
+ self.scoped = true;
+ }
+
+ /// What kind of structure the property interrupts.
+ pub fn interruption(&self) -> Option<Interruption> {
+ if self.is_of::<PageNode>() {
+ Some(Interruption::Page)
+ } else if self.is_of::<ParNode>() {
+ Some(Interruption::Par)
+ } else if self.is_of::<ListNode>()
+ || self.is_of::<EnumNode>()
+ || self.is_of::<DescNode>()
+ {
+ Some(Interruption::List)
+ } else {
+ None
+ }
+ }
+}
+
+impl Debug for Property {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ #[cfg(debug_assertions)]
+ write!(f, "{} = ", self.name)?;
+ write!(f, "{:?}", self.value)?;
+ if self.scoped {
+ write!(f, " [scoped]")?;
+ }
+ Ok(())
+ }
+}
+
+impl PartialEq for Property {
+ fn eq(&self, other: &Self) -> bool {
+ self.key == other.key
+ && self.value.eq(&other.value)
+ && self.scoped == other.scoped
+ }
+}
+
+trait Bounds: Debug + Sync + Send + 'static {
+ fn as_any(&self) -> &dyn Any;
+}
+
+impl<T> Bounds for T
+where
+ T: Debug + Sync + Send + 'static,
+{
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+}
+
+/// A style property key.
+///
+/// This trait is not intended to be implemented manually, but rather through
+/// the `#[node]` proc-macro.
+pub trait Key<'a>: Copy + 'static {
+ /// The unfolded type which this property is stored as in a style map.
+ type Value: Debug + Clone + Hash + Sync + Send + 'static;
+
+ /// The folded type of value that is returned when reading this property
+ /// from a style chain.
+ type Output;
+
+ /// The name of the property, used for debug printing.
+ const NAME: &'static str;
+
+ /// The id of the node the key belongs to.
+ fn node() -> NodeId;
+
+ /// Compute an output value from a sequence of values belonging to this key,
+ /// folding if necessary.
+ fn get(
+ chain: StyleChain<'a>,
+ values: impl Iterator<Item = &'a Self::Value>,
+ ) -> Self::Output;
+}
+
+/// A unique identifier for a property key.
+#[derive(Copy, Clone, Eq, PartialEq, Hash)]
+struct KeyId(ReadableTypeId);
+
+impl KeyId {
+ /// The id of the given key.
+ pub fn of<'a, T: Key<'a>>() -> Self {
+ Self(ReadableTypeId::of::<T>())
+ }
+}
+
+impl Debug for KeyId {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ self.0.fmt(f)
+ }
+}
+
+/// A scoped property barrier.
+///
+/// Barriers interact with [scoped](super::StyleMap::scoped) styles: A scoped
+/// style can still be read through a single barrier (the one of the node it
+/// _should_ apply to), but a second barrier will make it invisible.
+#[derive(Copy, Clone, Eq, PartialEq, Hash)]
+pub struct Barrier(NodeId);
+
+impl Barrier {
+ /// Create a new barrier for the given node.
+ pub fn new(node: NodeId) -> Self {
+ Self(node)
+ }
+
+ /// Whether this barrier is for the node `T`.
+ pub fn is_for(&self, node: NodeId) -> bool {
+ self.0 == node
+ }
+}
+
+impl Debug for Barrier {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ write!(f, "Barrier for {:?}", self.0)
+ }
+}
+
+/// A property that is resolved with other properties from the style chain.
+pub trait Resolve {
+ /// The type of the resolved output.
+ type Output;
+
+ /// Resolve the value using the style chain.
+ fn resolve(self, styles: StyleChain) -> Self::Output;
+}
+
+impl Resolve for Em {
+ type Output = Abs;
+
+ fn resolve(self, styles: StyleChain) -> Self::Output {
+ if self.is_zero() {
+ Abs::zero()
+ } else {
+ self.at(styles.get(TextNode::SIZE))
+ }
+ }
+}
+
+impl Resolve for Length {
+ type Output = Abs;
+
+ fn resolve(self, styles: StyleChain) -> Self::Output {
+ self.abs + self.em.resolve(styles)
+ }
+}
+
+impl<T: Resolve> Resolve for Option<T> {
+ type Output = Option<T::Output>;
+
+ fn resolve(self, styles: StyleChain) -> Self::Output {
+ self.map(|v| v.resolve(styles))
+ }
+}
+
+impl<T: Resolve> Resolve for Smart<T> {
+ type Output = Smart<T::Output>;
+
+ fn resolve(self, styles: StyleChain) -> Self::Output {
+ self.map(|v| v.resolve(styles))
+ }
+}
+
+impl<T: Resolve> Resolve for Axes<T> {
+ type Output = Axes<T::Output>;
+
+ fn resolve(self, styles: StyleChain) -> Self::Output {
+ self.map(|v| v.resolve(styles))
+ }
+}
+
+impl<T: Resolve> Resolve for Sides<T> {
+ type Output = Sides<T::Output>;
+
+ fn resolve(self, styles: StyleChain) -> Self::Output {
+ self.map(|v| v.resolve(styles))
+ }
+}
+
+impl<T: Resolve> Resolve for Corners<T> {
+ type Output = Corners<T::Output>;
+
+ fn resolve(self, styles: StyleChain) -> Self::Output {
+ self.map(|v| v.resolve(styles))
+ }
+}
+
+impl<T> Resolve for Rel<T>
+where
+ T: Resolve + Numeric,
+ <T as Resolve>::Output: Numeric,
+{
+ type Output = Rel<<T as Resolve>::Output>;
+
+ fn resolve(self, styles: StyleChain) -> Self::Output {
+ self.map(|abs| abs.resolve(styles))
+ }
+}
+
+/// A property that is folded to determine its final value.
+pub trait Fold {
+ /// The type of the folded output.
+ type Output;
+
+ /// Fold this inner value with an outer folded value.
+ fn fold(self, outer: Self::Output) -> Self::Output;
+}
+
+impl<T> Fold for Option<T>
+where
+ T: Fold,
+ T::Output: Default,
+{
+ type Output = Option<T::Output>;
+
+ fn fold(self, outer: Self::Output) -> Self::Output {
+ self.map(|inner| inner.fold(outer.unwrap_or_default()))
+ }
+}
+
+impl<T> Fold for Smart<T>
+where
+ T: Fold,
+ T::Output: Default,
+{
+ type Output = Smart<T::Output>;
+
+ fn fold(self, outer: Self::Output) -> Self::Output {
+ self.map(|inner| inner.fold(outer.unwrap_or_default()))
+ }
+}
+
+impl<T> Fold for Sides<T>
+where
+ T: Fold,
+{
+ type Output = Sides<T::Output>;
+
+ fn fold(self, outer: Self::Output) -> Self::Output {
+ self.zip(outer, |inner, outer| inner.fold(outer))
+ }
+}
+
+impl Fold for Sides<Option<Rel<Abs>>> {
+ type Output = Sides<Rel<Abs>>;
+
+ fn fold(self, outer: Self::Output) -> Self::Output {
+ self.zip(outer, |inner, outer| inner.unwrap_or(outer))
+ }
+}
+
+impl Fold for Sides<Option<Smart<Rel<Length>>>> {
+ type Output = Sides<Smart<Rel<Length>>>;
+
+ fn fold(self, outer: Self::Output) -> Self::Output {
+ self.zip(outer, |inner, outer| inner.unwrap_or(outer))
+ }
+}
+
+impl<T> Fold for Corners<T>
+where
+ T: Fold,
+{
+ type Output = Corners<T::Output>;
+
+ fn fold(self, outer: Self::Output) -> Self::Output {
+ self.zip(outer, |inner, outer| inner.fold(outer))
+ }
+}
+
+impl Fold for Corners<Option<Rel<Abs>>> {
+ type Output = Corners<Rel<Abs>>;
+
+ fn fold(self, outer: Self::Output) -> Self::Output {
+ self.zip(outer, |inner, outer| inner.unwrap_or(outer))
+ }
+}
+
+/// A show rule recipe.
+#[derive(Clone, PartialEq, Hash)]
+pub struct Recipe {
+ /// The patterns to customize.
+ pub pattern: Pattern,
+ /// The function that defines the recipe.
+ pub func: Spanned<Func>,
+}
+
+impl Recipe {
+ /// Whether the recipe is applicable to the target.
+ pub fn applicable(&self, target: Target) -> bool {
+ match (&self.pattern, target) {
+ (Pattern::Node(id), Target::Node(node)) => *id == node.id(),
+ (Pattern::Regex(_), Target::Text(_)) => true,
+ _ => false,
+ }
+ }
+
+ /// Try to apply the recipe to the target.
+ pub fn apply(
+ &self,
+ world: Tracked<dyn World>,
+ sel: Selector,
+ target: Target,
+ ) -> SourceResult<Option<Content>> {
+ let content = match (target, &self.pattern) {
+ (Target::Node(node), &Pattern::Node(id)) if node.id() == id => {
+ let node = node.to::<dyn Show>().unwrap().unguard_parts(sel);
+ self.call(world, || Value::Content(node))?
+ }
+
+ (Target::Text(text), Pattern::Regex(regex)) => {
+ let mut result = vec![];
+ let mut cursor = 0;
+
+ for mat in regex.find_iter(text) {
+ let start = mat.start();
+ if cursor < start {
+ result.push(TextNode(text[cursor .. start].into()).pack());
+ }
+
+ result.push(self.call(world, || Value::Str(mat.as_str().into()))?);
+ cursor = mat.end();
+ }
+
+ if result.is_empty() {
+ return Ok(None);
+ }
+
+ if cursor < text.len() {
+ result.push(TextNode(text[cursor ..].into()).pack());
+ }
+
+ Content::sequence(result)
+ }
+
+ _ => return Ok(None),
+ };
+
+ Ok(Some(content.styled_with_entry(StyleEntry::Guard(sel))))
+ }
+
+ /// Call the recipe function, with the argument if desired.
+ fn call<F>(&self, world: Tracked<dyn World>, arg: F) -> SourceResult<Content>
+ where
+ F: FnOnce() -> Value,
+ {
+ let args = if self.func.v.argc() == Some(0) {
+ Args::new(self.func.span, [])
+ } else {
+ Args::new(self.func.span, [arg()])
+ };
+
+ Ok(self.func.v.call_detached(world, args)?.display(world))
+ }
+
+ /// What kind of structure the property interrupts.
+ pub fn interruption(&self) -> Option<Interruption> {
+ if let Pattern::Node(id) = self.pattern {
+ if id == NodeId::of::<ListNode>()
+ || id == NodeId::of::<EnumNode>()
+ || id == NodeId::of::<DescNode>()
+ {
+ return Some(Interruption::List);
+ }
+ }
+
+ None
+ }
+}
+
+impl Debug for Recipe {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ write!(
+ f,
+ "Recipe matching {:?} from {:?}",
+ self.pattern, self.func.span
+ )
+ }
+}
+
+/// A show rule pattern that may match a target.
+#[derive(Debug, Clone, PartialEq, Hash)]
+pub enum Pattern {
+ /// Defines the appearence of some node.
+ Node(NodeId),
+ /// Defines text to be replaced.
+ Regex(Regex),
+}
+
+impl Pattern {
+ /// Define a simple text replacement pattern.
+ pub fn text(text: &str) -> Self {
+ Self::Regex(Regex::new(&regex::escape(text)).unwrap())
+ }
+}
+
+/// A target for a show rule recipe.
+#[derive(Debug, Copy, Clone, PartialEq)]
+pub enum Target<'a> {
+ /// A showable node.
+ Node(&'a Content),
+ /// A slice of text.
+ Text(&'a str),
+}
+
+/// Identifies a show rule recipe.
+#[derive(Debug, Copy, Clone, PartialEq, Hash)]
+pub enum Selector {
+ /// The nth recipe from the top of the chain.
+ Nth(usize),
+ /// The base recipe for a kind of node.
+ Base(NodeId),
+}
+
+/// A node that can be realized given some styles.
+#[capability]
+pub trait Show: 'static + Sync + Send {
+ /// Unguard nested content against recursive show rules.
+ fn unguard_parts(&self, sel: Selector) -> Content;
+
+ /// Access a field on this node.
+ fn field(&self, name: &str) -> Option<Value>;
+
+ /// The base recipe for this node that is executed if there is no
+ /// user-defined show rule.
+ fn realize(
+ &self,
+ world: Tracked<dyn World>,
+ styles: StyleChain,
+ ) -> SourceResult<Content>;
+
+ /// Finalize this node given the realization of a base or user recipe. Use
+ /// this for effects that should work even in the face of a user-defined
+ /// show rule, for example:
+ /// - Application of general settable properties
+ ///
+ /// Defaults to just the realized content.
+ #[allow(unused_variables)]
+ fn finalize(
+ &self,
+ world: Tracked<dyn World>,
+ styles: StyleChain,
+ realized: Content,
+ ) -> SourceResult<Content> {
+ Ok(realized)
+ }
+}
diff --git a/src/model/value.rs b/src/model/value.rs
index 4741401c..d68f42a0 100644
--- a/src/model/value.rs
+++ b/src/model/value.rs
@@ -307,7 +307,7 @@ where
}
fn hash128(&self) -> u128 {
- // Also hash the TypeId since nodes with different types but
+ // Also hash the TypeId since values with different types but
// equal data should be different.
let mut state = SipHasher::new();
self.type_id().hash(&mut state);